TemplateBuilderImpl offset issue

I have an intention which currently uses a text template using TemplateManager.createTemplate(). For various reasons I'd like to build my replacement code using the PSI rather than text, and then replace some elements in the resulting tree using a template.

Following some examples in the Community code, I have something now. I create my new PSI tree as usual, record some elements within the new tree to replace, and reformat it. I then create a template builder like this:

 (document/unblock! project (document/from editor))
(let [builder (TemplateBuilderImpl. new-fn)
manager (TemplateManager/getInstance project)]
(doseq [[usage name] name-by-usage]
(.replaceElement builder ^PsiElement usage ^String name (ConstantNode. name) true))
(.startTemplate manager ^Editor editor (.buildInlineTemplate builder))))))

I'm using Clojure here, but hopefully it's somewhat clear - I call PsiDocumentManager.doPostponedOperationsAndUnblockDocument, then create a new TemplateBuilderImpl passing in a PsiElement which contains all the elements to be replaced. I call TemplateBuilderImpl#replaceElement(PsiElement, String, Expression, boolean) on each of my elements that I'd like to replace, and then call TemplateManager.startTemplate(editor, builder.buildInlineTemplate()).

What I'm finding is that the template creates the replacement strings at the wrong offsets, and I can't figure out why. Am I missing a step here in the template creation, or after manipulating the PSI?

7 comments
Comment actions Permalink

Here's an exception I just got testing this at the end of a file:

Incorrect offsets: startOffset=1220, endOffset=1234, text length=1230
java.lang.Throwable: Incorrect offsets: startOffset=1220, endOffset=1234, text length=1230
at com.intellij.openapi.diagnostic.Logger.error(Logger.java:123)
at com.intellij.openapi.editor.impl.DocumentImpl.createRangeMarker(DocumentImpl.java:427)
at com.intellij.openapi.editor.Document.createRangeMarker(Document.java:241)
at com.intellij.codeInsight.template.impl.TemplateState.start(TemplateState.java:385)
at com.intellij.codeInsight.template.impl.TemplateManagerImpl.lambda$startTemplate$2(TemplateManagerImpl.java:189)
at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:141)
at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:109)
at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:99)
at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:85)
at com.intellij.codeInsight.template.impl.TemplateManagerImpl.startTemplate(TemplateManagerImpl.java:192)
at com.intellij.codeInsight.template.impl.TemplateManagerImpl.startTemplate(TemplateManagerImpl.java:209)
at com.intellij.codeInsight.template.impl.TemplateManagerImpl.startTemplate(TemplateManagerImpl.java:151)
at cursive.intentions.functions$convert_anonymous_to_fn.invoke(functions.clj:199)
at cursive.intentions.functions$convert_anonymous_to_fn.invoke(functions.clj:189)
at clojure.lang.Var.invoke(Var.java:388)
at cursive.api.DelayedFn.invoke(DelayedFn.java:41)
at cursive.intentions.ClIntentionAction.invoke(Intentions.kt:53)
at com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper.invoke(IntentionActionWrapper.java:67)
0
Comment actions Permalink

TemplateBuilderImpl constructor takes a PsiElement, covering the whole range to be tempate-d. Where is it in your code?

Have you tried TemplateBuilder#run? It should start at the correct offsets.

Note that doPostponedOperationsAndUnblockDocument can invalidate some PSI, and their offsets would be invalid then.

0
Comment actions Permalink

The PsiElement in the constructor is new-fn in my example, which does cover all the usages to be updated.

TemplateBuilder#run works for the offset issue, thanks! However I still have an issue which is that the template variables are correctly marked but I can't type into them. The first template variable is selected as normal and typing removes the selection, but doesn't update the contents of the template. I can move the caret within the variable, if I move it to the start and begin typing there then I do get an autocomplete popup consistent with the text I've entered, but the typed text isn't shown in the editor, and hitting enter or selecting the autocomplete item doesn't do anything either.

What is the best solution to the PSI invalidation problem - smart pointers?

0
Comment actions Permalink

I'm afraid I can't help by this description, I can only suggest you to debug deeper :(

> What is the best solution to the PSI invalidation problem - smart pointers?

Yes

0
Comment actions Permalink

The problem was my use of ConstantNode. Once I replaced that with an Expression implementation returning the current value of the relevant variable, things worked fine.

Is there any way to say that I don't want any autocomplete at all for a particular template variable? I still get suggestions from the local environment even though I'm returning null from Expression.calculateLookupItems.

0
Comment actions Permalink

Define "any autocomplete" :) Completion autopopup works everywhere you type. You can disable it at specific places, but people would probably still expect it to work.

0
Comment actions Permalink

Hehe, fair enough. This is for new function parameters, but I need a more general solution for autocompleting them that works everywhere so I'll try to fix that instead.

0

Please sign in to leave a comment.