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/ISuffixPositionProvider.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISuffixPositionProvider.java new file mode 100644 index 0000000000..835c94f97f --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/ISuffixPositionProvider.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 org.eclipse.lsp4j.Position; + +/** + * Suffix position provider API. + * + * @author Angelo ZERR + * + */ +public interface ISuffixPositionProvider { + + /** + * Returns the suffix position provider of the given sufix and null + * otherwise. + * + * @param suffix + * @return the suffix position provider of the given sufix and null + * otherwise. + */ + Position findSuffixPosition(String suffix); +} 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..dd93272115 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/Snippet.java @@ -0,0 +1,98 @@ +/******************************************************************************* +* 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 String suffix; + + 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 String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + 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..7ddaace2d7 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetDeserializer.java @@ -0,0 +1,117 @@ +/******************************************************************************* +* 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 SUFFIX_ELT = "suffix"; + + 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); + + // suffix + JsonElement suffixElt = snippetObj.get(SUFFIX_ELT); + if (suffixElt != null) { + String suffix = suffixElt.getAsString(); + snippet.setSuffix(suffix); + } + + // 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..5ee1fb381d --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/snippets/SnippetRegistry.java @@ -0,0 +1,391 @@ +/******************************************************************************* +* 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.lemminx.utils.StringUtils; +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.Position; +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, + ISuffixPositionProvider suffixProvider) { + 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(prefix); + String insertText = getInsertText(snippet, model, !snippetsSupported, lineDelimiter); + item.setKind(CompletionItemKind.Snippet); + item.setDocumentation( + Either.forRight(createDocumentation(snippet, model, canSupportMarkdown, lineDelimiter))); + item.setFilterText(prefix); + item.setDetail(snippet.getDescription()); + Range range = replaceRange; + if (!StringUtils.isEmpty(snippet.getSuffix()) && suffixProvider != null) { + Position end = suffixProvider.findSuffixPosition(snippet.getSuffix()); + if (end != null) { + range = new Range(replaceRange.getStart(), end); + } + } + item.setTextEdit(new TextEdit(range, insertText)); + item.setInsertTextFormat(InsertTextFormat.Snippet); + return item; + + }).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)) { + if (replace) { + newLine.append(line.substring(offset, dollarIndex)); + } + int lastDigitOffset = dollarIndex + 1; + while (Character.isDigit(line.charAt(lastDigitOffset))) { + lastDigitOffset++; + } + if (!replace) { + newLine.append(line.substring(offset, 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/extensions/prolog/PrologModel.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/prolog/PrologModel.java index fa146f5f2f..c6e227d40a 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/prolog/PrologModel.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/prolog/PrologModel.java @@ -11,44 +11,35 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.prolog; -import java.text.MessageFormat; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.logging.Level; import java.util.logging.Logger; -import com.google.common.base.Charsets; - import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.dom.DOMAttr; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMNode; -import org.eclipse.lemminx.dom.parser.ScannerState; -import org.eclipse.lemminx.dom.parser.TokenType; import org.eclipse.lemminx.services.AttributeCompletionItem; -import org.eclipse.lemminx.services.XMLCompletions; import org.eclipse.lemminx.services.extensions.ICompletionRequest; import org.eclipse.lemminx.services.extensions.ICompletionResponse; import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lemminx.utils.StringUtils; 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.w3c.dom.NamedNodeMap; +import com.google.common.base.Charsets; + /** * This class holds values that represent the XSI xsd. Can be seen at * https://www.w3.org/2001/XMLSchema-instance */ public class PrologModel { - - private static final Logger LOGGER = Logger.getLogger(PrologModel.class.getName()); - public static final String VERSION_NAME = "version"; public static final String ENCODING_NAME = "encoding"; public static final String STANDALONE_NAME = "standalone"; @@ -66,150 +57,103 @@ public class PrologModel { public static final String YES = "yes"; public static final String NO = "no"; - - + // Don't change order of list items - public static final List VERSION_VALUES = Arrays.asList(VERSION_1, VERSION_1_1); - public static final List ENCODING_VALUES = Arrays.asList(UTF_8, ISO_8859_1, WINDOWS_1251, WINDOWS_1252, SHIFT_JIS, GB2312, EUC_KR); + public static final List VERSION_VALUES = Arrays.asList(VERSION_1, VERSION_1_1); + public static final List ENCODING_VALUES = Arrays.asList(UTF_8, ISO_8859_1, WINDOWS_1251, WINDOWS_1252, + SHIFT_JIS, GB2312, EUC_KR); public static final List STANDALONE_VALUES = Arrays.asList(YES, NO); - - public static void computePrologCompletionResponses(int tokenEndOffset, String tag, ICompletionRequest request, - ICompletionResponse response, boolean inPIState, SharedSettings settings) { - DOMDocument document = request.getXMLDocument(); - CompletionItem item = new CompletionItem(); - - item.setLabel(""); - item.setKind(CompletionItemKind.Property); - item.setFilterText("xml version=\"1.0\" encoding=\"UTF-8\"?>"); - boolean isSnippetsSupported = request.isCompletionSnippetsSupported(); - InsertTextFormat insertText = isSnippetsSupported ? InsertTextFormat.Snippet : InsertTextFormat.PlainText; - item.setInsertTextFormat(insertText); - int closingBracketOffset; - if (inPIState) { - closingBracketOffset = XMLCompletions.getOffsetFollowedBy(document.getText(), tokenEndOffset, ScannerState.WithinPI, - TokenType.PIEnd); - } else {// prolog state - closingBracketOffset = XMLCompletions.getOffsetFollowedBy(document.getText(), tokenEndOffset, ScannerState.WithinTag, - TokenType.PrologEnd); - } - - if (closingBracketOffset != -1) { - // Include '?>' - closingBracketOffset += 2; - } else { - closingBracketOffset = XMLCompletions.getOffsetFollowedBy(document.getText(), tokenEndOffset, ScannerState.WithinTag, - TokenType.StartTagClose); - if (closingBracketOffset == -1) { - closingBracketOffset = tokenEndOffset; - } else { - closingBracketOffset++; - } - } - int startOffset = tokenEndOffset - tag.length(); - try { - Range editRange = XMLCompletions.getReplaceRange(startOffset, closingBracketOffset, request); - String q = settings.getPreferences().getQuotationAsString(); - String cursor = isSnippetsSupported ? "$0" : ""; - String text = MessageFormat.format("xml version={0}{1}{0} encoding={0}{2}{0}?>" + cursor , q, VERSION_1, UTF_8); - item.setTextEdit(new TextEdit(editRange, text)); - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "While performing getReplaceRange for prolog completion.", e); - } - response.addCompletionItem(item); - } - private static void createCompletionItem(String attrName, boolean canSupportSnippet, boolean generateValue, Range editRange, String defaultValue, Collection enumerationValues, String documentation, - ICompletionResponse response, SharedSettings sharedSettings){ + ICompletionResponse response, SharedSettings sharedSettings) { CompletionItem item = new AttributeCompletionItem(attrName, canSupportSnippet, editRange, generateValue, defaultValue, enumerationValues, sharedSettings); MarkupContent markup = new MarkupContent(); markup.setKind(MarkupKind.MARKDOWN); - + markup.setValue(StringUtils.getDefaultString(documentation)); item.setDocumentation(markup); response.addCompletionItem(item); } - public static void computeAttributeNameCompletionResponses(ICompletionRequest request, - ICompletionResponse response, Range editRange, DOMDocument document, - SharedSettings sharedSettings) - throws BadLocationException { + public static void computeAttributeNameCompletionResponses(ICompletionRequest request, ICompletionResponse response, + Range editRange, DOMDocument document, SharedSettings sharedSettings) throws BadLocationException { if (document.hasProlog() == false) { return; } int offset = document.offsetAt(editRange.getStart()); DOMNode prolog = document.findNodeAt(offset); - if(!prolog.isProlog()) { + if (!prolog.isProlog()) { return; } boolean isSnippetsSupported = request.isCompletionSnippetsSupported(); int attrIndex = getAttributeCompletionPosition(offset, prolog); - if(attrIndex == 0) { // 1st attribute - if(isCurrentAttributeEqual(VERSION_NAME, prolog, 0)) { + if (attrIndex == 0) { // 1st attribute + if (isCurrentAttributeEqual(VERSION_NAME, prolog, 0)) { return; } - createCompletionItem(VERSION_NAME, isSnippetsSupported, true, editRange, - VERSION_1, VERSION_VALUES, null, response, sharedSettings); + createCompletionItem(VERSION_NAME, isSnippetsSupported, true, editRange, VERSION_1, VERSION_VALUES, null, + response, sharedSettings); return; } - if(attrIndex == 1) { // 2nd attribute - if(!isCurrentAttributeEqual(ENCODING_NAME, prolog, 1)) { - createCompletionItem(ENCODING_NAME, isSnippetsSupported, true, editRange, - UTF_8, ENCODING_VALUES, null, response, sharedSettings); + if (attrIndex == 1) { // 2nd attribute + if (!isCurrentAttributeEqual(ENCODING_NAME, prolog, 1)) { + createCompletionItem(ENCODING_NAME, isSnippetsSupported, true, editRange, UTF_8, ENCODING_VALUES, null, + response, sharedSettings); } else { return; } - if(!isCurrentAttributeEqual(STANDALONE_NAME, prolog, 1)) { - createCompletionItem(STANDALONE_NAME, isSnippetsSupported, true, editRange, YES, - STANDALONE_VALUES, null, response, sharedSettings); + if (!isCurrentAttributeEqual(STANDALONE_NAME, prolog, 1)) { + createCompletionItem(STANDALONE_NAME, isSnippetsSupported, true, editRange, YES, STANDALONE_VALUES, + null, response, sharedSettings); } return; } - if(attrIndex == 2) { // 3rd attribute + if (attrIndex == 2) { // 3rd attribute DOMAttr attrBefore = prolog.getAttributeAtIndex(1); - if(!STANDALONE_NAME.equals(attrBefore.getName()) && !isCurrentAttributeEqual(STANDALONE_NAME, prolog, 2)) { - createCompletionItem(STANDALONE_NAME, isSnippetsSupported, true, editRange, YES, - STANDALONE_VALUES, null, response, sharedSettings); + if (!STANDALONE_NAME.equals(attrBefore.getName()) && !isCurrentAttributeEqual(STANDALONE_NAME, prolog, 2)) { + createCompletionItem(STANDALONE_NAME, isSnippetsSupported, true, editRange, YES, STANDALONE_VALUES, + null, response, sharedSettings); } return; } } - public static void computeValueCompletionResponses(ICompletionRequest request, - ICompletionResponse response, Range editRange, DOMDocument document) throws BadLocationException { - + public static void computeValueCompletionResponses(ICompletionRequest request, ICompletionResponse response, + Range editRange, DOMDocument document) throws BadLocationException { + if (document.hasProlog() == false) { return; } int offset = document.offsetAt(editRange.getStart()); DOMNode prolog = document.findNodeAt(offset); - if(!prolog.isProlog()) { + if (!prolog.isProlog()) { return; } DOMAttr attr = prolog.findAttrAt(offset); - if(VERSION_NAME.equals(attr.getName())) { // version + if (VERSION_NAME.equals(attr.getName())) { // version createCompletionItemsForValues(VERSION_VALUES, editRange, document, request, response); } - else if(ENCODING_NAME.equals(attr.getName())) { // encoding + else if (ENCODING_NAME.equals(attr.getName())) { // encoding createCompletionItemsForValues(ENCODING_VALUES, editRange, document, request, response); } - else if(STANDALONE_NAME.equals(attr.getName())) { + else if (STANDALONE_NAME.equals(attr.getName())) { createCompletionItemsForValues(STANDALONE_VALUES, editRange, document, request, response); } } - private static void createCompletionItemsForValues(Collection enumerationValues, Range editRange, DOMDocument document, ICompletionRequest request, ICompletionResponse response) { + private static void createCompletionItemsForValues(Collection enumerationValues, Range editRange, + DOMDocument document, ICompletionRequest request, ICompletionResponse response) { int sortText = 1; CompletionItem item; for (String option : enumerationValues) { @@ -226,7 +170,8 @@ private static void createCompletionItemsForValues(Collection enumeratio } /** - * Returns the position the offset is in in relation to the attributes and their order + * Returns the position the offset is in in relation to the attributes and their + * order * * example: * @@ -234,40 +179,41 @@ private static void createCompletionItemsForValues(Collection enumeratio * * This will return 2 since if you insert a new attribute there you can access * it from the list of attributes with this index. + * * @param completionOffset * @param element * @return */ private static int getAttributeCompletionPosition(int completionOffset, DOMNode element) { - + NamedNodeMap attributeList = element.getAttributes(); - - if(attributeList == null) { + + if (attributeList == null) { return 0; } - + int attributeListLength = attributeList.getLength(); - - if(attributeListLength == 0) { + if (attributeListLength == 0) { return 0; } - + DOMAttr attr; for (int i = 0; i < attributeListLength; i++) { attr = element.getAttributeAtIndex(i); - if(completionOffset <= attr.getStart()) { + if (completionOffset <= attr.getStart()) { return i; } } - + return attributeListLength; } /** - * Returns true if the current attribute in the given position of the element's list of attributes - * equals the provided attributeName + * Returns true if the current attribute in the given position of the element's + * list of attributes equals the provided attributeName + * * @param attributeName * @param element * @param position @@ -276,15 +222,15 @@ private static int getAttributeCompletionPosition(int completionOffset, DOMNode private static boolean isCurrentAttributeEqual(String attributeName, DOMNode element, int index) { NamedNodeMap attributeList = element.getAttributes(); - if(attributeList == null) { + if (attributeList == null) { return false; } - if(index >= attributeList.getLength()) { + if (index >= attributeList.getLength()) { return false; } - if(attributeName.equals(element.getAttributeAtIndex(index).getName())) { + if (attributeName.equals(element.getAttributeAtIndex(index).getName())) { return true; } 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 cf60d7de75..b0337d9359 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 @@ -45,7 +45,7 @@ public CompletionRequest(DOMDocument xmlDocument, Position position, SharedSetti super(xmlDocument, position, extensionsRegistry); this.sharedSettings = settings; } - + @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..cbf1b2c95b 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; @@ -34,11 +35,11 @@ import org.eclipse.lemminx.dom.parser.ScannerState; import org.eclipse.lemminx.dom.parser.TokenType; import org.eclipse.lemminx.dom.parser.XMLScanner; -import org.eclipse.lemminx.extensions.prolog.PrologModel; import org.eclipse.lemminx.services.extensions.ICompletionParticipant; 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 +47,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 +63,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 +80,299 @@ public CompletionList doComplete(DOMDocument xmlDocument, Position position, Sha return completionResponse; } + String text = xmlDocument.getText(); int offset = completionRequest.getOffset(); DOMNode node = completionRequest.getNode(); + 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; - } else if (!isWhitespace(ch)) { - break; + case BeforeAttributeValue: + collectAttributeValueSuggestions(scanner.getTokenEnd(), offset, completionRequest, + completionResponse); + return completionResponse; + 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)) { + 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, true); return completionResponse; } 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); + + default: + if (offset <= scanner.getTokenEnd()) { return completionResponse; } - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "In XMLCompletions, StartPrologOrPI position error", e); + break; } - break; + token = scanner.scan(); } - 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; + return completionResponse; + } finally { + collectSnippetSuggestions(completionRequest, completionResponse); + } + } + + /** + * Collect snippets suggestions. + * + * @param completionRequest completion request. + * @param completionResponse completion response. + */ + private void collectSnippetSuggestions(CompletionRequest completionRequest, CompletionResponse completionResponse) { + DOMDocument document = completionRequest.getXMLDocument(); + String text = document.getText(); + int endExpr = completionRequest.getOffset(); + // compute the from for search expression according to the node + int fromSearchExpr = getExprLimitStart(completionRequest.getNode(), endExpr); + // compute the start expression + int startExpr = getExprStart(text, fromSearchExpr, endExpr); + try { + Range replaceRange = getReplaceRange(startExpr, endExpr, completionRequest); + completionRequest.setReplaceRange(replaceRange); + String lineDelimiter = document.lineDelimiter(replaceRange.getStart().getLine()); + List snippets = getSnippetRegistry().getCompletionItems(replaceRange, lineDelimiter, + completionRequest.canSupportMarkupKind(MarkupKind.MARKDOWN), + completionRequest.getSharedSettings().getCompletionSettings().isCompletionSnippetsSupported(), + (context, model) -> { + if (context instanceof IXMLSnippetContext) { + return (((IXMLSnippetContext) context).isMatch(completionRequest, model)); } - } - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "In XMLCompletions, StartPrologOrPI position error", e); - } - break; + return false; + }, (suffix) -> { + // Search the prefix from the left of completion offset. + for (int i = endExpr + 1; i < text.length(); i++) { + char ch = text.charAt(i); + if (Character.isWhitespace(ch)) { + continue; + } else if (suffix.charAt(0) == ch) { + try { + return document.positionAt(i + 1); + } catch (BadLocationException e) { + return null; + } + } else { + // The current character is nor a whitespace, nor the prefix, we stop + break; + } + } + return null; + }); + for (CompletionItem completionItem : snippets) { + completionResponse.addCompletionItem(completionItem); } - 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); - } - break; + + } 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(); } - // 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; - } - break; + return currentNode.getStart(); + } + if (!currentNode.isElement()) { + if (offset >= currentNode.getEnd() && currentNode.isClosed()) { + // | + // --> 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; + } - default: - if (offset <= scanner.getTokenEnd()) { - return completionResponse; - } - break; + private static int getExprStart(String value, int from, int to) { + if (to == 0) { + return to; + } + int index = to - 1; + while (index > 0) { + if (Character.isWhitespace(value.charAt(index))) { + return index + 1; } - token = scanner.scan(); + if (index <= from) { + return from; + } + index--; } - - return completionResponse; + return index; } /** @@ -317,7 +393,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; } @@ -548,19 +624,6 @@ private static boolean isGenerateEndTag(CompletionRequest completionRequest, Str return node.getOrphanEndElement(offset, tagName) == null; } - /** - * Collect xml prolog completions. - * - * @param startOffset - * @param tag - * @param request - * @param response - */ - private void collectPrologSuggestion(int startOffset, String tag, CompletionRequest request, - CompletionResponse response, SharedSettings settings) { - PrologModel.computePrologCompletionResponses(startOffset, tag, request, response, false, settings); - } - private void collectCloseTagSuggestions(int afterOpenBracket, boolean inOpenTag, int tagNameEnd, CompletionRequest completionRequest, CompletionResponse completionResponse) { try { @@ -641,8 +704,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 +743,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 +824,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(); @@ -962,7 +866,7 @@ public static int getOffsetFollowedBy(String s, int offset, ScannerState intialS return (token == expectedToken) ? scanner.getTokenOffset() : -1; } - public static Range getReplaceRange(int replaceStart, int replaceEnd, ICompletionRequest context) + private static Range getReplaceRange(int replaceStart, int replaceEnd, ICompletionRequest context) throws BadLocationException { int offset = context.getOffset(); if (replaceStart > offset) { @@ -990,4 +894,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..ee0fb5cef3 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CDATASnippetContext.java @@ -0,0 +1,39 @@ +/******************************************************************************* +* 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.DOMElement; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +/** + * CDATA snippet context used to filter the CDATA snippets. + * + */ +public class CDATASnippetContext implements IXMLSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new CDATASnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + if (SnippetContextUtils.canAcceptExpression(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..abf76f9cdf --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/CommentSnippetContext.java @@ -0,0 +1,42 @@ +/******************************************************************************* +* 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; + +/** + * Comment snippet context used to filter the comment snippets. + * + */ +public class CommentSnippetContext extends DTDNodeSnippetContext { + + public static IXMLSnippetContext DEFAULT_CONTEXT = new CommentSnippetContext(); + + @Override + public boolean isMatch(ICompletionRequest request, Map model) { + if (super.isMatch(request, model)) { + // completion was triggered inside a DTD or a DOCTYPE subset + // --> comments are allowed + return true; + } + DOMNode node = request.getNode(); + if (node.isDoctype()) { + // completion was triggered inside doctype declaration, ignore the snippets + return false; + } + return SnippetContextUtils.canAcceptExpression(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..9c8dd6ec08 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/DTDNodeSnippetContext.java @@ -0,0 +1,59 @@ +/******************************************************************************* +* 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.DOMDocumentType; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +/** + * DTD nodes snippet context used to filter the model) { + DOMNode node = request.getNode(); + if (node == null) { + return false; + } + DOMDocument document = node.getOwnerDocument(); + // 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.isComment() || node.isProcessingInstruction() || node.isProlog()) && offset < node.getEnd()) { + // completion was triggered inside comment, 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() || previous.isComment())) { + 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..4093c93a31 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/IXMLSnippetContext.java @@ -0,0 +1,23 @@ +/******************************************************************************* +* 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.commons.snippets.ISnippetContext; +import org.eclipse.lemminx.services.extensions.ICompletionRequest; + +/** + * XML snippet context API. + * + */ +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..fc73c32fba --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/NewFileSnippetContext.java @@ -0,0 +1,61 @@ +/******************************************************************************* +* 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; + } + DOMNode node = request.getNode(); + int offset = request.getOffset(); + if ((node.isComment() || node.isProcessingInstruction() || node.isProlog()) && offset < node.getEnd()) { + // completion was triggered inside comment, xml processing instruction + // --> + return false; + } + // The file contains some contents, the contents allowed are: + // - comments + // - processing instruction + // - text + // - '<', ' model) { + DOMDocument document = request.getXMLDocument(); + DOMNode node = request.getNode(); + int offset = request.getOffset(); + if ((node.isComment() || node.isDoctype()) && offset < node.getEnd()) { + // completion was triggered inside comment, xml processing instruction + // --> + return false; + } + // check if document already defined a xml processing instruction. + if (hasProlog(document, offset)) { + 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) { + return offset <= documentElement.getStart(); + } + return true; + } + + private static boolean hasProlog(DOMDocument document, int offset) { + DOMNode node = document.getFirstChild(); + if (node == null) { + return false; + } + if (node.isProlog()) { + // is offset is the prolog ? + return offset > node.getStart() + " + 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..5829b7378a --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/snippets/XMLSnippetRegistryLoader.java @@ -0,0 +1,39 @@ +/******************************************************************************* +* 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.commons.snippets.ISnippetRegistryLoader; +import org.eclipse.lemminx.commons.snippets.SnippetRegistry; + +/** + * Load default XML snippets. + * + */ +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..ff42820803 --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/cdata-snippets.json @@ -0,0 +1,12 @@ +{ + "Insert CDATA": { + "prefix": [ + "", + "body": [ + "" + ], + "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..e607edf6ab --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/comment-snippets.json @@ -0,0 +1,12 @@ +{ + "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..5512b5477c --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/doctype-snippets.json @@ -0,0 +1,58 @@ +{ + "Insert SYSTEM DOCTYPE": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert SYSTEM DOCTYPE" + }, + "Insert PUBLIC DOCTYPE": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert PUBLIC DOCTYPE" + }, + "Insert SYSTEM DOCTYPE with subset": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert SYSTEM DOCTYPE with subset" + }, + "Insert PUBLIC DOCTYPE with subset": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert PUBLIC DOCTYPE with subset" + }, + "Insert DOCTYPE with subset": { + "prefix": [ + "", + "body": [ + "", + "]>" + ], + "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..69a578dffc --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/dtdnode-snippets.json @@ -0,0 +1,42 @@ +{ + "Insert DTD Element Declaration": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert DTD Element Declaration" + }, + "Insert DTD Attributes List Declaration": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert DTD Attributes List Declaration" + }, + "Insert Internal DTD Entity Declaration": { + "prefix": [ + "", + "body": [ + "" + ], + "description": "Insert Internal DTD Entity Declaration" + }, + "Insert External DTD Entity Declaration": { + "prefix": [ + "", + "body": [ + "" + ], + "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..7cb2bf05a6 --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/newfile-snippets.json @@ -0,0 +1,97 @@ +{ + "New XML with SYSTEM DOCTYPE": { + "prefix": [ + "", + "body": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "New XML with SYSTEM DOCTYPE" + }, + "New XML with PUBLIC DOCTYPE": { + "prefix": [ + "", + "body": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "New XML with PUBLIC DOCTYPE" + }, + "New XML with SYSTEM DOCTYPE with subset": { + "prefix": [ + "", + "body": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "New XML with SYSTEM DOCTYPE with subset" + }, + "New XML with PUBLIC DOCTYPE with subset": { + "prefix": [ + "", + "body": [ + "", + "<${1:root-element}>${0}", + "" + ], + "description": "New XML with PUBLIC DOCTYPE with subset" + }, + "New XML with DOCTYPE with subset": { + "prefix": [ + "", + "body": [ + "", + "]>", + "<${1:root-element}>${0}", + "" + ], + "description": "New XML with DOCTYPE with subset" + }, + "New XML bound with schemaLocation": { + "prefix": [ + "schemaLocation" + ], + "suffix": ">", + "body": [ + "<${1:root-element} xmlns=\"${2:https://github.com/eclipse/lemminx}\"", + "\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", + "\txsi:schemaLocation=\"", + "\t\t${2:https://github.com/eclipse/lemminx} ${3:file}.xsd\">", + "\t${0}", + "" + ], + "description": "New XML bound with schemaLocation" + }, + "New XML bound with noNamespaceShemaLocation": { + "prefix": [ + "noNamespaceSchemaLocation" + ], + "suffix": ">", + "body": [ + "<${1:root-element}", + "\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", + "\txsi:noNamespaceSchemaLocation=\"${2:file}.xsd\">", + "\t${0}", + "" + ], + "description": "New XML bound with noNamespaceSchemaLocation" + } + } \ 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..06722f5525 --- /dev/null +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/services/snippets/prolog-snippets.json @@ -0,0 +1,22 @@ +{ + "Insert xml Processing Instruction": { + "prefix": [ + "", + "body": [ + "${0}" + ], + "description": "Insert xml Processing Instruction" + }, + "Insert xml Processing Instruction with standalone": { + "prefix": [ + "", + "body": [ + "${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 be8f821ca9..e63176e8f9 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 @@ -86,8 +86,24 @@ 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 = 7; + + public static final int PROLOG_SNIPPETS = 2; + + public static final int REGION_SNIPPETS = 2; + private static final String FILE_URI = "test.xml"; + private static final List DUPLICATE_LABELS = Arrays.asList(" labels = list.getItems().stream().map(i -> i.getLabel()).sorted().collect(Collectors.toList()); String previous = null; for (String label : labels) { + if (isIgnoreDuplicateLabel(label)) { + continue; + } assertNotEquals(previous, label, () -> { return "Duplicate label " + label + " in " + labels.stream().collect(Collectors.joining(",")) + "}"; }); @@ -186,18 +205,29 @@ public static void testCompletionFor(XMLLanguageService xmlLanguageService, Stri } } + private static boolean isIgnoreDuplicateLabel(String label) { + return DUPLICATE_LABELS.contains(label); + } + private static void assertCompletion(CompletionList completions, CompletionItem expected, TextDocument document, int offset) { List matches = completions.getItems().stream().filter(completion -> { return expected.getLabel().equals(completion.getLabel()); }).collect(Collectors.toList()); - assertEquals(1, matches.size(), () -> { - return expected.getLabel() + " should only exist once: Actual: " - + completions.getItems().stream().map(c -> c.getLabel()).collect(Collectors.joining(",")); - }); + if (isIgnoreDuplicateLabel(expected.getLabel())) { + assertTrue(matches.size() >= 1, () -> { + return expected.getLabel() + " should only exist once: Actual: " + + completions.getItems().stream().map(c -> c.getLabel()).collect(Collectors.joining(",")); + }); + } else { + assertEquals(1, matches.size(), () -> { + return expected.getLabel() + " should only exist once: Actual: " + + completions.getItems().stream().map(c -> c.getLabel()).collect(Collectors.joining(",")); + }); + } - CompletionItem match = matches.get(0); + CompletionItem match = getCompletionMatch(matches, expected); if (expected.getTextEdit() != null && match.getTextEdit() != null) { if (expected.getTextEdit().getNewText() != null) { assertEquals(expected.getTextEdit().getNewText(), match.getTextEdit().getNewText()); @@ -217,6 +247,15 @@ private static void assertCompletion(CompletionList completions, CompletionItem } + private static CompletionItem getCompletionMatch(List matches, CompletionItem expected) { + for (CompletionItem item : matches) { + if (expected.getTextEdit().getNewText().equals(item.getTextEdit().getNewText())) { + return item; + } + } + return matches.get(0); + } + public static CompletionItem c(String label, TextEdit textEdit, String filterText, String documentation) { return c(label, textEdit, filterText, documentation, null); } 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 ab980a5bdd..aabe4100b6 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(""), // + c("")); 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(""), c("")); xml = "\r\n" + // "\r\n" + // ""; - testCompletionFor(xml, 2, - c("encoding", te(0, 20, 0, 20, "encoding=\"UTF-8\""), "encoding"), - c("standalone", te(0, 20, 0, 20, "standalone=\"yes\""), "standalone")); + testCompletionFor(xml, 2, c("encoding", te(0, 20, 0, 20, "encoding=\"UTF-8\""), "encoding"), + c("standalone", te(0, 20, 0, 20, "standalone=\"yes\""), "standalone")); } @Test @@ -73,7 +72,7 @@ public void noCompletionsAfterStandalone() throws BadLocationException { // completion on | String xml = "\r\n" + // ""; - testCompletionFor(xml, 0, (CompletionItem []) null); + testCompletionFor(xml, 0, (CompletionItem[]) null); } @Test @@ -182,120 +181,181 @@ public void completionEncodingSingleQuotes() throws BadLocationException { @Test public void testAutoCompletionPrologWithXML() throws BadLocationException { // With 'xml' label - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 5), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", true, true, c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 6), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", true, true, c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 7), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); + testCompletionFor("${0}", // + r(0, 0, 0, 5), // + "", true, // + c("${0}", // + r(0, 0, 0, 5), // + "", true, // + c("${0}", // + r(0, 0, 0, 7), // + "", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 2), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>", - r(0, 2, 0, 2), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", true, true, c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 3), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", true, true, c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 4), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); + testCompletionFor("${0}", // + r(0, 0, 0, 2), // + "", // + r(0, 0, 0, 2), // + "", true, // + c("${0}", // + r(0, 0, 0, 2), // + "", true, // + c("${0}", // + r(0, 0, 0, 4), // + "", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 3), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 4), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 3), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", true, true, c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", - r(0, 2, 0, 6), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", true, false, c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>", - r(0, 2, 0, 6), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); + testCompletionFor("${0}", // + r(0, 0, 0, 3), // + "${0}", // + r(0, 0, 0, 4), // + "${0}", // + r(0, 0, 0, 5), // + "${0}", // + r(0, 0, 0, 5), // + "", true, // + c("${0}", // + r(0, 0, 0, 7), // + "", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 5), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", dtdFileURI, true, true, - c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 6), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", dtdFileURI, true, true, - c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 7), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); + testCompletionFor("${0}", // + r(0, 0, 0, 5), // + "", dtdFileURI, true, // + c("${0}", // + r(0, 0, 0, 5), // + "", dtdFileURI, true, // + c("${0}", // + r(0, 0, 0, 7), // + "", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 2), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>", - r(0, 2, 0, 2), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", dtdFileURI, true, true, - c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 3), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", dtdFileURI, true, true, - c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 4), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); + testCompletionFor("${0}", // + r(0, 0, 0, 2), // + "", // + r(0, 0, 0, 2), // + "", dtdFileURI, true, // + c("${0}", // + r(0, 0, 0, 2), // + "", dtdFileURI, true, // + c("${0}", // + r(0, 0, 0, 4), // + "", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 3), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 4), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 3), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", dtdFileURI, true, true, - c("", "xml version=\"1.0\" encoding=\"UTF-8\"?>$0", r(0, 2, 0, 6), - "xml version=\"1.0\" encoding=\"UTF-8\"?>")); - testCompletionFor("", dtdFileURI, true, false, c("", - "xml version=\"1.0\" encoding=\"UTF-8\"?>", r(0, 2, 0, 6), "xml version=\"1.0\" encoding=\"UTF-8\"?>")); + testCompletionFor("${0}", // + r(0, 0, 0, 3), // + "${0}", // + r(0, 0, 0, 4), // + "${0}", // + r(0, 0, 0, 5), // + "${0}", // + r(0, 0, 0, 5), // + "", dtdFileURI, true, // + c("${0}", // + r(0, 0, 0, 7), // + "" + lineSeparator() + // + "" + lineSeparator() + // + "", // + r(0, 0, 0, 0), "", // + r(0, 0, 0, 0), "" + lineSeparator() + // + " " + lineSeparator() + // + "", // + r(0, 0, 0, 0), "schemaLocation"), + c("noNamespaceSchemaLocation", // + "" + lineSeparator() + // + " " + lineSeparator() + // + "", // + r(0, 0, 0, 0), "noNamespaceSchemaLocation"), + c("", // + r(0, 0, 0, 0), "", // + r(0, 0, 0, 1), "", // + r(0, 0, 0, 2), "|", NEWFILE_SNIPPETS /* DOCTYPE snippets */ + // + PROLOG_SNIPPETS /* Prolog snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("" + 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), "|", // + DOCTYPE_SNIPPETS /* DOCTYPE snippets */ + // + PROLOG_SNIPPETS /* Prolog snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("", // + r(0, 8, 0, 8), "\r\n" + // + "\r\n" + // + "|", // + DOCTYPE_SNIPPETS /* DOCTYPE snippets */ + // + PROLOG_SNIPPETS /* Prolog snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("", // + r(2, 0, 2, 0), "", // + r(0, 0, 0, 2), "", 0); + testCompletionFor("", 0); + } + + @Test + public void emptyCompletionInsideDOCTYPE() throws BadLocationException { + testCompletionFor("", 0); + testCompletionFor("", 0); + testCompletionFor("", 0); + } +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLCompletionTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLCompletionTest.java index ee0c58c865..fd06c96ac6 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLCompletionTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLCompletionTest.java @@ -12,6 +12,8 @@ */ package org.eclipse.lemminx.services; +import static org.eclipse.lemminx.XMLAssert.CDATA_SNIPPETS; +import static org.eclipse.lemminx.XMLAssert.COMMENT_SNIPPETS; import static org.eclipse.lemminx.XMLAssert.c; import static org.eclipse.lemminx.XMLAssert.r; import static org.eclipse.lemminx.XMLAssert.testCompletionFor; @@ -108,15 +110,24 @@ public void unneededEndTagCompletion() throws BadLocationException { @Test public void startTagOpenBracket() throws BadLocationException { - testCompletionFor("", 1, c("h", "", "

", 3, c("h", "", "", "", "", 1 + // + CDATA_SNIPPETS /* CDATA snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("h", "", "

", 3 + // + CDATA_SNIPPETS /* CDATA snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("h", "", "", "", "
", 1, c("h", "", "

", 3, c("h", "", "", "

", 3 + // + CDATA_SNIPPETS /* CDATA snippets */ + // + COMMENT_SNIPPETS /* Comment snippets */ , // + c("h", "", "", "", "|", 1 + 2 /* CDATA and Comments */, c("b", "", r(0, 8, 0, 8), "b")); testCompletionFor("<|", 1 + 2 /* CDATA and Comments */, c("b", "", r(0, 8, 0, 9), "|", 1 + 2 /* CDATA and Comments */, c("b", "", r(0, 3, 0, 3), "b")); - testCompletionFor("<|b", 1 + 2 /* CDATA and Comments */, c("b", "", r(0, 3, 0, 5), "<|b", c("b", "", r(0, 3, 0, 5), " ``")); } - @Test - public void testMatch() { - String content = "