Custom Template Language: insert outer language code at any place (not only template fragments)

So I managed to create two PSIs for my Template language[1], using `MultiplePsiFilesPerDocumentFileViewProvider`. My template language does not contain complete/syntactically valid "outer language" statements, so I subclass `TemplateDataElementType` [2] to inject some glue to make them valid.

Example Coco code:

Resolver<Position* &pos>   (. std::string foo; .)
=
"IF" "(" (. int beg = la->pos; int col = la->col; int line = la->line; .)
Condition<foo> (. pos = new Position(beg, t->pos, col, line); .)
.

would get translated to C++ similar to

public void Resolver(Position* &pos) {
std::string foo;
...
int beg = la->pos; int col = la->col; int line = la->line;
...
Condition(foo);
...
pos = new Position(beg, t->pos, col, line);
}

Now, inserting code before/after Coco fragments (e.g. where (. and .) are) is easy with `TemplateDataElementType.appendCurrentTemplateToken()`. However, I may have to insert code at places where there are not Coco fragments (e.g. a rule like the "Resolver" above doesn't need to have <arguments>, but is always translated to a function).

How can I do that?

 

[1] <https://github.com/azrdev/coco-idea-plugin/tree/next>

[2] <https://github.com/azrdev/coco-idea-plugin/commit/2a799411d705847f7d4dc3339c2d6315c041cde5#diff-5855d34e888bd520bd440efab55b734a>

13 comments
Official comment

Sorry, I don't understand what you need. :( Which parts of the first fragment are in which language? Which glue precisely do you want to insert where?

sorry for the confusion: In Coco code (my first block) everything inside <...> or (.  .) is outer language (Java/C++/...). Everything else is Coco code.

Thus, the outer language parser would -- without glue -- see the following:

COCO_FRAGMENT
Position* &pos
COCO_FRAGMENT
std::string foo;
COCO_FRAGMENT
int beg = la->pos; int col = la->col; int line = la->line;
COCO_FRAGMENT
foo
COCO_FRAGMENT
pos = new Position(beg, t->pos, col, line);
COCO_FRAGMENT

I'd have to insert a "public void Resolver(" before the "Position* &pos" token, a ") {" after it, and the corresponding "}" at the end where the dot is in Coco code. However, I'm not guaranteed to have the arguments to resolver, nor a (. .) block anywhere. I could also have a rule like this:

Resolver =
Condition (. ...outer code... .)
Condition
.

which would resolve (if run through the Coco parser generator) to something like

public void Resolver() {
Condition();
...outer code...
Condition();
}

In which case the "outer code" still needs that function block around it (from the view of the outer language parser) to be valid, but I've got no token upon which appendCurrentTemplateToken would be called so I can insert my function glue around it.

0

It seems that the current API doesn't allow that, unless you override createTemplateText and do everything yourself.

But if your language is so well-structured, I'm not sure you need MultiplePsiFilesPerDocumentFileViewProvider at all. Have you considered having a single PSI root with C/C++ fragments inside, or added via language injection?

0

Ok, good to know.

What would I do to have only a single PSI? extend InjectedFileViewProvider or SingleRootFileViewProvider? And how would I construct the C++ glue in that case?

I had tried extending LanguageInjector, see https://github.com/azrdev/coco-idea-plugin/commit/e8f0e8278bbfcf6be39292c6aeacc2850f9dfebd#diff-2e071e005e6e1e0def160676ee9eb00f. However I didn't manage (a) to configure the outer language to use, which is simple in my current case by using the ConfigurableTemplateLanguageFileViewProvider marker interface, and (b) to construct the glue code.

 

 

Unrelated: The forum still tells me "Post is pending approval.", can you moderate it?

0

So your language isn't only templating over C++, it supports other languages? Then things get a bit more complicated, but you can still get the language from TemplateDataLanguageMappings and inject it.

With language injection approach, glue isn't something between fragments anymore, it's prefix and suffix instead, separate for every injected fragment. I'd use prefix/suffix combination resulting in a well-formed C++ file for each fragment.

0

> So your language isn't only templating over C++, it supports other languages?

Yes. Coco/R is a parser generator, and I'm building support for its "attributed grammar" files, which are some EBNF variant plus the "instrumentation" code which is part of the generated parser, and allows to do things with the parsed productions/tokens.

 

> you can still get the language from TemplateDataLanguageMappings and inject it.

thx, I'll try that!

 

> With language injection approach, glue isn't something between fragments anymore, it's prefix and suffix instead, separate for every injected fragment

Yes, that was how far I got (see the already linked commit <https://github.com/azrdev/coco-idea-plugin/commit/e8f0e8278bbfcf6be39292c6aeacc2850f9dfebd#diff-2e071e005e6e1e0def160676ee9eb00f>).

However, I didn't get syntax highlighting in the fragments, probably because I didn't set the C++/Java/... language correctly. I'll try again.

0

I tried again using the LanguageInjector, and indeed with the TemplateDataLanguageMappings I could tell the IDE to handle my fragments as the outer language (configurable in the settings dialog). With prefix and suffix I can also construct valid statements, as can be seen in the commit <https://github.com/azrdev/coco-idea-plugin/commit/a054c9c5db986dcc97dc34d54123096fc7971fa6>

 

However, the fragments don't know about each other, so no reference resolution. Can I fix that, or is it impossible with Language Injection?

 

Example:

Coco file header (an outer language block) imports/includes standard library String definition.

Some coco production defines a string parameter, and its body fills that (here token.value were some string defined by the generated parser code):

MethodCall<String name> =
identifier (. name = token.value; .)
.

I'm trying to get name resolution (and suggestions while typing etc) from "name" usage to "name" definition, and from "String" via the import/include statement in the coco file header to the stdlib type.

0

You could use MultiHostInjector to inject into several places at once. But then you'd need to provide some prefix/suffix "glue" between fragments for the template data tree to be well-formed, and those probably would be language-dependent.

0

> You could use MultiHostInjector to inject into several places at once. But then you'd need to provide some prefix/suffix "glue" between fragments for the template data tree to be well-formed

that's what I was looking for, thx!

Judging from HtmlScriptLanguageInjector [1] I'd use this no different than (currently) my LanguageInjector, only startInjecting() and doneInjecting() would have to be called for each injection (why? they're not documented. calling them once for the whole file or once per injected Language I'd understand).

 

> and those probably would be language-dependent.

of course.

 

[1] https://upsource.jetbrains.com/idea-ce/file/idea-ce-52d6fa1c015dda55dadd3e7b0e17a23e6f348112/xml/impl/src/com/intellij/psi/impl/source/html/HtmlScriptLanguageInjector.java

0

You call startInjecting, then as many times addPlace as you need, then doneInjecting. HtmlScriptLanguageInjector just injects into once place for some reason.

0

but getLanguagesToInject() is already called separately for each token where I may inject (so in the html case,for each script-tag, probably). Should I circumvent this somehow, aggregate all these tokens of mine, and then select only one of the getLanguagesToInject calls where I do addPlace for each of these tokens?

Otherwise, as I'm currently doing, I'm calling startInjecting() and doneInjecting() once for each token/place, like the HtmlScriptLanguageInjector. However, all my tokens are marked with the error (for java language) "class or interface expected".

0

You can do something like this:

<code>

class MyInjector implements MultiHostInjector {

 void getLanguagesToInject(MultiHostRegistrar registrar, PsiElement context) {

  if (context instanceof MyStrangeFile) {

    registrar.startInjecting(myStrangeLanguage);

    for each strange comment in the file {

       registrar.addPlace("prefix for that comment", "suffix for that comment", comment, range)

    }

    registrar.doneInjecting();

 }

 }

}

<code>

0

> You can do something like this:

>   if (context instanceof MyStrangeFile) {

 

… which does not get fired, even when my CocoFile implements PsiLanguageInjectionHost and my MultiHostInjector.elementsToInjectIn() returns/includes CocoFile. I conclude the interface is not really made for cases like mine, where the whole file should be subject to a single MultiHostRegistrar?

0

Please sign in to leave a comment.