Is it possible to change the Editor language / content type dynamically?

Answered

I'm wondering if it's possible to change the language of the Editor after it was instantiated? E.g. I create an editor as follows:

EditorFactory.getInstance().createEditor(f.getViewProvider().getDocument(), getProject(), language.getAssociatedFileType(), false)

and the language at that point was XML, how can I change it to JSON without creating a new Editor? 

27 comments
Comment actions Permalink
Official comment

Hi Eugene,

It's hard to answer without knowing a real problem you're trying to solve and without knowing the context where you're going to change the language of a created editor.

Likely you want to implement `com.intellij.psi.LanguageSubstitutor` extension.

Comment actions Permalink

OK, let me explain this in more details. I'm working on a custom plugin which allows conversions between various different data formats, e.g. between XML, JSON, CSV, etc. I created a custom editor for the conversion script, which has a main window with the script itself plus multiple tabs for inputs and outputs. Those tabs don't have actual files associated with them, I create PSI files from text, as follows:

PsiFile f = PsiFileFactory.getInstance(getProject()).createFileFromText(language,"");
Editor editor = EditorFactory.getInstance().createEditor(f.getViewProvider().getDocument(), getProject(), language.getAssociatedFileType(), false);

The whole setup looks like this:

Now here's the tricky part. Sometimes I may want to change the mime type of the input directive, e.g. from application/xml to application/json. In this case I want to locate the appropriate tab and change the formatting, syntax highlighting and validation on the fly. I did figure out the syntax highlighting part, but formatting, folding and validation still don't work as expected.

0
Comment actions Permalink

> I did figure out the syntax highlighting part, but formatting, folding and validation still don't work as expected.

