From cf6bbf78d127aaaa38440a8b65f0f854e881a7b6 Mon Sep 17 00:00:00 2001 From: azerr Date: Tue, 11 Jul 2023 11:40:30 +0200 Subject: [PATCH] feat: Improve completion with snippet support (#786) Fixes #786 Signed-off-by: azerr --- .../lsp4ij/internal/SupportedFeatures.java | 4 +- .../completion/LSContentAssistProcessor.java | 11 +- .../LSIncompleteCompletionProposal.java | 164 +++----- .../completion/SnippetTemplateFactory.java | 46 +++ .../completion/SnippetTemplateLoader.java | 111 +++++ .../completion/snippet/Location.java | 80 ++++ .../completion/snippet/LspSnippetHandler.java | 89 ++++ .../completion/snippet/LspSnippetParser.java | 379 ++++++++++++++++++ .../completion/snippet/ParseException.java | 51 +++ .../completion/snippet/AdvancedTest.java | 39 ++ .../completion/snippet/ChoiceTest.java | 29 ++ .../completion/snippet/LspSnippetAssert.java | 69 ++++ .../completion/snippet/PlaceholderTest.java | 29 ++ .../completion/snippet/TabstopTest.java | 52 +++ .../completion/snippet/TextTest.java | 29 ++ .../completion/snippet/VariableTest.java | 52 +++ .../snippet/handler/ChoiceNode.java | 87 ++++ .../handler/LspSnippetHandlerImpl.java | 83 ++++ .../snippet/handler/LspSnippetNode.java | 18 + .../snippet/handler/PlaceholderNode.java | 76 ++++ .../snippet/handler/TabstopNode.java | 55 +++ .../completion/snippet/handler/TextNode.java | 58 +++ .../snippet/handler/VariableNode.java | 58 +++ 23 files changed, 1556 insertions(+), 113 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/SnippetTemplateFactory.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/SnippetTemplateLoader.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/Location.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetHandler.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetParser.java create mode 100644 src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ParseException.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/AdvancedTest.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ChoiceTest.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetAssert.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/PlaceholderTest.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TabstopTest.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TextTest.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/VariableTest.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/ChoiceNode.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetHandlerImpl.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetNode.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/PlaceholderNode.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TabstopNode.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TextNode.java create mode 100644 src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/VariableNode.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/SupportedFeatures.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/SupportedFeatures.java index 24fa8c63e..0d851075a 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/SupportedFeatures.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/SupportedFeatures.java @@ -78,10 +78,10 @@ public class SupportedFeatures { textDocumentClientCapabilities.setInlayHint(new InlayHintCapabilities()); // TODO : support textDocument/colorPresentation // textDocumentClientCapabilities.setColorProvider(new ColorProviderCapabilities()); - final var completionItemCapabilities = new CompletionItemCapabilities(Boolean.FALSE); + final var completionItemCapabilities = new CompletionItemCapabilities(Boolean.TRUE); completionItemCapabilities .setDocumentationFormat(Arrays.asList(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); - // completionItemCapabilities.setInsertTextModeSupport(new CompletionItemInsertTextModeSupportCapabilities(List.of(InsertTextMode.AsIs, InsertTextMode.AdjustIndentation))); + completionItemCapabilities.setInsertTextModeSupport(new CompletionItemInsertTextModeSupportCapabilities(List.of(InsertTextMode.AsIs, InsertTextMode.AdjustIndentation))); // completionItemCapabilities.setResolveSupport(new CompletionItemResolveSupportCapabilities(List.of("documentation", "detail", "additionalTextEdits"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ textDocumentClientCapabilities.setCompletion(new CompletionCapabilities(completionItemCapabilities)); final var definitionCapabilities = new DefinitionCapabilities(); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSContentAssistProcessor.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSContentAssistProcessor.java index 2017d0c26..376c0291c 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSContentAssistProcessor.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSContentAssistProcessor.java @@ -54,18 +54,17 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No Project project = parameters.getOriginalFile().getProject(); int offset = parameters.getOffset(); CompletableFuture>> completionLanguageServersFuture = initiateLanguageServers(project, document); - CompletionParams param; try { /* process the responses out of the completable loop as it may cause deadlock if user is typing more characters as toProposals will require as read lock that this thread already have and async processing is occuring on a separate thread. */ - param = LSPIJUtils.toCompletionParams(LSPIJUtils.toUri(document), offset, document); + CompletionParams params = LSPIJUtils.toCompletionParams(LSPIJUtils.toUri(document), offset, document); BlockingDeque, CompletionList>, LanguageServer>> proposals = new LinkedBlockingDeque<>(); CompletableFuture future = completionLanguageServersFuture .thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> languageServer.getSecond().getTextDocumentService().completion(param) + .map(languageServer -> languageServer.getSecond().getTextDocumentService().completion(params) .thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer.getSecond())))) .toArray(CompletableFuture[]::new))); while (!future.isDone() || !proposals.isEmpty()) { @@ -77,7 +76,7 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No } } - } catch (ProcessCanceledException cancellation){ + } catch (ProcessCanceledException cancellation) { throw cancellation; } catch (RuntimeException | InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); @@ -90,7 +89,7 @@ private Collection toProposals(Project project, Editor int offset, Either, CompletionList> completion, LanguageServer languageServer) { if (completion != null) { - List items = completion.isLeft()?completion.getLeft():completion.getRight().getItems(); + List items = completion.isLeft() ? completion.getLeft() : completion.getRight().getItems(); boolean isIncomplete = completion.isRight() && completion.getRight().isIncomplete(); return items.stream().map(item -> createLookupItem(project, editor, offset, item, isIncomplete, languageServer)). filter(item -> item.validate(document, offset, null)). @@ -103,7 +102,7 @@ private Collection toProposals(Project project, Editor private LSIncompleteCompletionProposal createLookupItem(Project project, Editor editor, int offset, CompletionItem item, boolean isIncomplete, LanguageServer languageServer) { - return isIncomplete?new LSIncompleteCompletionProposal(editor, offset, item, languageServer): + return isIncomplete ? new LSIncompleteCompletionProposal(editor, offset, item, languageServer) : new LSCompletionProposal(editor, offset, item, languageServer); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSIncompleteCompletionProposal.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSIncompleteCompletionProposal.java index d8d702b5a..b825408cc 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSIncompleteCompletionProposal.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSIncompleteCompletionProposal.java @@ -15,6 +15,10 @@ import com.intellij.codeInsight.completion.InsertionContext; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateManager; +import com.intellij.codeInsight.template.impl.ConstantNode; +import com.intellij.codeInsight.template.impl.TemplateImpl; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.event.DocumentEvent; @@ -27,6 +31,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,29 +46,47 @@ public class LSIncompleteCompletionProposal extends LookupElement { // Those variables should be defined in LSP4J and reused here whenever done there // See https://github.com/eclipse/lsp4j/issues/149 - /** The currently selected text or the empty string */ + /** + * The currently selected text or the empty string + */ private static final String TM_SELECTED_TEXT = "TM_SELECTED_TEXT"; //$NON-NLS-1$ - /** The contents of the current line */ + /** + * The contents of the current line + */ private static final String TM_CURRENT_LINE = "TM_CURRENT_LINE"; //$NON-NLS-1$ - /** The contents of the word under cursor or the empty string */ + /** + * The contents of the word under cursor or the empty string + */ private static final String TM_CURRENT_WORD = "TM_CURRENT_WORD"; //$NON-NLS-1$ - /** The zero-index based line number */ + /** + * The zero-index based line number + */ private static final String TM_LINE_INDEX = "TM_LINE_INDEX"; //$NON-NLS-1$ - /** The one-index based line number */ + /** + * The one-index based line number + */ private static final String TM_LINE_NUMBER = "TM_LINE_NUMBER"; //$NON-NLS-1$ - /** The filename of the current document */ + /** + * The filename of the current document + */ private static final String TM_FILENAME = "TM_FILENAME"; //$NON-NLS-1$ - /** The filename of the current document without its extensions */ + /** + * The filename of the current document without its extensions + */ private static final String TM_FILENAME_BASE = "TM_FILENAME_BASE"; //$NON-NLS-1$ - /** The directory of the current document */ + /** + * The directory of the current document + */ private static final String TM_DIRECTORY = "TM_DIRECTORY"; //$NON-NLS-1$ - /** The full file path of the current document */ + /** + * The full file path of the current document + */ private static final String TM_FILEPATH = "TM_FILEPATH"; //$NON-NLS-1$ protected final CompletionItem item; protected final int initialOffset; - protected int currentOffset; - protected int bestOffset; + protected int currentOffset; + protected int bestOffset; protected final Editor editor; private Integer rankCategory; private Integer rankScore; @@ -79,7 +102,25 @@ public LSIncompleteCompletionProposal(Editor editor, int offset, CompletionItem this.currentOffset = offset; this.bestOffset = getPrefixCompletionStart(editor.getDocument(), offset); //this.bestOffset = offset; - putUserData(CodeCompletionHandlerBase.DIRECT_INSERTION, true); + if(item.getInsertTextFormat() != InsertTextFormat.Snippet) { + putUserData(CodeCompletionHandlerBase.DIRECT_INSERTION, true); + } + } + + @Override + public void handleInsert(@NotNull InsertionContext context) { + if (item.getInsertTextFormat() == InsertTextFormat.Snippet){ + Template myTemplate = SnippetTemplateFactory.createTemplate(getInsertText(), context.getProject(), name -> getVariableValue(name)); + startTemplate(context, myTemplate); + } else { + apply(context.getDocument(), context.getCompletionChar(), 0, context.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET)); + } + } + + private static void startTemplate(InsertionContext context, @NotNull Template template) { + context.getDocument().deleteString(context.getStartOffset(), context.getTailOffset()); + context.setAddCompletionChar(false); + TemplateManager.getInstance(context.getProject()).startTemplate(context.getEditor(), template); } /** @@ -123,7 +164,7 @@ protected String getInsertText() { String insertText = this.item.getInsertText(); Either eitherTextEdit = this.item.getTextEdit(); if (eitherTextEdit != null) { - if(eitherTextEdit.isLeft()) { + if (eitherTextEdit.isLeft()) { insertText = eitherTextEdit.getLeft().getNewText(); } else { insertText = eitherTextEdit.getRight().getNewText(); @@ -173,7 +214,7 @@ public int getPrefixCompletionStart(Document document, int completionOffset) { @NotNull @Override public String getLookupString() { - String lookup = StringUtils.isNotBlank(item.getFilterText())?item.getFilterText():item.getLabel(); + String lookup = StringUtils.isNotBlank(item.getFilterText()) ? item.getFilterText() : item.getLabel(); if (lookup.charAt(0) == '@') { return lookup.substring(1); } @@ -182,7 +223,7 @@ public String getLookupString() { private boolean isDeprecated() { return (item.getTags() != null && item.getTags().contains(CompletionItemTag.Deprecated)) - || (item.getDeprecated() != null && item.getDeprecated().booleanValue()); + || (item.getDeprecated() != null && item.getDeprecated().booleanValue()); } @Override @@ -200,7 +241,7 @@ protected void apply(Document document, char trigger, int stateMask, int offset) Either eitherTextEdit = item.getTextEdit(); TextEdit textEdit = null; if (eitherTextEdit != null) { - if(eitherTextEdit.isLeft()) { + if (eitherTextEdit.isLeft()) { textEdit = eitherTextEdit.getLeft(); } else { // trick to partially support the new InsertReplaceEdit from LSP 3.16. Reuse previously code for TextEdit. @@ -247,66 +288,7 @@ protected void apply(Document document, char trigger, int stateMask, int offset) } textEdit.getRange().getEnd().setCharacter(textEdit.getRange().getEnd().getCharacter() + commonSize); } - insertText = textEdit.getNewText(); - int insertionOffset = LSPIJUtils.toOffset(textEdit.getRange().getStart(), document); - insertionOffset = computeNewOffset(item.getAdditionalTextEdits(), insertionOffset, document); - if (item.getInsertTextFormat() == InsertTextFormat.Snippet) { - int currentSnippetOffsetInInsertText = 0; - while ((currentSnippetOffsetInInsertText = insertText.indexOf('$', currentSnippetOffsetInInsertText)) != -1) { - StringBuilder keyBuilder = new StringBuilder(); - boolean isChoice = false; - List snippetProposals = new ArrayList<>(); - int offsetInSnippet = 1; - while (currentSnippetOffsetInInsertText + offsetInSnippet < insertText.length() && Character.isDigit(insertText.charAt(currentSnippetOffsetInInsertText + offsetInSnippet))) { - keyBuilder.append(insertText.charAt(currentSnippetOffsetInInsertText + offsetInSnippet)); - offsetInSnippet++; - } - if (keyBuilder.length() == 0 && insertText.substring(currentSnippetOffsetInInsertText).startsWith("${")) { //$NON-NLS-1$ - offsetInSnippet = 2; - while (currentSnippetOffsetInInsertText + offsetInSnippet < insertText.length() && Character.isDigit(insertText.charAt(currentSnippetOffsetInInsertText + offsetInSnippet))) { - keyBuilder.append(insertText.charAt(currentSnippetOffsetInInsertText + offsetInSnippet)); - offsetInSnippet++; - } - if (currentSnippetOffsetInInsertText + offsetInSnippet < insertText.length()) { - char currentChar = insertText.charAt(currentSnippetOffsetInInsertText + offsetInSnippet); - if (currentChar == ':' || currentChar == '|') { - isChoice |= currentChar == '|'; - offsetInSnippet++; - } - } - boolean close = false; - StringBuilder valueBuilder = new StringBuilder(); - while (currentSnippetOffsetInInsertText + offsetInSnippet < insertText.length() && !close) { - char currentChar = insertText.charAt(currentSnippetOffsetInInsertText + offsetInSnippet); - if (valueBuilder.length() > 0 && - ((isChoice && (currentChar == ',' || currentChar == '|') || currentChar == '}'))) { - String value = valueBuilder.toString(); - if (value.startsWith("$")) { //$NON-NLS-1$ - String varValue = getVariableValue(value.substring(1)); - if (varValue != null) { - value = varValue; - } - } - snippetProposals.add(value); - valueBuilder = new StringBuilder(); - } else if (currentChar != '}') { - valueBuilder.append(currentChar); - } - close = currentChar == '}'; - offsetInSnippet++; - } - } - String defaultProposal = snippetProposals.isEmpty() ? "" : snippetProposals.get(0); //$NON-NLS-1$ - if (keyBuilder.length() > 0) { - String key = keyBuilder.toString(); - insertText = insertText.substring(0, currentSnippetOffsetInInsertText) + defaultProposal + insertText.substring(currentSnippetOffsetInInsertText + offsetInSnippet); - currentSnippetOffsetInInsertText += defaultProposal.length(); - } else { - currentSnippetOffsetInInsertText++; - } - } - } - textEdit.setNewText(insertText); // insertText now has placeholder removed + List additionalEdits = item.getAdditionalTextEdits(); if (additionalEdits != null && !additionalEdits.isEmpty()) { List allEdits = new ArrayList<>(); @@ -330,30 +312,7 @@ protected void apply(Document document, char trigger, int stateMask, int offset) } } - private int computeNewOffset(List additionalTextEdits, int insertionOffset, Document doc) { - if (additionalTextEdits != null && !additionalTextEdits.isEmpty()) { - int adjustment = 0; - for (TextEdit edit : additionalTextEdits) { - try { - Range rng = edit.getRange(); - int start = LSPIJUtils.toOffset(rng.getStart(), doc); - if (start <= insertionOffset) { - int end = LSPIJUtils.toOffset(rng.getEnd(), doc); - int orgLen = end - start; - int newLeng = edit.getNewText().length(); - int editChange = newLeng - orgLen; - adjustment += editChange; - } - } catch (RuntimeException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - } - return insertionOffset + adjustment; - } - return insertionOffset; - } - - private String getVariableValue(String variableName) { + private @Nullable String getVariableValue(String variableName) { Document document = editor.getDocument(); switch (variableName) { case TM_FILENAME_BASE: @@ -416,11 +375,6 @@ public String getFilterString() { return item.getLabel(); } - @Override - public void handleInsert(@NotNull InsertionContext context) { - apply(context.getDocument(), context.getCompletionChar(), 0, context.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET)); - } - public boolean validate(Document document, int offset, DocumentEvent event) { return true; } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/SnippetTemplateFactory.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/SnippetTemplateFactory.java new file mode 100644 index 000000000..b68826d94 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/SnippetTemplateFactory.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion; + +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateManager; +import com.intellij.openapi.project.Project; +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetParser; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +/** + * Intellij {@link Template} factory to create the proper template structure (text segment, variables, etc) according the LSP snippet content. + * + * @author Angelo ZERR + * @see choices) { + String value = choices.isEmpty() ? "" : choices.get(0); + choice(value, choices); + } + + @Override + public void choice(String name, List choices) { + template.addVariable(new ConstantNode(name).withLookupStrings(choices), true); + } + + @Override + public void startPlaceholder(int index, String name, int level) { + variable(name); + } + + @Override + public void endPlaceholder(int level) { + + } + + @Override + public void variable(String name) { + String resolvedValue = variableResolver.apply(name); + if (resolvedValue != null) { + // ex : ${TM_SELECTED_TEXT} + // the TM_SELECTED_TEXT is resolved, we do a simple replacement + template.addVariable(new ConstantNode(resolvedValue), false); + } else { + if (existingVariables.contains(name)) { + // The variable (ex : ${name}) has already been declared, add a simple variable segment + // which will be updated by the previous add variable + template.addVariableSegment(name); + } else { + // The variable doesn't exists, add a variable which can be updated + // and which will replace other variables with the same name. + existingVariables.add(name); + template.addVariable(name, new ConstantNode(name), null, true, false); + } + } + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/Location.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/Location.java new file mode 100644 index 000000000..159259d6a --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/Location.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2013, 2016 EclipseSource. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + + +/** + * An immutable object that represents a location in the parsed text. + * + * This code is a copy/paste from + * https://github.com/ralfstx/minimal-json/blob/master/com.eclipsesource.json/src/main/java/com/eclipsesource/json/Location.java + * adapted for LSP Snippet. + */ +public class Location { + + /** + * The absolute character index, starting at 0. + */ + public final int offset; + + /** + * The line number, starting at 1. + */ + public final int line; + + /** + * The column number, starting at 1. + */ + public final int column; + + Location(int offset, int line, int column) { + this.offset = offset; + this.column = column; + this.line = line; + } + + @Override + public String toString() { + return line + ":" + column; + } + + @Override + public int hashCode() { + return offset; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Location other = (Location)obj; + return offset == other.offset && column == other.column && line == other.line; + } + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetHandler.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetHandler.java new file mode 100644 index 000000000..6f07b8f88 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetHandler.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import java.util.List; + +/** + * LSP snippet handler (aka SAXHandler). + * + * @author Angelo ZERR + * @see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax + */ +public interface LspSnippetHandler { + + /** + * On start snippet. + */ + void startSnippet(); + + /** + * On end snippet. + */ + void endSnippet(); + + /** + * On text block. + * + * @param text the text block. + */ + void text(String text); + + /** + * On tabstop (ex: $1}. + * + * @param index the tabstop index (ex:1) + */ + void tabstop(int index); + + /** + * On choice (ex : ${1|one,two,three|}). + * + * @param index the choice index (ex:1) + * @param choices the choices list (ex: [one,two,three]) + */ + void choice(int index, List choices); + + /** + * On choice (ex : ${two|one,two,three|}). + * + * @param name the choice name (ex:two) + * @param choices the choices list (ex: [one,two,three]) + */ + void choice(String name, List choices); + + /** + * On start placeholder (ex : {1:name}). + * + * @param index the placeholder index (ex:1) + * @param name the placeholder name (ex:name) + * @param level the placeholder level (1 for root and other for nested placeholder) + */ + void startPlaceholder(int index, String name, int level); + + /** + * On end place holder. + * + * @param level the placeholder level (1 for root and other for nested placeholder) + */ + void endPlaceholder(int level); + + /** + * On variable (ex : ${name} + * + * @param name the variable name (ex:name) + */ + void variable(String name); + +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetParser.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetParser.java new file mode 100644 index 000000000..41cef5bd9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetParser.java @@ -0,0 +1,379 @@ +/******************************************************************************* + * Copyright (c) 2013, 2016 EclipseSource. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * LSP snippet parser. + * + * This code is a copy/paste from + * https://github.com/ralfstx/minimal-json/blob/master/com.eclipsesource.json/src/main/java/com/eclipsesource/json/JsonParser.java + * adapted for LSP Snippet. + * + * @see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax + * + */ +public class LspSnippetParser { + private static final int MIN_BUFFER_SIZE = 10; + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private final LspSnippetHandler handler; + private Reader reader; + private char[] buffer; + private int bufferOffset; + private int index; + private int fill; + private int line; + private int lineOffset; + private int current; + private StringBuilder captureBuffer; + private int captureStart; + private int nestingLevel; + + /* + * | bufferOffset v [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t] < input + * [l|m|n|o|p|q|r|s|t|?|?] < buffer ^ ^ | index fill + */ + + public LspSnippetParser(LspSnippetHandler handler) { + this.handler = handler; + } + + /** + * Parses the given input string. The input must contain a valid JSON value, + * optionally padded with whitespace. + * + * @param string the input string, must be valid JSON + * @throws ParseException if the input is not valid JSON + */ + public void parse(String string) { + if (string == null) { + throw new NullPointerException("string is null"); + } + int bufferSize = Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length())); + try { + parse(new StringReader(string), bufferSize); + } catch (IOException exception) { + // StringReader does not throw IOException + throw new RuntimeException(exception); + } + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input + * must contain a valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into a default-sized input buffer. Hence, + * wrapping a reader in an additional BufferedReader likely won't + * improve reading performance. + *

+ * + * @param reader the reader to read the input from + * @throws IOException if an I/O error occurs in the reader + * @throws ParseException if the input is not valid JSON + */ + public void parse(Reader reader) throws IOException { + parse(reader, DEFAULT_BUFFER_SIZE); + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input + * must contain a valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into an input buffer of the given size. Hence, + * wrapping a reader in an additional BufferedReader likely won't + * improve reading performance. + *

+ * + * @param reader the reader to read the input from + * @param buffersize the size of the input buffer in chars + * @throws IOException if an I/O error occurs in the reader + * @throws ParseException if the input is not valid JSON + */ + public void parse(Reader reader, int buffersize) throws IOException { + if (reader == null) { + throw new NullPointerException("reader is null"); + } + if (buffersize <= 0) { + throw new IllegalArgumentException("buffersize is zero or negative"); + } + this.reader = reader; + buffer = new char[buffersize]; + bufferOffset = 0; + index = 0; + fill = 0; + line = 1; + lineOffset = 0; + current = 0; + captureStart = -1; + handler.startSnippet(); + read(); + readAny(); + handler.endSnippet(); + if (!isEndOfText()) { + throw error("Unexpected character"); + } + } + + // Snippet syntax: + + /* + any ::= tabstop | placeholder | choice | variable | text + tabstop ::= '$' int | '${' int '}' + placeholder ::= '${' int ':' any '}' + choice ::= '${' int '|' text (',' text)* '|}' + variable ::= '$' var | '${' var }' + | '${' var ':' any '}' + | '${' var '/' regex '/' (format | text)+ '/' options '}' + format ::= '$' int | '${' int '}' + | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' + | '${' int ':+' if '}' + | '${' int ':?' if ':' else '}' + | '${' int ':-' else '}' | '${' int ':' else '}' + regex ::= Regular Expression value (ctor-string) + options ::= Regular Expression option (ctor-options) + var ::= [_a-zA-Z] [_a-zA-Z0-9]* + int ::= [0-9]+ + text ::= .* + if ::= text + else ::= text + */ + + /** + * + * @throws IOException + */ + private void readAny() throws IOException { + if (isEndOfText()) { + return; + } + switch (current) { + case '$': + // read next character + read(); + if (isDigit()) { + // ex : $0, $10 + int index = readInt(); + handleTabstop(index); + } else if (readChar('{')) { + if (isDigit()) { + // - ${1:name} <-- placeholder + // - ${1|one,two,three|} <-- choice + // - ${1} <-- tabstop + int index = readInt(); + if (readChar(':')) { + // - ${1:name} <-- placeholder + String name = readString('}', '$'); + nestingLevel++; + handler.startPlaceholder(index, name, nestingLevel); + // placeholder ::= '${' int ':' any '}' + if (current == '}') { + // read next character + read(); + } else { + readAny(); + } + handler.endPlaceholder(nestingLevel); + nestingLevel--; + } else if (readChar('|')) { + // - ${1|one,two,three|} <-- choice + handleChoice(null, index); + } else { + // - ${1} <-- tabstop + handleTabstop(index); + readRequiredChar('}'); + } + } else { + // - ${name} <-- variable + String name = readString('}'); + handleVariable(name); + readRequiredChar('}'); + } + + } else { + // - $name <-- variable + String name = readString('$', ' '); + handleVariable(name); + } + break; + default: + handleText(); + break; + } + readAny(); + } + + private void handleChoice(String name, Integer index) throws IOException { + List choices = new ArrayList<>(); + String choice = readString(',', '|'); + while (!choice.isEmpty()) { + choices.add(choice); + if (readChar(',')) { + choice = readString(',', '|'); + } else { + break; + } + } + if (name == null) { + handler.choice(index, choices); + } else { + handler.choice(name, choices); + } + readRequiredChar('|'); + readRequiredChar('}'); + } + + private void handleTabstop(int index) { + handler.tabstop(index); + } + + private void handleVariable(String name) { + handler.variable(name); + } + + private String readString(int... stopOn) throws IOException { + startCapture(); + do { + read(); + for (int i = 0; i < stopOn.length; i++) { + if (current == stopOn[i]) { + return endCapture(); + } + } + + } while (!isEndOfText()); + return endCapture(); + } + + private void handleText() throws IOException { + String text = readString('$'); + handler.text(text); + } + + private int readInt() throws IOException { + startCapture(); + int firstDigit = current; + if (!readDigit()) { + throw expected("digit"); + } + if (firstDigit != '0') { + while (readDigit()) { + } + } + return Integer.parseInt(endCapture()); + } + + private void readRequiredChar(char ch) throws IOException { + if (!readChar(ch)) { + throw expected("'" + ch + "'"); + } + } + + private boolean readChar(char ch) throws IOException { + if (current != ch) { + return false; + } + read(); + return true; + } + + private boolean readDigit() throws IOException { + if (!isDigit()) { + return false; + } + read(); + return true; + } + + private void read() throws IOException { + if (index == fill) { + if (captureStart != -1) { + captureBuffer.append(buffer, captureStart, fill - captureStart); + captureStart = 0; + } + bufferOffset += fill; + fill = reader.read(buffer, 0, buffer.length); + index = 0; + if (fill == -1) { + current = -1; + index++; + return; + } + } + if (current == '\n') { + line++; + lineOffset = bufferOffset + index; + } + current = buffer[index++]; + } + + private void startCapture() { + if (captureBuffer == null) { + captureBuffer = new StringBuilder(); + } + captureStart = index - 1; + } + + private String endCapture() { + int start = captureStart; + int end = index - 1; + captureStart = -1; + if (captureBuffer.length() > 0) { + captureBuffer.append(buffer, start, end - start); + String captured = captureBuffer.toString(); + captureBuffer.setLength(0); + return captured; + } + return new String(buffer, start, end - start); + } + + Location getLocation() { + int offset = bufferOffset + index - 1; + int column = offset - lineOffset + 1; + return new Location(offset, line, column); + } + + private ParseException expected(String expected) { + if (isEndOfText()) { + return error("Unexpected end of input"); + } + return error("Expected " + expected); + } + + private ParseException error(String message) { + return new ParseException(message, getLocation()); + } + + private boolean isDigit() { + return current >= '0' && current <= '9'; + } + + private boolean isEndOfText() { + return current == -1; + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ParseException.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ParseException.java new file mode 100644 index 000000000..7b9bd85d9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ParseException.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2013, 2016 EclipseSource. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +/** + * An unchecked exception to indicate that an input does not qualify as valid JSON. + * + * This code is a copy/paste from + * https://github.com/ralfstx/minimal-json/blob/master/com.eclipsesource.json/src/main/java/com/eclipsesource/json/ParserException.java + * adapted for LSP Snippet. + * + */ +@SuppressWarnings("serial") // use default serial UID +public class ParseException extends RuntimeException { + + private final Location location; + + ParseException(String message, Location location) { + super(message + " at " + location); + this.location = location; + } + + /** + * Returns the location at which the error occurred. + * + * @return the error location + */ + public Location getLocation() { + return location; + } + +} \ No newline at end of file diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/AdvancedTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/AdvancedTest.java new file mode 100644 index 000000000..cae42e44b --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/AdvancedTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.LspSnippetNode; +import org.junit.Test; + +import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetAssert.*; + +public class AdvancedTest { + + @Test + public void textAndlaceholdersAndTabStop() { + LspSnippetNode[] actual = parse("{#for ${1:item} in ${2:items}}\\r\\n\\t{${1:item}.${3:name}}$0\\r\\n{/for}"); + assertEquals(actual, text("{#for "), // + placeholder(1, "item", 1), // ${1:item} + text(" in "), // + placeholder(2, "items", 1), // ${2:items} + text("}\\r\\n\\t{"), // + placeholder(1, "item", 1), // ${1:item} + text("."), // + placeholder(3, "name", 1), // ${3:name} + text("}"), // + tabstop(0), // + text("\\r\\n{/for}")); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ChoiceTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ChoiceTest.java new file mode 100644 index 000000000..afe6908d0 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/ChoiceTest.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.LspSnippetNode; +import org.junit.Test; + +import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetAssert.*; + +public class ChoiceTest { + + @Test + public void simpleText() { + LspSnippetNode[] actual = parse("${1|auto,idea,vscode,eclipse,netbeans|}"); + assertEquals(actual, choice(1, "auto", "idea", "vscode", "eclipse", "netbeans")); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetAssert.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetAssert.java new file mode 100644 index 000000000..6d20a3870 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/LspSnippetAssert.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertArrayEquals; + +public class LspSnippetAssert { + + private LspSnippetAssert() { + + } + + public static LspSnippetNode[] parse(String snippet) { + LspSnippetHandlerImpl handler = new LspSnippetHandlerImpl(); + LspSnippetParser parser = new LspSnippetParser(handler); + parser.parse(snippet); + return handler.getNodes(); + } + + public static void assertEquals(LspSnippetNode[] actual, LspSnippetNode... expected) { + assertArrayEquals(expected, actual); + } + + public static TabstopNode tabstop(int index) { + return new TabstopNode(index); + } + + public static VariableNode variable(String name) { + return new VariableNode(name); + } + + public static TextNode text(String text) { + return new TextNode(text); + } + + public static PlaceholderNode placeholder(int index, String name, int level) { + return new PlaceholderNode(index, name, level); + } + + public static ChoiceNode choice(int index, String... choices) { + return choice(index, null, choices); + } + + public static ChoiceNode choice(String name, String... choices) { + return choice(null, name, choices); + } + + private static ChoiceNode choice(Integer index, String name, String... choices) { + List list = Arrays.stream(choices).collect(Collectors.toList()); + return new ChoiceNode(index, name, list); + } +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/PlaceholderTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/PlaceholderTest.java new file mode 100644 index 000000000..fbbec32fc --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/PlaceholderTest.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.LspSnippetNode; +import org.junit.Test; + +import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetAssert.*; + +public class PlaceholderTest { + + @Test + public void simpleText() { + LspSnippetNode[] actual = parse("${1:name}"); + assertEquals(actual, placeholder(1, "name", 1)); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TabstopTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TabstopTest.java new file mode 100644 index 000000000..a9941ba8e --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TabstopTest.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.LspSnippetNode; +import org.junit.Test; + +import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetAssert.*; + +public class TabstopTest { + + @Test + public void onlyTabstop() { + LspSnippetNode[] actual = parse("$123"); + assertEquals(actual, tabstop(123)); + } + + @Test + public void tabstopWithText() { + LspSnippetNode[] actual = parse("abcd $123 efgh"); + assertEquals(actual, + text("abcd "), // + tabstop(123), // + text(" efgh")); + } + + @Test + public void tabstopInBracket() { + LspSnippetNode[] actual = parse("${123}"); + assertEquals(actual, tabstop(123)); + } + + @Test + public void tabstopInBracketWithText() { + LspSnippetNode[] actual = parse("abcd ${123} efgh"); + assertEquals(actual, text("abcd "), // + tabstop(123), // + text(" efgh")); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TextTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TextTest.java new file mode 100644 index 000000000..0664ed68d --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/TextTest.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.LspSnippetNode; +import org.junit.Test; + +import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetAssert.*; + +public class TextTest { + + @Test + public void simpleText() { + LspSnippetNode[] actual = parse("abcd efgh"); + assertEquals(actual, text("abcd efgh")); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/VariableTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/VariableTest.java new file mode 100644 index 000000000..12a655ecd --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/VariableTest.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler.LspSnippetNode; +import org.junit.Test; + +import static com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetAssert.*; + +public class VariableTest { + + @Test + public void onlyVariable() { + LspSnippetNode[] actual = parse("$name"); + assertEquals(actual, variable("name")); + } + + @Test + public void variableWithText() { + LspSnippetNode[] actual = parse("abcd $name efgh"); + assertEquals(actual, + text("abcd "), // + variable("name"), // + text(" efgh")); + } + + @Test + public void variableInBracket() { + LspSnippetNode[] actual = parse("${name}"); + assertEquals(actual, variable("name")); + } + + @Test + public void variableInBracketWithText() { + LspSnippetNode[] actual = parse("abcd ${name} efgh"); + assertEquals(actual, text("abcd "), // + variable("name"), // + text(" efgh")); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/ChoiceNode.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/ChoiceNode.java new file mode 100644 index 000000000..9dc9efa3f --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/ChoiceNode.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +import java.util.List; +import java.util.stream.Collectors; + +public class ChoiceNode implements LspSnippetNode { + + private final Integer index; + + private final String name; + + private final List choices; + + public ChoiceNode(Integer index, String name, List choices) { + this.index = index; + this.name = name; + this.choices = choices; + } + + public Integer getIndex() { + return index; + } + + public String getName() { + return name; + } + + public List getChoices() { + return choices; + } + + @Override + public String toString() { + return "choice(" + index + "," + name + ",[" + choices.stream().collect(Collectors.joining(",")) + "])"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((choices == null) ? 0 : choices.hashCode()); + result = prime * result + ((index == null) ? 0 : index.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ChoiceNode other = (ChoiceNode) obj; + if (choices == null) { + if (other.choices != null) + return false; + } else if (!choices.equals(other.choices)) + return false; + if (index == null) { + if (other.index != null) + return false; + } else if (!index.equals(other.index)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetHandlerImpl.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetHandlerImpl.java new file mode 100644 index 000000000..3308ba0de --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetHandlerImpl.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +import com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.LspSnippetHandler; + +import java.util.ArrayList; +import java.util.List; + +public class LspSnippetHandlerImpl implements LspSnippetHandler { + + private final List nodes; + + private final List placeholderStack; + + public LspSnippetHandlerImpl() { + this.nodes = new ArrayList<>(); + this.placeholderStack = new ArrayList<>(); + } + + @Override + public void startSnippet() { + + } + + @Override + public void endSnippet() { + + } + + @Override + public void text(String text) { + nodes.add(new TextNode(text)); + } + + @Override + public void tabstop(int index) { + nodes.add(new TabstopNode(index)); + } + + @Override + public void variable(String name) { + nodes.add(new VariableNode(name)); + } + + @Override + public void startPlaceholder(int index, String name, int level) { + PlaceholderNode placeholder = new PlaceholderNode(index, name, level); + placeholderStack.add(placeholder); + nodes.add(placeholder); + } + + @Override + public void endPlaceholder(int level) { + placeholderStack.remove(level - 1); + } + + @Override + public void choice(int index, List choices) { + nodes.add(new ChoiceNode(index, null, choices)); + } + + @Override + public void choice(String name, List choices) { + nodes.add(new ChoiceNode(null, name, choices)); + } + + public LspSnippetNode[] getNodes() { + return nodes.toArray(new LspSnippetNode[nodes.size()]); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetNode.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetNode.java new file mode 100644 index 000000000..af1d29861 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/LspSnippetNode.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +public interface LspSnippetNode { + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/PlaceholderNode.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/PlaceholderNode.java new file mode 100644 index 000000000..5cc597750 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/PlaceholderNode.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +public class PlaceholderNode implements LspSnippetNode { + + private final int index; + private final String name; + private final int level; + + public PlaceholderNode(int index, String name, int level) { + this.index = index; + this.name = name; + this.level = level; + } + + public int getIndex() { + return index; + } + + public String getName() { + return name; + } + + public int getLevel() { + return level; + } + + @Override + public String toString() { + return "placeholder(" + index + "," + name + "," + level + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + index; + result = prime * result + level; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PlaceholderNode other = (PlaceholderNode) obj; + if (index != other.index) + return false; + if (level != other.level) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TabstopNode.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TabstopNode.java new file mode 100644 index 000000000..82e7fe217 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TabstopNode.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +public class TabstopNode implements LspSnippetNode { + + private final int index; + + public TabstopNode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + @Override + public String toString() { + return "tabstop(" + index + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + index; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TabstopNode other = (TabstopNode) obj; + if (index != other.index) + return false; + return true; + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TextNode.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TextNode.java new file mode 100644 index 000000000..6d7796493 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/TextNode.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +public class TextNode implements LspSnippetNode { + + private final String text; + + public TextNode(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return "text(" + getText() + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((text == null) ? 0 : text.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TextNode other = (TextNode) obj; + if (text == null) { + if (other.text != null) + return false; + } else if (!text.equals(other.text)) + return false; + return true; + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/VariableNode.java b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/VariableNode.java new file mode 100644 index 000000000..451c77d53 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/snippet/handler/VariableNode.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.lsp4ij.operations.completion.snippet.handler; + +public class VariableNode implements LspSnippetNode { + + private final String name; + + public VariableNode(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "variable(" + getName() + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + VariableNode other = (VariableNode) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + +}