From 14f0aa73784ee3c8ba44752694d3670f5fcd865b Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Mon, 9 Feb 2026 14:05:33 +0100 Subject: [PATCH] LSP-561 [lsp] Support live templates completion in LSP GitOrigin-RevId: cc42c46046fd8f24e16e11f3116a9e68d0c164ee --- .../template/JavaLiveTemplateTest.java | 21 ++++- .../template/impl/TemplateImpl.java | 91 ++++++++++++++++--- .../lang/impl/modcommand/PsiUpdateImpl.java | 50 +--------- ...LiveTemplateModCompletionItemProvider.java | 2 +- 4 files changed, 105 insertions(+), 59 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/template/JavaLiveTemplateTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/template/JavaLiveTemplateTest.java index c01bb7140645..f523cb80ce92 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/template/JavaLiveTemplateTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/template/JavaLiveTemplateTest.java @@ -1022,6 +1022,25 @@ public class JavaLiveTemplateTest extends LiveTemplateTestCase { """); } + public void testModCommandSoutm() { + TemplateImpl template = TemplateSettings.getInstance().getTemplate("soutm", "Java"); + configureByJavaText("Test.java", """ + class X { + void test() { + + } + } + """); + runModCommand(template); + myFixture.checkResult(""" + class X { + void test() { + System.out.println("X.test"); + } + } + """); + } + public void testModCommandMain() { TemplateImpl template = TemplateSettings.getInstance().getTemplate("main", "Java"); configureByJavaText("Test.java", """ @@ -1052,7 +1071,7 @@ public class JavaLiveTemplateTest extends LiveTemplateTestCase { myFixture.checkResult(""" class X { void test() { - for (int i = 0; i <; i++) { + for (int i = 0; i < ; i++) { \s } } diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java b/platform/analysis-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java index f00d3e9a46a9..c277683e9a44 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java @@ -3,19 +3,26 @@ package com.intellij.codeInsight.template.impl; import com.intellij.codeInsight.template.Expression; +import com.intellij.codeInsight.template.ExpressionContext; +import com.intellij.codeInsight.template.Result; import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TextResult; import com.intellij.modcommand.ModCommand; import com.intellij.modcommand.ModPsiUpdater; import com.intellij.modcommand.ModTemplateBuilder; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.RangeMarker; import com.intellij.openapi.options.SchemeElement; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; @@ -435,28 +442,46 @@ public class TemplateImpl extends TemplateBase implements SchemeElement { Variable variable = variableMap.get(segment.name); if (variable != null) { manager.commitDocument(document); - PsiElement element = updater.getPsiFile().findElementAt(start + segment.offset); + PsiElement element = updater.getPsiFile().findElementAt(info.marker.getStartOffset()); if (element != null) { - if (builder == null) builder = updater.templateBuilder(); - builder.field(element, info.marker.getTextRange().shiftLeft(element.getTextRange().getStartOffset()), segment.name, - variable.getExpression()); + if (!variable.isAlwaysStopAt()) { + Result result = variable.getExpression().calculateResult(new DummyContext(info.marker.getTextRange(), element, updater.getPsiFile())); + if (result != null) { + document.replaceString(info.marker.getStartOffset(), info.marker.getEndOffset(), result.toString()); + } + } else { + if (builder == null) builder = updater.templateBuilder(); + builder.field(element, info.marker.getTextRange().shiftLeft(element.getTextRange().getStartOffset()), segment.name, + variable.getExpression()); + } } } } if (isToReformat()) { + List emptyValues = new ArrayList<>(); + for (MarkerInfo info : markers) { + if (info.marker.getStartOffset() == info.marker.getEndOffset()) { + document.insertString(info.marker.getStartOffset(), "a"); + emptyValues.add(info); + } + } manager.commitDocument(document); CodeStyleManager.getInstance(updater.getProject()) .reformatText(updater.getPsiFile(), wholeTemplate.getStartOffset(), wholeTemplate.getEndOffset()); - } - if (endMarker != null) { - document.deleteString(endMarker.getStartOffset(), endMarker.getEndOffset()); - if (builder != null) { - builder.finishAt(endMarker.getStartOffset()); - } else { - updater.moveCaretTo(endMarker.getStartOffset()); + for (MarkerInfo value : emptyValues) { + document.deleteString(value.marker.getStartOffset(), value.marker.getEndOffset()); } - endMarker.dispose(); } + if (endMarker == null) { + endMarker = document.createRangeMarker(wholeTemplate.getEndOffset(), wholeTemplate.getEndOffset()); + } + document.deleteString(endMarker.getStartOffset(), endMarker.getEndOffset()); + if (builder != null) { + builder.finishAt(endMarker.getStartOffset()); + } else { + updater.moveCaretTo(endMarker.getStartOffset()); + } + endMarker.dispose(); for (MarkerInfo info : markers) { info.marker.dispose(); } @@ -467,4 +492,46 @@ public class TemplateImpl extends TemplateBase implements SchemeElement { public String toString() { return myGroupName +"/" + myKey; } + + @ApiStatus.Internal + public static class DummyContext implements ExpressionContext { + private final @NotNull TextRange myRange; + private final @NotNull PsiElement myElement; + private final @NotNull PsiFile myFile; + + public DummyContext(@NotNull TextRange range, @NotNull PsiElement element, @NotNull PsiFile file) { + myRange = range; + myElement = element; + myFile = file; + } + + @Override + public Project getProject() { return myFile.getProject(); } + + @Override + public @Nullable PsiFile getPsiFile() { + return myFile; + } + + @Override + public @Nullable Editor getEditor() { return null; } + + @Override + public int getStartOffset() { return myRange.getStartOffset(); } + + @Override + public int getTemplateStartOffset() { return myRange.getStartOffset(); } + + @Override + public int getTemplateEndOffset() { return myRange.getEndOffset(); } + + @Override + public T getProperty(Key key) { return null; } + + @Override + public @Nullable PsiElement getPsiElementAtStartOffset() { return myElement.isValid() ? myElement : null; } + + @Override + public @Nullable TextResult getVariableValue(String variableName) { return null; } + } } diff --git a/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java b/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java index 5dff82edbfef..7c7714d86b41 100644 --- a/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java +++ b/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java @@ -3,9 +3,8 @@ package com.intellij.lang.impl.modcommand; import com.intellij.analysis.AnalysisBundle; import com.intellij.codeInsight.template.Expression; -import com.intellij.codeInsight.template.ExpressionContext; import com.intellij.codeInsight.template.Result; -import com.intellij.codeInsight.template.TextResult; +import com.intellij.codeInsight.template.impl.TemplateImpl; import com.intellij.injected.editor.DocumentWindow; import com.intellij.injected.editor.InjectionEditService; import com.intellij.lang.Language; @@ -31,7 +30,6 @@ import com.intellij.modcommand.ModUpdateReferences; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.RangeMarker; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.editor.event.DocumentEvent; @@ -298,6 +296,7 @@ final class PsiUpdateImpl { private final Consumer<@NotNull Document> myCopyCleaner; private final List myHighlightInfos = new ArrayList<>(); private final List myTemplateFields = new ArrayList<>(); + private final Map myTemplateValues = new HashMap<>(); private final List myLaunchEditorActions = new ArrayList<>(); private @NotNull Function myTemplateFinishFunction = f -> nop(); private @Nullable ModStartRename myRenameSymbol; @@ -521,7 +520,9 @@ final class PsiUpdateImpl { } TextRange rangeForTemplate = templateRange(elementRange, rangeInElement); TextRange range = mapRange(rangeForTemplate); - Result result = expression.calculateResult(new DummyContext(range, element, getPsiFile())); + TextRange rangeForContext = range; + Result result = myTemplateValues.computeIfAbsent(varName, v -> expression.calculateResult(new TemplateImpl.DummyContext( + rangeForContext, element, getPsiFile()))); if (result != null) { FileTracker tracker = requireNonNull(myTracker); // guarded by getRange call String fieldValue = result.toString(); @@ -825,46 +826,5 @@ final class PsiUpdateImpl { if (myTemplateFields.isEmpty()) return nop(); return new ModStartTemplate(navigationFile(), myTemplateFields, myTemplateFinishFunction); } - - private class DummyContext implements ExpressionContext { - private final TextRange myRange; - private final @NotNull PsiElement myElement; - private final @NotNull PsiFile myFile; - - private DummyContext(TextRange range, @NotNull PsiElement element, @NotNull PsiFile file) { - myRange = range; - myElement = element; - myFile = file; - } - - @Override - public Project getProject() { return myActionContext.project(); } - - @Override - public @Nullable PsiFile getPsiFile() { - return myFile; - } - - @Override - public @Nullable Editor getEditor() { return null; } - - @Override - public int getStartOffset() { return myRange.getStartOffset(); } - - @Override - public int getTemplateStartOffset() { return myRange.getStartOffset(); } - - @Override - public int getTemplateEndOffset() { return myRange.getEndOffset(); } - - @Override - public T getProperty(Key key) { return null; } - - @Override - public @Nullable PsiElement getPsiElementAtStartOffset() { return myElement.isValid() ? myElement : null; } - - @Override - public @Nullable TextResult getVariableValue(String variableName) { return null; } - } } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/LiveTemplateModCompletionItemProvider.java b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/LiveTemplateModCompletionItemProvider.java index 55216621d7bb..59174d607f33 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/LiveTemplateModCompletionItemProvider.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/LiveTemplateModCompletionItemProvider.java @@ -25,7 +25,7 @@ final class LiveTemplateModCompletionItemProvider implements ModCompletionItemPr PsiFile file = context.getPosition().getContainingFile(); int offset = context.offset(); List availableTemplates = TemplateManagerImpl.listApplicableTemplates( - TemplateActionContext.expanding(file, offset)); + TemplateActionContext.expanding(file, offset - context.prefix().length())); Map templates = ListTemplatesHandler.filterTemplatesByPrefix(availableTemplates, file.getFileDocument(), offset, false, false); for (Map.Entry entry : templates.entrySet()) {