LSP-561 [lsp] Support live templates completion in LSP

GitOrigin-RevId: cc42c46046fd8f24e16e11f3116a9e68d0c164ee
This commit is contained in:
Tagir Valeev
2026-02-11 15:35:19 +00:00
committed by intellij-monorepo-bot
parent 3eee71e033
commit 14f0aa7378
4 changed files with 105 additions and 59 deletions
@@ -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() {
<caret>
}
}
""");
runModCommand(template);
myFixture.checkResult("""
class X {
void test() {
System.out.println("X.test");<caret>
}
}
""");
}
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 <selection>i<caret></selection> = 0; i <; i++) {
for (int <selection>i<caret></selection> = 0; i < ; i++) {
\s
}
}
@@ -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<MarkerInfo> 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> T getProperty(Key<T> key) { return null; }
@Override
public @Nullable PsiElement getPsiElementAtStartOffset() { return myElement.isValid() ? myElement : null; }
@Override
public @Nullable TextResult getVariableValue(String variableName) { return null; }
}
}
@@ -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<ModHighlight.HighlightInfo> myHighlightInfos = new ArrayList<>();
private final List<ModStartTemplate.TemplateField> myTemplateFields = new ArrayList<>();
private final Map<String, Result> myTemplateValues = new HashMap<>();
private final List<ModLaunchEditorAction> myLaunchEditorActions = new ArrayList<>();
private @NotNull Function<? super @NotNull PsiFile, ? extends @NotNull ModCommand> 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> T getProperty(Key<T> key) { return null; }
@Override
public @Nullable PsiElement getPsiElementAtStartOffset() { return myElement.isValid() ? myElement : null; }
@Override
public @Nullable TextResult getVariableValue(String variableName) { return null; }
}
}
}
@@ -25,7 +25,7 @@ final class LiveTemplateModCompletionItemProvider implements ModCompletionItemPr
PsiFile file = context.getPosition().getContainingFile();
int offset = context.offset();
List<TemplateImpl> availableTemplates = TemplateManagerImpl.listApplicableTemplates(
TemplateActionContext.expanding(file, offset));
TemplateActionContext.expanding(file, offset - context.prefix().length()));
Map<TemplateImpl, String> templates =
ListTemplatesHandler.filterTemplatesByPrefix(availableTemplates, file.getFileDocument(), offset, false, false);
for (Map.Entry<TemplateImpl, String> entry : templates.entrySet()) {