I think that file reparsing will help (FileContentUtilCore#reparseFiles), but it shouldn't be like that. I mean it's not a good way to make things work by fixing basic features one by one. There are just too many features to care about. 

I still recommend you to implement a LanguageSubstitutor which will decide what language should be used for a particular file. Just be careful in selecting files to handle, of course, your plugin should not try to substitute language for any file in a project.

The similar approach is used for Scratch files, here is the ScratchFileService that contains substitutor-extension and configuration mapping between files and corresponding languages. And here is LanguageAction located, which changes the language for a scratch file.

0
Comment actions Permalink

Yes, language mapping is not an issue, I already figured that piece out, but I'm still not quite sure how do I update the language in Editor and/or in the PsiFile.

 

0
Comment actions Permalink

From the LanguageAction code it seems like every time the language is changed, a new file is created, is that correct? So is it correct that there's no way to update the language in existing PsiFile and Editor and the new one has to be created?

 

0
Comment actions Permalink

> From the LanguageAction code it seems like every time the language is changed, a new file is created, is that correct?

Indeed, looks like that particular example is not the best one. Couldn't find any better in community. 

 

> So is it correct that there's no way to update the language in existing PsiFile and Editor and the new one has to be created?

No, it's not correct. Sorry, I don't know how to say it in a different way, so I just repeat: I believe that you need a language substitutor and per file mapping to keep language setting for a file. Reparsing file might be required on changing mapping.

0
Comment actions Permalink

Hmmm, tried to reparse file, didn't help...

0
Comment actions Permalink

Details needed. Have you tried to debug it? Does the language substitutor handle file correctly and select proper language?

0
Comment actions Permalink

I'll try to debug it further.

 

0
Comment actions Permalink

Sorry, for reopening the old conversation but I still haven't found the way to change the VirtualFile type on the fly. Here's what happens: I do have a way to resolve the language and get an instance of the Language class based on my settings.  Here's my code

logger.debug("*** NEW LANGUAGE IS " + language);

LanguageSubstitutors.INSTANCE.substituteLanguage(language, vfile, project);
FileContentUtilCore.reparseFiles(vfile);

logger.debug("*** NEW FILE TYPE IS " + vfile.getFileType());

However, the vfile.getFileType still returns an old type. E.g. I created file as XML, then change language to JSON, but the logger output is still:

*** NEW FILE TYPE IS com.intellij.ide.highlighter.XmlFileType@391c708c 

So my question remains, how do I change the file type of a VirtualFile on the fly?

 

0
Comment actions Permalink

Hi Eugene,

It's not supposed to invoke LanguageSubstitutors.INSTANCE.substituteLanguage manually unless you do this from some FileViewProvider. You should implement your own LanguageSubstitutor extension. So the answer is the same: implement substitutor and reparse file there if needed.

0
Comment actions Permalink

Oh, sorry, it seems you changed the language.

> So my question remains, how do I change the file type of a VirtualFile on the fly?

There is no way. VitualFile#getFileType has nothing to do with a language. Actually, it has nothing to do with PSI, it's just a type of file on the virtual file system. If you have `test.xml` file is still XML file whatever the language is inside it.

0
Comment actions Permalink

The thing is, that particular file is not on the file system, I instantiated it as follows:

PsiFile f = PsiFileFactory.getInstance(getProject()).createFileFromText(language, someDataString)

What I'm trying now is to create a new PsiFile and set the editor to its content, but it still doesn't work as expected.

0
Comment actions Permalink

So have you implemented language substitutor or not?

0
Comment actions Permalink

No, I'm still trying to figure out how to implement it based on my situation. I may need a bit more time.

0
Comment actions Permalink

I'd recommend you to read the code of IntelliJ platform and debug all things that you're trying to do.

For example, take a look at https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/psi/LanguageSubstitutors.java#L45, it does NOT substitute language, it asks substitutors to do this. If you didn't implement one – nothing happens. So the invokation you have shown me before does exactly nothing.

The answer is still the same: implement a substitutor which will know what languages in which files should be replaced based on some configuration. Or recreate the PSI file and an editor.

0
Comment actions Permalink

Yep, looks like I can't implement the substitutor because I can't find a way to tell it what is the new language for the file. The language is determined on the fly based on the user input. Not sure how the substitutor will fit this.

 

0
Comment actions Permalink

You still can store the mapping between file and target language somewhere and read this mapping in a language substitutor.The easiest way to do it is to store the language selected by

The easiest way is to store the language selected by user in "PsiFile#putUserData" and then check it (PsiFile#getUserData) in a language substitutor.

0
Comment actions Permalink

OK, so now I implemented a substitutor as a public static inner class of my custom editor (which is also a public class) and registered it in plugin.xml, but when I run it, I get java.lang.ClassNotFoundException. 

0
Comment actions Permalink

I cannot say anything by class name of exception. Stacktrace, content pluign.xml, content of your LanguageSubstitutor class might be helpful

0
Comment actions Permalink

here's the exception:

java.lang.RuntimeException: java.lang.ClassNotFoundException: org.mule.tooling.lang.dw.editor.WeaveEditor.WeaveIOSubstitutor PluginClassLoader[org.mule.tooling.intellij.dataweave, 0.8]

at com.intellij.lang.LanguageExtensionPoint$1.compute(LanguageExtensionPoint.java:45)

at com.intellij.openapi.util.NotNullLazyValue.getValue(NotNullLazyValue.java:39)

at com.intellij.lang.LanguageExtensionPoint.getInstance(LanguageExtensionPoint.java:53)

at com.intellij.openapi.util.KeyedExtensionCollector.buildExtensions(KeyedExtensionCollector.java:155)

at com.intellij.openapi.util.KeyedExtensionCollector.buildExtensions(KeyedExtensionCollector.java:128)

at com.intellij.openapi.util.KeyedExtensionCollector.forKey(KeyedExtensionCollector.java:116)

at com.intellij.psi.LanguageSubstitutors.substituteLanguage(LanguageSubstitutors.java:46)

at com.intellij.lang.LanguageUtil.getLanguageForPsi(LanguageUtil.java:57)

at com.intellij.psi.impl.file.impl.FileManagerImpl.createFileViewProvider(FileManagerImpl.java:283)

at com.intellij.psi.impl.file.impl.FileManagerImpl.findViewProvider(FileManagerImpl.java:224)

at com.intellij.psi.impl.file.impl.FileManagerImpl.findFile(FileManagerImpl.java:406)

at com.intellij.psi.impl.PsiManagerImpl.findFile(PsiManagerImpl.java:182)

at com.intellij.lang.properties.editor.ResourceBundleEditorProvider.lambda$accept$0(ResourceBundleEditorProvider.java:48)

at com.intellij.openapi.application.ReadAction.compute(ReadAction.java:53)

at com.intellij.lang.properties.editor.ResourceBundleEditorProvider.accept(ResourceBundleEditorProvider.java:48)

at com.intellij.openapi.fileEditor.impl.FileEditorProviderManagerImpl$2.compute(FileEditorProviderManagerImpl.java:94)

at com.intellij.openapi.fileEditor.impl.FileEditorProviderManagerImpl$2.compute(FileEditorProviderManagerImpl.java:88)

at com.intellij.openapi.application.impl.ApplicationImpl.runReadAction(ApplicationImpl.java:892)

at com.intellij.openapi.fileEditor.impl.FileEditorProviderManagerImpl.getProviders(FileEditorProviderManagerImpl.java:88)

at com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl.openFileImpl4(FileEditorManagerImpl.java:829)

at com.intellij.openapi.fileEditor.impl.EditorsSplitters$UIBuilder.processFiles(EditorsSplitters.java:861)

at com.intellij.openapi.fileEditor.impl.EditorsSplitters$UIBuilder.processFiles(EditorsSplitters.java:816)

at com.intellij.openapi.fileEditor.impl.EditorsSplitters$ConfigTreeReader.process(EditorsSplitters.java:807)

at com.intellij.openapi.fileEditor.impl.EditorsSplitters.openFiles(EditorsSplitters.java:235)

at com.intellij.openapi.fileEditor.impl.OpenFilesActivity.runActivity(OpenFilesActivity.java:35)

at com.intellij.ide.startup.impl.StartupManagerImpl.lambda$runPostStartupActivitiesFromExtensions$1(StartupManagerImpl.java:146)

at com.intellij.ide.startup.impl.StartupManagerImpl.runActivity(StartupManagerImpl.java:344)

at com.intellij.ide.startup.impl.StartupManagerImpl.runPostStartupActivitiesFromExtensions(StartupManagerImpl.java:150)

at com.intellij.openapi.project.impl.ProjectManagerImpl.lambda$openProject$6(ProjectManagerImpl.java:339)

at com.intellij.openapi.project.impl.ProjectManagerImpl.openProject(ProjectManagerImpl.java:353)

at com.intellij.openapi.project.impl.ProjectManagerImpl$2.compute(ProjectManagerImpl.java:412)

at com.intellij.openapi.project.impl.ProjectManagerImpl$2.compute(ProjectManagerImpl.java:403)

at com.intellij.openapi.progress.Task$WithResult.run(Task.java:307)

at com.intellij.openapi.progress.impl.CoreProgressManager$TaskRunnable.run(CoreProgressManager.java:710)

at com.intellij.openapi.progress.impl.CoreProgressManager$11.run(CoreProgressManager.java:423)

at com.intellij.openapi.progress.impl.CoreProgressManager$3.run(CoreProgressManager.java:179)

at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:568)

at com.intellij.openapi.progress.impl.CoreProgressManager.executeProcessUnderProgress(CoreProgressManager.java:519)

at com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:54)

at com.intellij.openapi.progress.impl.CoreProgressManager.runProcess(CoreProgressManager.java:164)

at com.intellij.openapi.application.impl.ApplicationImpl.lambda$null$9(ApplicationImpl.java:569)

at com.intellij.openapi.application.impl.ApplicationImpl$2.run(ApplicationImpl.java:309)

at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)

at java.util.concurrent.FutureTask.run(FutureTask.java:266)

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

at java.lang.Thread.run(Thread.java:745)

Caused by: java.lang.ClassNotFoundException: org.mule.tooling.lang.dw.editor.WeaveEditor.WeaveIOSubstitutor PluginClassLoader[org.mule.tooling.intellij.dataweave, 0.8]

at com.intellij.ide.plugins.cl.PluginClassLoader.loadClass(PluginClassLoader.java:64)

at java.lang.ClassLoader.loadClass(ClassLoader.java:357)

at java.lang.Class.forName0(Native Method)

at java.lang.Class.forName(Class.java:348)

at com.intellij.openapi.extensions.AbstractExtensionPointBean.findClass(AbstractExtensionPointBean.java:42)

at com.intellij.openapi.extensions.AbstractExtensionPointBean.instantiate(AbstractExtensionPointBean.java:63)

at com.intellij.openapi.extensions.CustomLoadingExtensionPointBean.instantiateExtension(CustomLoadingExtensionPointBean.java:47)

at com.intellij.lang.LanguageExtensionPoint.access$000(LanguageExtensionPoint.java:28)

at com.intellij.lang

0
Comment actions Permalink

plugin.xml has this:

 

<lang.substitutor implementationClass="org.mule.tooling.lang.dw.editor.WeaveEditor.WeaveIOSubstitutor"/>

0
Comment actions Permalink

The code is:

 

public class WeaveEditor implements FileEditor {

...
...

public static class WeaveIOSubstitutor extends LanguageSubstitutor {
@Nullable
@Override
public Language getLanguage(@NotNull VirtualFile file, @NotNull Project project) {
return substituteLanguage(project, file);
}

@Nullable
public static Language substituteLanguage(@NotNull Project project, @NotNull VirtualFile file) {
Language language = PlainTextLanguage.INSTANCE; //Default is plain text
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (file != null) {
logger.debug("*** PSIFile is " + psiFile);
String newDataType = psiFile.getUserData(WeaveEditor.newFileDataTypeKey);
language = WeaveEditor.getLanguage(newDataType);
logger.debug("*** Language is " + language);
} else {
logger.debug("*** PSIFile is NULL ***");
}
return language;
}
}
}

 

0
Comment actions Permalink

Subclasses must be referenced via "$" in plugin.xml:

<lang.substitutor implementationClass="org.mule.tooling.lang.dw.editor.WeaveEditor$WeaveIOSubstitutor"/>
0
Comment actions Permalink

Ahhh, you are right, please pardon my stupidity :)

 

0
Comment actions Permalink

Well, this is getting ugly. I put the new type into the PsiFile, but in the substitutor I only have access to the VirtualFile. So I'm calling PsiManager.findFIle() which is calling the substitutor, causing circular reference and stack overflow. I have a gut feeling that the approach I'm taking is completely wrong... 

 

0
Comment actions Permalink

Then put it in VirtualFile, it's also UserDataHolder

 

0

Please sign in to leave a comment.