From d3817dd202bace2bbe66f3f5c8d888dcf226134d Mon Sep 17 00:00:00 2001 From: azerr Date: Tue, 28 Apr 2020 17:11:17 +0200 Subject: [PATCH] Manage snippet registry to write snippet in JSON Fixes #640 Signed-off-by: azerr --- .../commons/snippets/ISnippetContext.java | 33 + .../snippets/ISnippetRegistryLoader.java | 38 ++ .../lemminx/commons/snippets/Snippet.java | 88 +++ .../commons/snippets/SnippetDeserializer.java | 108 +++ .../commons/snippets/SnippetRegistry.java | 375 +++++++++++ .../org/eclipse/lemminx/dom/DOMElement.java | 4 + .../lemminx/services/CompletionRequest.java | 2 +- .../lemminx/services/XMLCompletions.java | 628 ++++++++---------- .../snippets/CDATASnippetContext.java | 24 + .../snippets/CommentSnippetContext.java | 38 ++ .../snippets/DTDNodeSnippetContext.java | 31 + .../snippets/DocTypeSnippetContext.java | 59 ++ .../services/snippets/IXMLSnippetContext.java | 8 + .../snippets/NewFileSnippetContext.java | 54 ++ .../snippets/PrologSnippetContext.java | 61 ++ .../snippets/SnippetContextUtils.java | 96 +++ .../snippets/XMLSnippetRegistryLoader.java | 24 + .../eclipse/lemminx/utils/StringUtils.java | 37 +- ...nx.commons.snippets.ISnippetRegistryLoader | 1 + .../services/snippets/cdata-snippets.json | 11 + .../services/snippets/comment-snippets.json | 11 + .../services/snippets/doctype-snippets.json | 53 ++ .../services/snippets/dtdnode-snippets.json | 38 ++ .../services/snippets/newfile-snippets.json | 63 ++ .../services/snippets/prolog-snippets.json | 20 + .../java/org/eclipse/lemminx/XMLAssert.java | 64 +- .../DTDCompletionExtensionsTest.java | 81 ++- .../XMLSchemaCompletionExtensionsTest.java | 6 +- .../services/XMLCompletionSnippetsTest.java | 289 ++++++++ .../lemminx/services/XMLCompletionTest.java | 21 +- .../lemminx/utils/StringUtilsTest.java | 14 - 31 files changed, 1932 insertions(+), 448 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetRegistryLoader.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/Snippet.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetDeserializer.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetRegistry.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CDATASnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CommentSnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DTDNodeSnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DocTypeSnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/IXMLSnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/NewFileSnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/PrologSnippetContext.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/SnippetContextUtils.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/XMLSnippetRegistryLoader.java create mode 100644 org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.commons.snippets.ISnippetRegistryLoader create mode 100644 org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/cdata-snippets.json create mode 100644 org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/comment-snippets.json create mode 100644 org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/doctype-snippets.json create mode 100644 org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/dtdnode-snippets.json create mode 100644 org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/newfile-snippets.json create mode 100644 org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/prolog-snippets.json create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLCompletionSnippetsTest.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetContext.java new file mode 100644 index 0000000000..c8fe1c0cf4 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetContext.java @@ -0,0 +1,33 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.commons.snippets; + +import java.util.Map; + +/** + * Snippet context used to filter the snippet. + * + * @author Angelo ZERR + * + * @param the value type waited by the snippet context. + */ +public interface ISnippetContext { + + /** + * Return true if the given value match the snippet context and false otherwise. + * + * @param value the value to check. + * @return true if the given value match the snippet context and false + * otherwise. + */ + boolean isMatch(T value, Map model); +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetRegistryLoader.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetRegistryLoader.java new file mode 100644 index 0000000000..e832b1eb5d --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISnippetRegistryLoader.java @@ -0,0 +1,38 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.commons.snippets; + +/** + * Loader used to load snippets in a given registry for a language id. + * + * @author Angelo ZERR + * + */ +public interface ISnippetRegistryLoader { + + /** + * Register snippets in the given snippet registry. + * + * @param registry + * @throws Exception + */ + void load(SnippetRegistry registry) throws Exception; + + /** + * Returns the language id and null otherwise. + * + * @return the language id and null otherwise. + */ + default String getLanguageId() { + return null; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/Snippet.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/Snippet.java new file mode 100644 index 0000000000..0e240798cf --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/Snippet.java @@ -0,0 +1,88 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.commons.snippets; + +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; + +/** + * Snippet description (like vscode snippet). + * + * @author Angelo ZERR + * + */ +public class Snippet { + + private List prefixes; + + private List body; + + private String description; + + private String scope; + + private ISnippetContext context; + + public List getPrefixes() { + return prefixes; + } + + public void setPrefixes(List prefixes) { + this.prefixes = prefixes; + } + + public List getBody() { + return body; + } + + public void setBody(List body) { + this.body = body; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public ISnippetContext getContext() { + return context; + } + + public void setContext(ISnippetContext context) { + this.context = context; + } + + public boolean hasContext() { + return getContext() != null; + } + + public boolean match(BiPredicate, Map> contextFilter, + Map model) { + if (!hasContext()) { + return true; + } + return contextFilter.test(getContext(), model); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetDeserializer.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetDeserializer.java new file mode 100644 index 0000000000..a2b11010f6 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetDeserializer.java @@ -0,0 +1,108 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.commons.snippets; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; + +/** + * GSON deserializer to build Snippet from vscode JSON snippet. + * + * @author Angelo ZERR + * + */ +class SnippetDeserializer implements JsonDeserializer { + + private static final String PREFIX_ELT = "prefix"; + private static final String DESCRIPTION_ELT = "description"; + private static final String SCOPE_ELT = "scope"; + private static final String BODY_ELT = "body"; + private static final String CONTEXT_ELT = "context"; + + private final TypeAdapter> contextDeserializer; + + public SnippetDeserializer(TypeAdapter> contextDeserializer) { + this.contextDeserializer = contextDeserializer; + } + + @Override + public Snippet deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + Snippet snippet = new Snippet(); + JsonObject snippetObj = json.getAsJsonObject(); + + // prefix + List prefixes = new ArrayList<>(); + JsonElement prefixElt = snippetObj.get(PREFIX_ELT); + if (prefixElt != null) { + if (prefixElt.isJsonArray()) { + JsonArray prefixArray = (JsonArray) prefixElt; + prefixArray.forEach(elt -> { + prefixes.add(elt.getAsString()); + }); + } else if (prefixElt.isJsonPrimitive()) { + prefixes.add(prefixElt.getAsString()); + } + } + snippet.setPrefixes(prefixes); + + // body + List body = new ArrayList<>(); + JsonElement bodyElt = snippetObj.get(BODY_ELT); + if (bodyElt != null) { + if (bodyElt.isJsonArray()) { + JsonArray bodyArray = (JsonArray) bodyElt; + bodyArray.forEach(elt -> { + body.add(elt.getAsString()); + }); + } else if (bodyElt.isJsonPrimitive()) { + body.add(bodyElt.getAsString()); + } + } + snippet.setBody(body); + + // description + JsonElement descriptionElt = snippetObj.get(DESCRIPTION_ELT); + if (descriptionElt != null) { + String description = descriptionElt.getAsString(); + snippet.setDescription(description); + } + + // scope + JsonElement scopeElt = snippetObj.get(SCOPE_ELT); + if (scopeElt != null) { + String scope = scopeElt.getAsString(); + snippet.setScope(scope); + } + + // context + if (contextDeserializer != null) { + JsonElement contextElt = snippetObj.get(CONTEXT_ELT); + if (contextElt != null) { + ISnippetContext snippetContext = contextDeserializer.fromJsonTree(contextElt); + snippet.setContext(snippetContext); + } + } + + return snippet; + } + +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetRegistry.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetRegistry.java new file mode 100644 index 0000000000..1f2da78dab --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetRegistry.java @@ -0,0 +1,375 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.commons.snippets; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.BiPredicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkupKind; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; + +/** + * A registry for snippets which uses the same format than vscode snippet. + * + * @author Angelo ZERR + * + */ +public class SnippetRegistry { + + private static final Logger LOGGER = Logger.getLogger(SnippetRegistry.class.getName()); + + private final List snippets; + + public SnippetRegistry() { + this(null); + } + + /** + * Snippet registry for a given language id. + * + * @param languageId the language id and null otherwise. + */ + public SnippetRegistry(String languageId) { + snippets = new ArrayList<>(); + // Load snippets from SPI + ServiceLoader loaders = ServiceLoader.load(ISnippetRegistryLoader.class); + loaders.forEach(loader -> { + if (Objects.equals(languageId, loader.getLanguageId())) { + try { + loader.load(this); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while consumming snippet loader " + loader.getClass().getName(), e); + } + } + }); + } + + /** + * Register the given snippet. + * + * @param snippet the snippet to register. + */ + public void registerSnippet(Snippet snippet) { + snippets.add(snippet); + } + + /** + * Register the snippets from the given JSON input stream. + * + * @param in the JSON input stream which declares snippets with vscode snippet + * format. + * @throws IOException + */ + public void registerSnippets(InputStream in) throws IOException { + registerSnippets(in, null, null); + } + + /** + * Register the snippets from the given JSON stream with a context. + * + * @param in the JSON input stream which declares snippets with + * vscode snippet format. + * @param contextDeserializer the GSON context deserializer used to create Java + * context. + * @throws IOException + */ + public void registerSnippets(InputStream in, TypeAdapter> contextDeserializer) + throws IOException { + registerSnippets(in, null, contextDeserializer); + } + + /** + * Register the snippets from the given JSON stream with a context. + * + * @param in the JSON input stream which declares snippets with + * vscode snippet format. + * @param defaultContext the default context. + * @throws IOException + */ + public void registerSnippets(InputStream in, ISnippetContext defaultContext) throws IOException { + registerSnippets(in, defaultContext, null); + } + + /** + * Register the snippets from the given JSON stream with a context. + * + * @param in the JSON input stream which declares snippets with + * vscode snippet format. + * @param defaultContext the default context. + * @param contextDeserializer the GSON context deserializer used to create Java + * context. + * @throws IOException + */ + public void registerSnippets(InputStream in, ISnippetContext defaultContext, + TypeAdapter> contextDeserializer) throws IOException { + registerSnippets(new InputStreamReader(in, StandardCharsets.UTF_8.name()), defaultContext, contextDeserializer); + } + + /** + * Register the snippets from the given JSON reader. + * + * @param in the JSON reader which declares snippets with vscode snippet format. + * @throws IOException + */ + public void registerSnippets(Reader in) throws IOException { + registerSnippets(in, null, null); + } + + /** + * Register the snippets from the given JSON reader with a context. + * + * @param in the JSON reader which declares snippets with + * vscode snippet format. + * @param contextDeserializer the GSON context deserializer used to create Java + * context. + * @throws IOException + */ + public void registerSnippets(Reader in, TypeAdapter> contextDeserializer) + throws IOException { + registerSnippets(in, null, contextDeserializer); + } + + /** + * Register the snippets from the given JSON stream with a context. + * + * @param in the JSON reader which declares snippets with vscode + * snippet format. + * @param defaultContext the default context. + * @throws IOException + */ + public void registerSnippets(Reader in, ISnippetContext defaultContext) throws IOException { + registerSnippets(in, defaultContext, null); + } + + /** + * Register the snippets from the given JSON stream with a context. + * + * @param in the JSON reader which declares snippets with + * vscode snippet format. + * @param defaultContext the default context. + * @param contextDeserializer the GSON context deserializer used to create Java + * context. + * @throws IOException + */ + public void registerSnippets(Reader in, ISnippetContext defaultContext, + TypeAdapter> contextDeserializer) throws IOException { + JsonReader reader = new JsonReader(in); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + Snippet snippet = createSnippet(reader, contextDeserializer); + if (snippet.getDescription() == null) { + snippet.setDescription(name); + } + if (snippet.getContext() == null) { + snippet.setContext(defaultContext); + } + registerSnippet(snippet); + } + reader.endObject(); + } + + private static Snippet createSnippet(JsonReader reader, + TypeAdapter> contextDeserializer) throws JsonIOException, JsonSyntaxException { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(Snippet.class, new SnippetDeserializer(contextDeserializer)); + return builder.create().fromJson(reader, Snippet.class); + } + + /** + * Returns all snippets. + * + * @return all snippets. + */ + public List getSnippets() { + return snippets; + } + + /** + * Returns the snippet completion items according to the context filter. + * + * @param replaceRange the replace range. + * @param lineDelimiter the line delimiter. + * @param canSupportMarkdown true if markdown is supported to generate + * documentation and false otherwise. + * @param contextFilter the context filter. + * @return the snippet completion items according to the context filter. + */ + public List getCompletionItems(Range replaceRange, String lineDelimiter, boolean canSupportMarkdown, + boolean snippetsSupported, BiPredicate, Map> contextFilter) { + if (replaceRange == null) { + return Collections.emptyList(); + } + Map model = new HashMap<>(); + return getSnippets().stream().filter(snippet -> { + return snippet.match(contextFilter, model); + }).map(snippet -> { + String prefix = snippet.getPrefixes().get(0); + CompletionItem item = new CompletionItem(); + item.setLabel(snippet.getDescription()); + String insertText = getInsertText(snippet, model, !snippetsSupported, lineDelimiter); + item.setKind(CompletionItemKind.Snippet); + item.setDocumentation( + Either.forRight(createDocumentation(snippet, model, canSupportMarkdown, lineDelimiter))); + item.setFilterText(prefix); + item.setTextEdit(new TextEdit(replaceRange, insertText)); + item.setInsertTextFormat(InsertTextFormat.Snippet); + return item; + + }).filter(item -> item != null).collect(Collectors.toList()); + } + + private static MarkupContent createDocumentation(Snippet snippet, Map model, + boolean canSupportMarkdown, String lineDelimiter) { + StringBuilder doc = new StringBuilder(); + if (canSupportMarkdown) { + doc.append(System.lineSeparator()); + doc.append("```"); + String scope = snippet.getScope(); + if (scope != null) { + doc.append(scope); + } + doc.append(System.lineSeparator()); + } + String insertText = getInsertText(snippet, model, true, lineDelimiter); + doc.append(insertText); + if (canSupportMarkdown) { + doc.append(System.lineSeparator()); + doc.append("```"); + doc.append(System.lineSeparator()); + } + return new MarkupContent(canSupportMarkdown ? MarkupKind.MARKDOWN : MarkupKind.PLAINTEXT, doc.toString()); + } + + private static String getInsertText(Snippet snippet, Map model, boolean replace, + String lineDelimiter) { + StringBuilder text = new StringBuilder(); + int i = 0; + List body = snippet.getBody(); + if (body != null) { + for (String bodyLine : body) { + if (i > 0) { + text.append(lineDelimiter); + } + bodyLine = merge(bodyLine, model, replace); + text.append(bodyLine); + i++; + } + } + return text.toString(); + } + + private static String merge(String line, Map model, boolean replace) { + return replace(line, 0, model, replace, null); + } + + private static String replace(String line, int offset, Map model, boolean replace, + StringBuilder newLine) { + int dollarIndex = line.indexOf("$", offset); + if (dollarIndex == -1 || dollarIndex == line.length() - 1) { + if (newLine == null) { + return line; + } + newLine.append(line.substring(offset, line.length())); + return newLine.toString(); + } + if (newLine == null) { + newLine = new StringBuilder(); + } + char next = line.charAt(dollarIndex + 1); + if (Character.isDigit(next)) { + newLine.append(line.substring(offset, dollarIndex)); + int lastDigitOffset = dollarIndex + 1; + while (Character.isDigit(line.charAt(lastDigitOffset))) { + lastDigitOffset++; + } + return replace(line, lastDigitOffset, model, replace, newLine); + } else if (next == '{') { + int startExpr = dollarIndex; + int endExpr = line.indexOf("}", startExpr); + if (endExpr == -1) { + // Should never occur + return line; + } + newLine.append(line.substring(offset, startExpr)); + // Parameter + int startParam = startExpr + 2; + int endParam = endExpr; + boolean startsWithNumber = true; + boolean onlyNumber = true; + for (int i = startParam; i < endParam; i++) { + char ch = line.charAt(i); + if (Character.isDigit(ch)) { + startsWithNumber = true; + } else { + onlyNumber = false; + if (ch == ':') { + + if (startsWithNumber) { + startParam = i + 1; + } + break; + } else if (ch == '|') { + if (startsWithNumber) { + startParam = i + 1; + int index = line.indexOf(',', startExpr); + if (index != -1) { + endParam = index; + } + } + break; + } else { + break; + } + } + } + String paramName = line.substring(startParam, endParam); + if (model.containsKey(paramName)) { + paramName = model.get(paramName); + } else if (!replace) { + paramName = line.substring(startExpr, endExpr + 1); + } + if (!(replace && onlyNumber)) { + newLine.append(paramName); + } + return replace(line, endExpr + 1, model, replace, newLine); + } + return line; + } + +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java index bf4e44bc68..e1f491fad1 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java @@ -296,6 +296,10 @@ public boolean isInEndTag(int offset) { return false; } + public boolean isInInsideStartEndTag(int offset) { + return offset > startTagCloseOffset && offset <= endTagOpenOffset; + } + /** * Returns the start tag open offset and {@link DOMNode#NULL_VALUE} if it * doesn't exist. diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/CompletionRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/CompletionRequest.java index e66df101cf..0d3b4c10cb 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/CompletionRequest.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/CompletionRequest.java @@ -50,7 +50,7 @@ public CompletionRequest(DOMDocument xmlDocument, Position position, SharedSetti this.formattingSettings = settings.getFormattingSettings(); this.completionSettings = settings.getCompletionSettings(); } - + @Override protected DOMNode findNodeAt(DOMDocument xmlDocument, int offset) { return xmlDocument.findNodeBefore(offset); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCompletions.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCompletions.java index 324f5d716b..3b3c4f2135 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCompletions.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCompletions.java @@ -25,6 +25,7 @@ import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.commons.TextDocument; +import org.eclipse.lemminx.commons.snippets.SnippetRegistry; import org.eclipse.lemminx.customservice.AutoCloseTagResponse; import org.eclipse.lemminx.dom.DOMAttr; import org.eclipse.lemminx.dom.DOMDocument; @@ -39,6 +40,7 @@ import org.eclipse.lemminx.services.extensions.ICompletionRequest; import org.eclipse.lemminx.services.extensions.ICompletionResponse; import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.snippets.IXMLSnippetContext; import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lemminx.utils.StringUtils; import org.eclipse.lemminx.utils.XMLPositionUtility; @@ -46,6 +48,7 @@ import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.MarkupKind; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; @@ -61,6 +64,7 @@ public class XMLCompletions { private static final Pattern regionCompletionRegExpr = Pattern.compile("^(\\s*)(<(!(-(-\\s*(#\\w*)?)?)?)?)?$"); private final XMLExtensionsRegistry extensionsRegistry; + private SnippetRegistry snippetRegistry; public XMLCompletions(XMLExtensionsRegistry extensionsRegistry) { this.extensionsRegistry = extensionsRegistry; @@ -77,226 +81,316 @@ public CompletionList doComplete(DOMDocument xmlDocument, Position position, Sha return completionResponse; } + String text = xmlDocument.getText(); int offset = completionRequest.getOffset(); DOMNode node = completionRequest.getNode(); + Scanner scanner = null; + try { + if (text.isEmpty()) { + // When XML document is empty, try to collect root element (from file + // association) + collectInsideContent(completionRequest, completionResponse); + return completionResponse; + } - String text = xmlDocument.getText(); - if (text.isEmpty()) { - // When XML document is empty, try to collect root element (from file - // association) - collectInsideContent(completionRequest, completionResponse); - return completionResponse; - } - - Scanner scanner = XMLScanner.createScanner(text, node.getStart(), isInsideDTDContent(node, xmlDocument)); - String currentTag = ""; - TokenType token = scanner.scan(); - while (token != TokenType.EOS && scanner.getTokenOffset() <= offset) { - cancelChecker.checkCanceled(); - switch (token) { - case StartTagOpen: - if (scanner.getTokenEnd() == offset) { - int endPos = scanNextForEndPos(offset, scanner, TokenType.StartTag); - collectTagSuggestions(offset, endPos, completionRequest, completionResponse); - collectCDATACompletion(completionRequest, completionResponse); - collectCommentCompletion(completionRequest, completionResponse); - return completionResponse; - } else if (text.charAt(scanner.getTokenOffset() + 1) == '!') { - // Case where completion was triggered after = 0) { - char ch = text.charAt(start); - if (ch == '/') { - collectCloseTagSuggestions(start, false, scanner.getTokenEnd(), completionRequest, + break; + case AttributeValue: + if (scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd()) { + collectAttributeValueSuggestions(scanner.getTokenOffset(), scanner.getTokenEnd(), + completionRequest, completionResponse); + return completionResponse; + } + break; + case Whitespace: + if (offset <= scanner.getTokenEnd()) { + switch (scanner.getScannerState()) { + case AfterOpeningStartTag: + int startPos = scanner.getTokenOffset(); + int endTagPos = scanNextForEndPos(offset, scanner, TokenType.StartTag); + collectTagSuggestions(startPos, endTagPos, completionRequest, completionResponse); + return completionResponse; + case WithinTag: + case AfterAttributeName: + collectAttributeNameSuggestions(scanner.getTokenEnd(), completionRequest, + completionResponse); + return completionResponse; + case BeforeAttributeValue: + collectAttributeValueSuggestions(scanner.getTokenEnd(), offset, completionRequest, completionResponse); return completionResponse; - } else if (!isWhitespace(ch)) { - break; + case AfterOpeningEndTag: + collectCloseTagSuggestions(scanner.getTokenOffset() - 1, false, offset, completionRequest, + completionResponse); + return completionResponse; + case WithinContent: + collectInsideContent(completionRequest, completionResponse); + return completionResponse; + default: } - start--; } - } - break; - case StartTagClose: - if (offset <= scanner.getTokenEnd()) { - if (currentTag != null && currentTag.length() > 0) { - collectInsideContent(completionRequest, completionResponse); + break; + case EndTagOpen: + if (offset <= scanner.getTokenEnd()) { + int afterOpenBracket = scanner.getTokenOffset() + 1; + int endOffset = scanNextForEndPos(offset, scanner, TokenType.EndTag); + collectCloseTagSuggestions(afterOpenBracket, false, endOffset, completionRequest, + completionResponse); return completionResponse; } - } - break; - case StartTagSelfClose: - if (offset <= scanner.getTokenEnd()) { - if (currentTag != null && currentTag.length() > 0 - && xmlDocument.getText().charAt(offset - 1) == '>') { // if the actual character typed was - // '>' - collectInsideContent(completionRequest, completionResponse); - return completionResponse; + break; + case EndTag: + if (offset <= scanner.getTokenEnd()) { + int start = scanner.getTokenOffset() - 1; + while (start >= 0) { + char ch = text.charAt(start); + if (ch == '/') { + collectCloseTagSuggestions(start, false, scanner.getTokenEnd(), completionRequest, + completionResponse); + return completionResponse; + } else if (!isWhitespace(ch)) { + break; + } + start--; + } } - } - break; - case EndTagClose: - if (offset <= scanner.getTokenEnd()) { - if (currentTag != null && currentTag.length() > 0) { + break; + case StartTagClose: + if (offset <= scanner.getTokenEnd()) { + if (currentTag != null && currentTag.length() > 0) { + collectInsideContent(completionRequest, completionResponse); + return completionResponse; + } + } + break; + case StartTagSelfClose: + if (offset <= scanner.getTokenEnd()) { + if (currentTag != null && currentTag.length() > 0 + && xmlDocument.getText().charAt(offset - 1) == '>') { // if the actual character typed + // was + // '>' + collectInsideContent(completionRequest, completionResponse); + return completionResponse; + } + } + break; + case EndTagClose: + if (offset <= scanner.getTokenEnd()) { + if (currentTag != null && currentTag.length() > 0) { + collectInsideContent(completionRequest, completionResponse); + return completionResponse; + } + } + break; + case Content: + if (completionRequest.getXMLDocument().isDTD() + || completionRequest.getXMLDocument().isWithinInternalDTD(offset)) { + if (scanner.getTokenOffset() <= offset) { + return completionResponse; + } + break; + } + if (offset <= scanner.getTokenEnd()) { collectInsideContent(completionRequest, completionResponse); return completionResponse; } - } - break; - case Content: - if (completionRequest.getXMLDocument().isDTD() - || completionRequest.getXMLDocument().isWithinInternalDTD(offset)) { - if (scanner.getTokenOffset() <= offset) { - collectInsideDTDContent(completionRequest, completionResponse, true); - return completionResponse; + break; + case StartPrologOrPI: { + try { + boolean isFirstNode = xmlDocument.positionAt(scanner.getTokenOffset()).getLine() == 0; + if (isFirstNode && offset <= scanner.getTokenEnd()) { + collectPrologSuggestion(scanner.getTokenEnd(), "", completionRequest, completionResponse, + settings); + return completionResponse; + } + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "In XMLCompletions, StartPrologOrPI position error", e); } break; } - if (offset <= scanner.getTokenEnd()) { - collectInsideContent(completionRequest, completionResponse); - return completionResponse; - } - break; - case StartPrologOrPI: { - try { - boolean isFirstNode = xmlDocument.positionAt(scanner.getTokenOffset()).getLine() == 0; - if (isFirstNode && offset <= scanner.getTokenEnd()) { - collectPrologSuggestion(scanner.getTokenEnd(), "", completionRequest, completionResponse, - settings); - return completionResponse; + case PIName: { + try { + boolean isFirstNode = xmlDocument.positionAt(scanner.getTokenOffset()).getLine() == 0; + if (isFirstNode && offset <= scanner.getTokenEnd()) { + String substringXML = "xml".substring(0, scanner.getTokenText().length()); + if (scanner.getTokenText().equals(substringXML)) { + PrologModel.computePrologCompletionResponses(scanner.getTokenEnd(), + scanner.getTokenText(), completionRequest, completionResponse, true, settings); + return completionResponse; + } + } + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "In XMLCompletions, StartPrologOrPI position error", e); } - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "In XMLCompletions, StartPrologOrPI position error", e); + break; } - break; - } - case PIName: { - try { - boolean isFirstNode = xmlDocument.positionAt(scanner.getTokenOffset()).getLine() == 0; - if (isFirstNode && offset <= scanner.getTokenEnd()) { - String substringXML = "xml".substring(0, scanner.getTokenText().length()); - if (scanner.getTokenText().equals(substringXML)) { - PrologModel.computePrologCompletionResponses(scanner.getTokenEnd(), scanner.getTokenText(), - completionRequest, completionResponse, true, settings); + case PrologName: { + try { + boolean isFirstNode = xmlDocument.positionAt(scanner.getTokenOffset()).getLine() == 0; + if (isFirstNode && offset <= scanner.getTokenEnd()) { + collectPrologSuggestion(scanner.getTokenEnd(), scanner.getTokenText(), completionRequest, + completionResponse, settings); return completionResponse; } + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "In XMLCompletions, PrologName position error", e); } - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "In XMLCompletions, StartPrologOrPI position error", e); + break; } - break; - } - case PrologName: { - try { - boolean isFirstNode = xmlDocument.positionAt(scanner.getTokenOffset()).getLine() == 0; - if (isFirstNode && offset <= scanner.getTokenEnd()) { - collectPrologSuggestion(scanner.getTokenEnd(), scanner.getTokenText(), completionRequest, - completionResponse, settings); + // DTD + case DTDAttlistAttributeName: + case DTDAttlistAttributeType: + case DTDAttlistAttributeValue: + case DTDStartAttlist: + case DTDStartElement: + case DTDStartEntity: + case DTDEndTag: + case DTDStartInternalSubset: + case DTDEndInternalSubset: { + if (scanner.getTokenOffset() <= offset) { return completionResponse; } - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "In XMLCompletions, PrologName position error", e); + break; } - break; - } - // DTD - case DTDAttlistAttributeName: - case DTDAttlistAttributeType: - case DTDAttlistAttributeValue: - case DTDStartAttlist: - case DTDStartElement: - case DTDStartEntity: - case DTDEndTag: - case DTDStartInternalSubset: - case DTDEndInternalSubset: { - if (scanner.getTokenOffset() <= offset) { - collectInsideDTDContent(completionRequest, completionResponse); - return completionResponse; + + default: + if (offset <= scanner.getTokenEnd()) { + return completionResponse; + } + break; } - break; + token = scanner.scan(); } + return completionResponse; + } finally { + collectSnippetSuggestions(scanner, completionRequest, completionResponse); + } + } - default: - if (offset <= scanner.getTokenEnd()) { - return completionResponse; - } - break; + private void collectSnippetSuggestions(Scanner scanner, CompletionRequest completionRequest, + CompletionResponse completionResponse) { + DOMDocument document = completionRequest.getXMLDocument(); + String text = document.getText(); + int offset = completionRequest.getOffset(); + int limit = getExprLimitStart(completionRequest.getNode(), offset); + int start = getExprStart(text, offset, limit); + // String token = text.substring(start, offset); + try { + Range replaceRange = getReplaceRange(start, offset, completionRequest); + completionRequest.setReplaceRange(replaceRange); + String lineDelimiter = completionRequest.getXMLDocument().lineDelimiter(replaceRange.getStart().getLine()); + List snippets = getSnippetRegistry().getCompletionItems(replaceRange, lineDelimiter, + completionRequest.canSupportMarkupKind(MarkupKind.MARKDOWN), + completionRequest.getCompletionSettings().isCompletionSnippetsSupported(), (context, model) -> { + if (context instanceof IXMLSnippetContext) { + return (((IXMLSnippetContext) context).isMatch(completionRequest, model)); + } + return false; + }); + for (CompletionItem completionItem : snippets) { + completionResponse.addCompletionItem(completionItem); } - token = scanner.scan(); + + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "In XMLCompletions, collectSnippetSuggestions position error", e); + } + } + + /** + * Returns the limit start offset of the expression according to the current + * node. + * + * @param currentNode the node. + * @param offset the offset. + * @return the limit start offset of the expression according to the current + * node. + */ + private static int getExprLimitStart(DOMNode currentNode, int offset) { + if (currentNode == null) { + // should never occurs + return 0; + } + if (currentNode.isText()) { + return currentNode.getStart(); + } + if (currentNode.isComment() || currentNode.isCDATA()) { + if (offset >= currentNode.getEnd()) { + return currentNode.isClosed() ? currentNode.getEnd() : currentNode.getStart(); + } + return currentNode.getStart(); + } + if (!currentNode.isElement()) { + if (offset >= currentNode.getEnd()) { + // | + // --> in this case the offset of '>' is returned + return currentNode.getEnd(); + } + // processing instruction, comments, etc + // + // --> in this case the offset of '<' is returned + return currentNode.getStart(); + } + DOMElement element = (DOMElement) currentNode; + if (element.isInStartTag(offset)) { + return element.getStartTagOpenOffset(); + } + if (element.isInEndTag(offset)) { + return element.getEndTagOpenOffset(); } + if (offset >= currentNode.getEnd()) { + // | + return currentNode.getEnd(); + } + return element.getStartTagCloseOffset() + 1; + } - return completionResponse; + private static int getExprStart(String value, int offset, int end) { + if (offset == 0) { + return offset; + } + int index = offset - 1; + while (index > 0) { + if (Character.isWhitespace(value.charAt(index))) { + return index + 1; + } + if (index <= end) { + return end; + } + index--; + } + return index; } /** @@ -317,7 +411,7 @@ private static boolean isInsideDTDContent(DOMNode node, DOMDocument xmlDocument) return (node.getParentNode() != null && node.getParentNode().isDoctype()); } - public boolean isBalanced(DOMNode node) { + private boolean isBalanced(DOMNode node) { if (node.isClosed() == false) { return false; } @@ -641,8 +735,6 @@ private void collectInsideContent(CompletionRequest request, CompletionResponse } } collectionRegionProposals(request, response); - collectCDATACompletion(request, response); - collectCommentCompletion(request, response); } private void collectionRegionProposals(ICompletionRequest request, ICompletionResponse response) { @@ -682,79 +774,6 @@ private void collectionRegionProposals(ICompletionRequest request, ICompletionRe } } - private void collectCDATACompletion(ICompletionRequest request, ICompletionResponse response) { - try { - boolean isSnippetsSupported = request.isCompletionSnippetsSupported(); - InsertTextFormat insertFormat = request.getInsertTextFormat(); - - DOMDocument document = request.getXMLDocument(); - - String filter = "" : ""; - cdataProposal.setTextEdit(new TextEdit(editRange, textEdit)); - cdataProposal.setDocumentation("Insert "); - response.addCompletionItem(cdataProposal); - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "While performing collectCDATACompletion", e); - } - } - - private void collectCommentCompletion(ICompletionRequest request, ICompletionResponse response) { - try { - boolean isSnippetsSupported = request.isCompletionSnippetsSupported(); - InsertTextFormat insertFormat = request.getInsertTextFormat(); - - DOMDocument document = request.getXMLDocument(); - String filter = "" : ""; - commentProposal.setTextEdit(new TextEdit(editRange, textEdit)); - commentProposal.setDocumentation("Insert "); - response.addCompletionItem(commentProposal); - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "While performing collectCommentCompletion", e); - } - } - private void collectAttributeNameSuggestions(int nameStart, CompletionRequest completionRequest, CompletionResponse completionResponse) { collectAttributeNameSuggestions(nameStart, completionRequest.getOffset(), completionRequest, @@ -836,90 +855,6 @@ private void collectAttributeValueSuggestions(int valueStart, int valueEnd, Comp } } - private void collectInsideDTDContent(CompletionRequest request, CompletionResponse response) { - collectInsideDTDContent(request, response, false); - } - - private void collectInsideDTDContent(CompletionRequest request, CompletionResponse response, boolean isContent) { - // Insert DTD Element Declaration - // see https://www.w3.org/TR/REC-xml/#dt-eldecl - boolean isSnippetsSupported = request.isCompletionSnippetsSupported(); - InsertTextFormat insertFormat = request.getInsertTextFormat(); - CompletionItem elementDecl = new CompletionItem(); - elementDecl.setLabel("Insert DTD Element declaration"); - elementDecl.setKind(CompletionItemKind.EnumMember); - elementDecl.setFilterText("" - : ""; - elementDecl.setTextEdit(new TextEdit(editRange, textEdit)); - elementDecl.setDocumentation(""); - response.addCompletionItem(elementDecl); - - // Insert DTD AttrList Declaration - // see https://www.w3.org/TR/REC-xml/#attdecls - CompletionItem attrListDecl = new CompletionItem(); - attrListDecl.setLabel("Insert DTD Attributes list declaration"); - attrListDecl.setKind(CompletionItemKind.EnumMember); - attrListDecl.setFilterText("" - : ""; - attrListDecl.setTextEdit(new TextEdit(editRange, textEdit)); - attrListDecl.setDocumentation(""); - response.addCompletionItem(attrListDecl); - - // Insert Internal DTD Entity Declaration - // see https://www.w3.org/TR/REC-xml/#dt-entdecl - CompletionItem internalEntity = new CompletionItem(); - internalEntity.setLabel("Insert Internal DTD Entity declaration"); - internalEntity.setKind(CompletionItemKind.EnumMember); - internalEntity.setFilterText("" - : ""; - internalEntity.setTextEdit(new TextEdit(editRange, textEdit)); - internalEntity.setDocumentation(""); - response.addCompletionItem(internalEntity); - - // Insert External DTD Entity Declaration - // see https://www.w3.org/TR/REC-xml/#dt-entdecl - CompletionItem externalEntity = new CompletionItem(); - externalEntity.setLabel("Insert External DTD Entity declaration"); - externalEntity.setKind(CompletionItemKind.EnumMember); - externalEntity.setFilterText("" - : ""; - externalEntity.setTextEdit(new TextEdit(editRange, textEdit)); - externalEntity.setDocumentation(""); - response.addCompletionItem(externalEntity); - } - private static int scanNextForEndPos(int offset, Scanner scanner, TokenType nextToken) { if (offset == scanner.getTokenEnd()) { TokenType token = scanner.scan(); @@ -990,4 +925,11 @@ private static String getLineIndent(int offset, String text) { private boolean isEmptyElement(String tag) { return false; } + + private SnippetRegistry getSnippetRegistry() { + if (snippetRegistry == null) { + snippetRegistry = new SnippetRegistry(); + } + return snippetRegistry; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CDATASnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CDATASnippetContext.java new file mode 100644 index 0000000000..47d2816424 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CDATASnippetContext.java @@ -0,0 +1,24 @@ +package org.eclipse.lemminx.services.snippets; + +import java.util.Map; + +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +public class CDATASnippetContext implements IXMLSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new CDATASnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + if (SnippetContextUtils.isBracketExpression(request)) { + DOMElement parent = request.getParentElement(); + if (parent == null) { + return false; + } + return true; + } + return false; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CommentSnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CommentSnippetContext.java new file mode 100644 index 0000000000..b9391cde11 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CommentSnippetContext.java @@ -0,0 +1,38 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.snippets; + +import java.util.Map; + +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; +import org.w3c.dom.Node; + +/** + * Comment snippet context used to filter the comment snippets. + * + */ +public class CommentSnippetContext implements IXMLSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new CommentSnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + DOMNode node = request.getNode(); + if (node.getNodeType() == Node.DOCUMENT_TYPE_NODE || node.isComment()) { + // comments are allowed inside DTD + return true; + } + return SnippetContextUtils.isBracketExpression(request); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DTDNodeSnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DTDNodeSnippetContext.java new file mode 100644 index 0000000000..03d9435662 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DTDNodeSnippetContext.java @@ -0,0 +1,31 @@ +package org.eclipse.lemminx.services.snippets; + +import java.util.Map; + +import org.eclipse.lemminx.dom.DOMDocumentType; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +public class DTDNodeSnippetContext implements IXMLSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new DTDNodeSnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + DOMNode node = request.getNode(); + if (node == null) { + return false; + } + DOMDocumentType docType = null; + if (node.isDoctype()) { + docType = (DOMDocumentType) node; + } else if (node.getParentNode() != null && node.getParentNode().isDoctype()) { + docType = (DOMDocumentType) node.getParentNode(); + } + if (docType == null || !request.getXMLDocument().isWithinInternalDTD(request.getOffset())) { + return false; + } + return true; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DocTypeSnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DocTypeSnippetContext.java new file mode 100644 index 0000000000..ebf3d28476 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DocTypeSnippetContext.java @@ -0,0 +1,59 @@ +package org.eclipse.lemminx.services.snippets; + +import java.util.Map; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +public class DocTypeSnippetContext implements IXMLSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new DocTypeSnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + DOMDocument document = request.getXMLDocument(); + DOMElement documentElement = document.getDocumentElement(); + if (documentElement == null) { + return false; + } + if (document.getDoctype() != null) { + return false; + } + String tagName = documentElement.getTagName(); + if (tagName == null && documentElement.hasChildNodes() && documentElement.getChild(0).isElement()) { + documentElement = ((DOMElement) documentElement.getChild(0)); + tagName = documentElement.getTagName(); + } + if (tagName == null) { + return false; + } + DOMNode node = request.getNode(); + DOMNode parent = node.getParentNode(); + if (parent != null && parent.isDoctype()) { + // inside DTD + return false; + } + int offset = request.getOffset(); + if ((node.isProcessingInstruction() || node.isProlog()) && offset < node.getEnd()) { + // inside xml processing instruction + // --> + return false; + } + + if (offset > documentElement.getStart()) { + return false; + } + DOMNode previous = documentElement.getPreviousSibling(); + while (previous != null) { + if (!(previous.isText() || previous.isProlog() || previous.isProcessingInstruction())) { + return false; + } + previous = previous.getPreviousSibling(); + } + model.put("root-element", tagName); + return true; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/IXMLSnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/IXMLSnippetContext.java new file mode 100644 index 0000000000..3a7129773b --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/IXMLSnippetContext.java @@ -0,0 +1,8 @@ +package org.eclipse.lemminx.services.snippets; + +import org.eclipse.lemminx.commons.snippets.ISnippetContext; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +public interface IXMLSnippetContext extends ISnippetContext { + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/NewFileSnippetContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/NewFileSnippetContext.java new file mode 100644 index 0000000000..76d3fe9ff5 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/NewFileSnippetContext.java @@ -0,0 +1,54 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.snippets; + +import java.util.Map; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +/** + * Snippet context used to filter snippets if XML file is empty or not. + * + */ +public class NewFileSnippetContext implements IXMLSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new NewFileSnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + DOMDocument document = request.getXMLDocument(); + if (!document.hasChildNodes()) { + // Empty file + return true; + } + // The file contains some contents, the contents allowed are: + // - comments + // - processing instruction + // - text + // - '<', ' model) { + DOMDocument document = request.getXMLDocument(); + // check if document already defined a xml processing instruction. + if (hasProlog(document)) { + return false; + } + Position start = request.getReplaceRange().getStart(); + if (start.getLine() > 0) { + // The xml processing instruction must be declared in the first line. + return false; + } + // No xml processing instruction, check if completion was triggered before the + // document element + DOMElement documentElement = document.getDocumentElement(); + if (documentElement != null && documentElement.getTagName() != null) { + int offset = request.getOffset(); + return offset <= documentElement.getStart(); + } + return true; + } + + private static boolean hasProlog(DOMDocument document) { + DOMNode node = document.getFirstChild(); + if (node == null) { + return false; + } + return node.isProlog(); + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/SnippetContextUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/SnippetContextUtils.java new file mode 100644 index 0000000000..81dc41e9c9 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/SnippetContextUtils.java @@ -0,0 +1,96 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.snippets; + +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; +import org.w3c.dom.Node; + +public class SnippetContextUtils { + + private SnippetContextUtils() { + + } + + /** + * Returns true if the expression (with or without bracket) can be proceed + * according the completion trigger and false otherwise. + * + * @param request the completion request. + * @return true if the expression (with or without bracket) can be proceed + * according the completion trigger and false otherwise. + */ + public static boolean isBracketExpression(ICompletionRequest request) { + DOMNode node = request.getNode(); + int offset = request.getOffset(); + if (node.getNodeType() == Node.DOCUMENT_NODE) { + // | + return true; + } + if (node.isElement()) { + DOMElement element = (DOMElement) node; + if (element.getTagName() == null) { + // <| + // | + // + String text = request.getXMLDocument().getText(); + if (text.charAt(offset - 1) == '/') { + // -> should be ignore + return false; + } + return true; + } + if (element.isInStartTag(offset)) { + // + // + // | + return offset >= node.getEnd(); + } + if (element.isInEndTag(offset)) { + // + return false; + } + if (!element.hasEndTag()) { + // | + // should be ignore + return false; + } + return true; + } + if (offset >= node.getEnd()) { + // | + return true; + } + return false; + } + if (offset > node.getEnd()) { + DOMElement parent = node.getParentElement(); + if (parent != null && parent.isInEndTag(offset)) { + return false; + } + } + if (offset < node.getEnd()) { + // --> should ignore expression + // text node like | -> it can be an expression + return node.isText(); + } + return true; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/XMLSnippetRegistryLoader.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/XMLSnippetRegistryLoader.java new file mode 100644 index 0000000000..e126215e7b --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/XMLSnippetRegistryLoader.java @@ -0,0 +1,24 @@ +package org.eclipse.lemminx.services.snippets; + +import org.eclipse.lemminx.commons.snippets.ISnippetRegistryLoader; +import org.eclipse.lemminx.commons.snippets.SnippetRegistry; + +public class XMLSnippetRegistryLoader implements ISnippetRegistryLoader { + + @Override + public void load(SnippetRegistry registry) throws Exception { + registry.registerSnippets(XMLSnippetRegistryLoader.class.getResourceAsStream("newfile-snippets.json"), + NewFileSnippetContext.DEFAULT_CONTEXT); + registry.registerSnippets(XMLSnippetRegistryLoader.class.getResourceAsStream("cdata-snippets.json"), + CDATASnippetContext.DEFAULT_CONTEXT); + registry.registerSnippets(XMLSnippetRegistryLoader.class.getResourceAsStream("comment-snippets.json"), + CommentSnippetContext.DEFAULT_CONTEXT); + registry.registerSnippets(XMLSnippetRegistryLoader.class.getResourceAsStream("doctype-snippets.json"), + DocTypeSnippetContext.DEFAULT_CONTEXT); + registry.registerSnippets(XMLSnippetRegistryLoader.class.getResourceAsStream("prolog-snippets.json"), + PrologSnippetContext.DEFAULT_CONTEXT); + registry.registerSnippets(XMLSnippetRegistryLoader.class.getResourceAsStream("dtdnode-snippets.json"), + DTDNodeSnippetContext.DEFAULT_CONTEXT); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java index 2e67ddedc4..0e6000f843 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java @@ -39,13 +39,14 @@ public static boolean isQuote(char c) { return c == '\'' || c == '"'; } - public static boolean isWhitespace(String value) { + public static boolean isWhitespace(String value, int index) { + return isWhitespace(value, index, value.length()); + } + public static boolean isWhitespace(String value, int index, int end) { if (value == null) { return false; } char c; - int end = value.length(); - int index = 0; while (index < end) { c = value.charAt(index); if (Character.isWhitespace(c) == false) { @@ -56,6 +57,10 @@ public static boolean isWhitespace(String value) { return true; } + public static boolean isWhitespace(String value) { + return isWhitespace(value, 0); + } + /** * Normalizes the whitespace characters of a given string and applies it to the * given string builder. @@ -379,32 +384,6 @@ public static boolean isTagOutsideOfBackticks(String text) { } - public static int findExprBeforeAt(String text, String expr, int offset) { - if (offset <= 0) { - return -1; - } - expr = expr.toUpperCase(); - int startOffset = -1; - char first = expr.charAt(0); - int length = Math.min(offset, expr.length()); - int i = 0; - for (i = 1; i <= length; i++) { - if (Character.toUpperCase(text.charAt(offset - i)) == first) { - startOffset = offset - i; - break; - } - } - if (startOffset == -1) { - return -1; - } - for (int j = 0; j < i; j++) { - if (Character.toUpperCase(text.charAt(startOffset + j)) != expr.charAt(j)) { - return -1; - } - } - return startOffset - 1; - } - public static String getString(Object obj) { if (obj != null) { return obj.toString(); diff --git a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.commons.snippets.ISnippetRegistryLoader b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.commons.snippets.ISnippetRegistryLoader new file mode 100644 index 0000000000..ea64856b8d --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.commons.snippets.ISnippetRegistryLoader @@ -0,0 +1 @@ +org.eclipse.lemminx.services.snippets.XMLSnippetRegistryLoader \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/cdata-snippets.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/cdata-snippets.json new file mode 100644 index 0000000000..11aff00ab9 --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/cdata-snippets.json @@ -0,0 +1,11 @@ +{ + "Insert CDATA": { + "prefix": [ + "" + ], + "description": "Insert CDATA" + } + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/comment-snippets.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/comment-snippets.json new file mode 100644 index 0000000000..c008a3191f --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/comment-snippets.json @@ -0,0 +1,11 @@ +{ + "Insert Comment": { + "prefix": [ + "" + ], + "description": "Insert Comment" + } + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/doctype-snippets.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/doctype-snippets.json new file mode 100644 index 0000000000..3e1b5ecfec --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/doctype-snippets.json @@ -0,0 +1,53 @@ +{ + "Insert SYSTEM DOCTYPE": { + "prefix": [ + "" + ], + "description": "Insert SYSTEM DOCTYPE" + }, + "Insert PUBLIC DOCTYPE": { + "prefix": [ + "" + ], + "description": "Insert PUBLIC DOCTYPE" + }, + "Insert SYSTEM DOCTYPE with subset": { + "prefix": [ + "" + ], + "description": "Insert SYSTEM DOCTYPE with subset" + }, + "Insert PUBLIC DOCTYPE with subset": { + "prefix": [ + "" + ], + "description": "Insert PUBLIC DOCTYPE with subset" + }, + "Insert DOCTYPE with subset": { + "prefix": [ + "", + "]>" + ], + "description": "Insert DOCTYPE with subset" + } + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/dtdnode-snippets.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/dtdnode-snippets.json new file mode 100644 index 0000000000..eb08db861e --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/dtdnode-snippets.json @@ -0,0 +1,38 @@ +{ + "Insert DTD Element Declaration": { + "prefix": [ + "" + ], + "description": "Insert DTD Element Declaration" + }, + "Insert DTD Attributes List Declaration": { + "prefix": [ + "" + ], + "description": "Insert DTD Attributes List Declaration" + }, + "Insert Internal DTD Entity Declaration": { + "prefix": [ + "" + ], + "description": "Insert Internal DTD Entity Declaration" + }, + "Insert External DTD Entity Declaration": { + "prefix": [ + "" + ], + "description": "Insert External DTD Entity Declaration" + } + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/newfile-snippets.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/newfile-snippets.json new file mode 100644 index 0000000000..6e66be8472 --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/newfile-snippets.json @@ -0,0 +1,63 @@ +{ + "Insert SYSTEM DOCTYPE": { + "prefix": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "Insert SYSTEM DOCTYPE" + }, + "Insert PUBLIC DOCTYPE": { + "prefix": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "Insert PUBLIC DOCTYPE" + }, + "Insert SYSTEM DOCTYPE with subset": { + "prefix": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "Insert SYSTEM DOCTYPE with subset" + }, + "Insert PUBLIC DOCTYPE with subset": { + "prefix": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "Insert PUBLIC DOCTYPE with subset" + }, + "Insert DOCTYPE with subset": { + "prefix": [ + "", + "]>", + "<${1:root-element}>${0}", + "" + ], + "description": "Insert DOCTYPE with subset" + } + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/prolog-snippets.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/prolog-snippets.json new file mode 100644 index 0000000000..d99ce78b31 --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/prolog-snippets.json @@ -0,0 +1,20 @@ +{ + "Insert xml Processing Instruction": { + "prefix": [ + "${0}" + ], + "description": "Insert xml Processing Instruction" + }, + "Insert xml Processing Instruction with standalone": { + "prefix": [ + "${0}" + ], + "description": "Insert xml Processing Instruction with standalone" + } + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java index 48fadcda69..a4f266ca04 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java @@ -12,7 +12,13 @@ */ package org.eclipse.lemminx; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; import java.nio.file.Paths; @@ -75,13 +81,27 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; /** - * XML + * XML * */ public class XMLAssert { // ------------------- Completion assert + public static final int COMMENT_SNIPPETS = 1; + + public static final int CDATA_SNIPPETS = 1; + + public static final int DOCTYPE_SNIPPETS = 5; + + public static final int DTDNODE_SNIPPETS = 4; + + public static final int NEWFILE_SNIPPETS = 5; + + public static final int PROLOG_SNIPPETS = 2; + + public static final int REGION_SNIPPETS = 2; + private static final String FILE_URI = "test.xml"; public static class SettingsSaveContext extends AbstractSaveContext { @@ -177,7 +197,7 @@ public static void testCompletionFor(XMLLanguageService xmlLanguageService, Stri String previous = null; for (String label : labels) { assertNotEquals(previous, label, () -> { - return "Duplicate label " + label + " in " + labels.stream().collect(Collectors.joining(",")) + "}"; + return "Duplicate label " + label + " in " + labels.stream().collect(Collectors.joining(",")) + "}"; }); previous = label; } @@ -197,17 +217,16 @@ private static void assertCompletion(CompletionList completions, CompletionItem return expected.getLabel().equals(completion.getLabel()); }).collect(Collectors.toList()); - assertEquals( 1, matches.size(), ()-> { + assertEquals(1, matches.size(), () -> { return expected.getLabel() + " should only exist once: Actual: " - + completions.getItems().stream().map(c -> c.getLabel()).collect(Collectors.joining(",")); - }); + + completions.getItems().stream().map(c -> c.getLabel()).collect(Collectors.joining(",")); + }); CompletionItem match = matches.get(0); /* * if (expected.documentation != null) { - * assertEquals(match.getDocumentation().getRight().getValue(), - * expected.getd); } if (expected.kind) { assertEquals(match.kind, - * expected.kind); } + * assertEquals(match.getDocumentation().getRight().getValue(), expected.getd); + * } if (expected.kind) { assertEquals(match.kind, expected.kind); } */ if (expected.getTextEdit() != null && match.getTextEdit() != null) { if (expected.getTextEdit().getNewText() != null) { @@ -451,13 +470,13 @@ public static PublishDiagnosticsParams pd(String uri, Diagnostic... diagnostics) // ------------------- CodeAction assert - public static void testCodeActionsFor(String xml, Diagnostic diagnostic, - CodeAction... expected) throws BadLocationException { + public static void testCodeActionsFor(String xml, Diagnostic diagnostic, CodeAction... expected) + throws BadLocationException { testCodeActionsFor(xml, diagnostic, null, expected); } - public static void testCodeActionsFor(String xml, Diagnostic diagnostic, String catalogPath, - CodeAction... expected) throws BadLocationException { + public static void testCodeActionsFor(String xml, Diagnostic diagnostic, String catalogPath, CodeAction... expected) + throws BadLocationException { SharedSettings settings = new SharedSettings(); settings.getFormattingSettings().setTabSize(4); settings.getFormattingSettings().setInsertSpaces(false); @@ -534,8 +553,7 @@ public static CodeAction ca(Diagnostic d, TextEdit... te) { VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(FILE_URI, 0); - TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, - Arrays.asList(te)); + TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, Arrays.asList(te)); WorkspaceEdit workspaceEdit = new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit))); codeAction.setEdit(workspaceEdit); return codeAction; @@ -629,9 +647,9 @@ public static DocumentLink dl(Range range, String target) { public static void assertDocumentLinks(List actual, DocumentLink... expected) { assertEquals(expected.length, actual.size()); for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i].getRange(), actual.get(i).getRange()," Range test '" + i + "' link"); - assertEquals(Paths.get(expected[i].getTarget()).toUri().toString(), - actual.get(i).getTarget()," Target test '" + i + "' link"); + assertEquals(expected[i].getRange(), actual.get(i).getRange(), " Range test '" + i + "' link"); + assertEquals(Paths.get(expected[i].getTarget()).toUri().toString(), actual.get(i).getTarget(), + " Target test '" + i + "' link"); } } @@ -640,7 +658,9 @@ public static void assertDocumentLinks(List actual, DocumentLink.. public static void testDocumentSymbolsFor(String xml, DocumentSymbol... expected) { testDocumentSymbolsFor(xml, null, new XMLSymbolSettings(), expected); } - public static void testDocumentSymbolsFor(String xml, XMLSymbolSettings symbolSettings, DocumentSymbol... expected) { + + public static void testDocumentSymbolsFor(String xml, XMLSymbolSettings symbolSettings, + DocumentSymbol... expected) { testDocumentSymbolsFor(xml, null, symbolSettings, expected); } @@ -648,7 +668,8 @@ public static void testDocumentSymbolsFor(String xml, String fileURI, DocumentSy testDocumentSymbolsFor(xml, fileURI, new XMLSymbolSettings(), expected); } - public static void testDocumentSymbolsFor(String xml, String fileURI, XMLSymbolSettings symbolSettings, DocumentSymbol... expected) { + public static void testDocumentSymbolsFor(String xml, String fileURI, XMLSymbolSettings symbolSettings, + DocumentSymbol... expected) { TextDocument document = new TextDocument(xml, fileURI != null ? fileURI : "test.xml"); XMLLanguageService xmlLanguageService = new XMLLanguageService(); @@ -913,8 +934,7 @@ public static void assertHighlights(String value, int[] expectedMatches, String assertEquals(expectedMatches[i], actualStartOffset); int actualEndOffset = document.offsetAt(highlight.getRange().getEnd()); assertEquals(expectedMatches[i] + (elementName != null ? elementName.length() : 0), actualEndOffset); - assertEquals(elementName, - document.getText().substring(actualStartOffset, actualEndOffset).toLowerCase()); + assertEquals(elementName, document.getText().substring(actualStartOffset, actualEndOffset).toLowerCase()); } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDCompletionExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDCompletionExtensionsTest.java index b41468156a..48cef9468a 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDCompletionExtensionsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDCompletionExtensionsTest.java @@ -11,6 +11,8 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.contentmodel; +import static org.eclipse.lemminx.XMLAssert.COMMENT_SNIPPETS; +import static org.eclipse.lemminx.XMLAssert.DTDNODE_SNIPPETS; import static org.eclipse.lemminx.XMLAssert.c; import static org.eclipse.lemminx.XMLAssert.te; @@ -65,10 +67,11 @@ public void testCompletionDocumentationWithSource() throws BadLocationException " \"http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd\">\r\n" + // "\r\n" + // " <|"; - testCompletionFor(xml, c("catalog", te(5, 2, 5, 3, "$1$0"), "$1$0"), "\r\n" + // " " + // ""; - testCompletionFor(xml, c("Insert DTD Element declaration", te(3, 1, 3, 11, ""), ""), "\r\n" + // " " + // ""; - testCompletionFor(xml, c("Insert DTD Element declaration", te(3, 1, 3, 7, ""), ""), "\r\n" + // " " + // ""; - testCompletionFor(xml, true, 4, c("Insert DTD Element declaration", te(3, 1, 3, 1, ""), ""), ""), ""), ""), + ""), ""), + ""), "\r\n" + // " " + // ""; - testCompletionFor(xml, false, 4, c("Insert DTD Element declaration", te(3, 1, 3, 1, ""), ""), ""), ""), ""), ""), + ""), ""), "\n" + - "\n" + - "\n" + - " \n" + - " <|\n" + // <-- completion - " \n" + - ""; - testCompletionFor(xml, false, 3 + 2 /* CDATA and Comments */, c("desc", te(4, 8, 4, 9, ""), ""), ""), "\n" + + "\n" + + "\n" + " \n" + + " <|\n" + // <-- completion + " \n" + ""; + testCompletionFor(xml, false, 3 + 2 /* CDATA and Comments */, + c("desc", te(4, 8, 4, 9, ""), ""), ""), ""), c("End with ''", ""), c("#region", ""), - c("#endregion", ""), c("cdata", ""), // - c("comment", "")); + c("#endregion", ""), c("Insert CDATA", ""), // + c("Insert Comment", "")); xml = "\r\n" + // ""; XMLAssert.testCompletionFor(xml, null, "src/test/resources/sequence.xml", 3 + 2 /* CDATA and Comments */, c("tag", ""), c("#region", ""), c("#endregion", ""), - c("cdata", ""), c("comment", "")); + c("Insert CDATA", ""), c("Insert Comment", "")); xml = "\r\n" + // "" + lineSeparator() + // + "" + lineSeparator() + // + "", // + r(0, 0, 0, 0), "", // + r(0, 0, 0, 0), "", // + r(0, 0, 0, 0), "", // + r(0, 0, 0, 1), "", // + r(0, 0, 0, 2), "|", DOCTYPE_SNIPPETS /* DOCTYPE snippets */ + // + PROLOG_SNIPPETS /* Prolog snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("Insert SYSTEM DOCTYPE", // + "" + lineSeparator() + // + "" + lineSeparator() + // + "", // + r(0, 8, 0, 8), "", // + r(0, 8, 0, 8), "", // + r(0, 38, 0, 38), "", // + r(0, 38, 0, 39), "", // + r(0, 38, 0, 39), "", // + r(0, 38, 0, 40), "", // + r(1, 0, 1, 0), "", // + r(0, 0, 0, 0), "", // + r(0, 0, 0, 1), "", // + r(0, 0, 0, 2), "", // + r(0, 0, 0, 3), "", // + r(0, 0, 0, 4), "", // + r(0, 0, 0, 2), "", // + r(0, 3, 0, 3), "", // + r(0, 3, 0, 4), "", // + r(0, 3, 0, 5), "", // + r(0, 4, 0, 4), "", // + r(0, 4, 0, 5), "", // + r(0, 4, 0, 6), "", // + r(0, 3, 0, 3), "", // + r(0, 4, 0, 4), "", // + r(0, 4, 0, 4), "", // + r(0, 3, 0, 3), "", // + r(0, 7, 0, 7), "