From 65723058b91c0229932a1241df63dfb355b0b368 Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 23 Nov 2023 08:40:07 +0100 Subject: [PATCH] XML Files support with settings Fixes #1464 Signed-off-by: azerr --- .../extensions/filepath/FilePathPlugin.java | 101 +++++++ .../FilePathCompletionParticipant.java | 176 +++++++++++ .../FilePathCompletionResult.java | 86 ++++++ .../filepath/settings/FilePathExpression.java | 63 ++++ .../filepath/settings/FilePaths.java | 46 +++ .../filepath/settings/FilePathsSettings.java | 64 ++++ .../extensions/general/FilePathPlugin.java | 40 --- .../FilePathCompletionParticipant.java | 286 ------------------ .../utils/CompletionItemDefaultsUtils.java | 15 +- .../eclipse/lemminx/utils/StringUtils.java | 2 +- .../META-INF/native-image/reflect-config.json | 24 ++ ....lemminx.services.extensions.IXMLExtension | 2 +- .../filepath/FilePathSettingsForTest.java | 55 ++++ .../participants}/FilePathCompletionTest.java | 101 ++++--- 14 files changed, 688 insertions(+), 373 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/FilePathPlugin.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionParticipant.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionResult.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathExpression.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePaths.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathsSettings.java delete mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/FilePathPlugin.java delete mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/completion/FilePathCompletionParticipant.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/FilePathSettingsForTest.java rename org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/{general => filepath/participants}/FilePathCompletionTest.java (75%) diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/FilePathPlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/FilePathPlugin.java new file mode 100644 index 000000000..f670b24ad --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/FilePathPlugin.java @@ -0,0 +1,101 @@ +/******************************************************************************* +* Copyright (c) 2019 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.extensions.filepath; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.colors.settings.XMLColorsSettings; +import org.eclipse.lemminx.extensions.filepath.participants.FilePathCompletionParticipant; +import org.eclipse.lemminx.extensions.filepath.settings.FilePathExpression; +import org.eclipse.lemminx.extensions.filepath.settings.FilePaths; +import org.eclipse.lemminx.extensions.filepath.settings.FilePathsSettings; +import org.eclipse.lemminx.services.extensions.IXMLExtension; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.extensions.save.ISaveContext; +import org.eclipse.lsp4j.InitializeParams; + +/** + * FilePathPlugin + */ +public class FilePathPlugin implements IXMLExtension { + + private final FilePathCompletionParticipant completionParticipant; + private FilePathsSettings filePathsSettings; + + public FilePathPlugin() { + completionParticipant = new FilePathCompletionParticipant(this); + } + + @Override + public void doSave(ISaveContext context) { + if (context.getType() != ISaveContext.SaveContextType.DOCUMENT) { + // Settings + updateSettings(context); + } + } + + private void updateSettings(ISaveContext saveContext) { + Object initializationOptionsSettings = saveContext.getSettings(); + FilePathsSettings settings = FilePathsSettings + .getFilePathsSettings(initializationOptionsSettings); + updateSettings(settings, saveContext); + } + + private void updateSettings(FilePathsSettings settings, ISaveContext context) { + this.filePathsSettings = settings; + } + + @Override + public void start(InitializeParams params, XMLExtensionsRegistry registry) { + registry.registerCompletionParticipant(completionParticipant); + } + + @Override + public void stop(XMLExtensionsRegistry registry) { + registry.unregisterCompletionParticipant(completionParticipant); + } + + public FilePathsSettings getFilePathsSettings() { + return filePathsSettings; + } + + /** + * Return the list of {@link FilePathExpression} for the given document and an + * empty list otherwise. + * + * @param xmlDocument the DOM document + * + * @return the list of {@link FilePathExpression} for the given document and an + * empty list otherwise. + */ + public List findFilePathExpression(DOMDocument xmlDocument) { + FilePathsSettings settings = getFilePathsSettings(); + if (settings == null) { + return Collections.emptyList(); + } + + List filePathsDef = settings.getFilePaths(); + if (filePathsDef == null) { + return Collections.emptyList(); + } + List expressions = new ArrayList<>(); + for (FilePaths filePaths : filePathsDef) { + if (filePaths.matches(xmlDocument.getDocumentURI())) { + expressions.addAll(filePaths.getExpressions()); + } + } + return expressions; + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionParticipant.java new file mode 100644 index 000000000..b01aabdbb --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionParticipant.java @@ -0,0 +1,176 @@ +/******************************************************************************* +* Copyright (c) 2023 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.extensions.filepath.participants; + +import static org.eclipse.lemminx.utils.platform.Platform.isWindows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.dom.DTDDeclParameter; +import org.eclipse.lemminx.extensions.filepath.FilePathPlugin; +import org.eclipse.lemminx.extensions.filepath.settings.FilePathExpression; +import org.eclipse.lemminx.services.extensions.completion.CompletionParticipantAdapter; +import org.eclipse.lemminx.services.extensions.completion.ICompletionRequest; +import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse; +import org.eclipse.lemminx.utils.CompletionSortTextHelper; +import org.eclipse.lemminx.utils.FilesUtils; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +/** + * Extension to support completion for file, folder path in: + * + *
    + *
  • attribute value: + * + *
    + * <item path="file:///C:/folder" />
    + * <item path="file:///C:/folder file:///C:/file.txt" />
    + * <item path="/folder" />
    + * 
    + * + *
  • + *
  • DTD DOCTYPE SYSTEM + * + *
    + * <!DOCTYPE parent SYSTEM "file.dtd">
    + * 
    + * + *
  • + * + *
+ * + *

+ * + *

+ */ +public class FilePathCompletionParticipant extends CompletionParticipantAdapter { + + private static final Logger LOGGER = Logger.getLogger(FilePathCompletionParticipant.class.getName()); + + private final FilePathPlugin filePathPlugin; + + public FilePathCompletionParticipant(FilePathPlugin filePathPlugin) { + this.filePathPlugin = filePathPlugin; + } + + @Override + public void onAttributeValue(String value, ICompletionRequest request, ICompletionResponse response, + CancelChecker cancelChecker) throws Exception { + // File path completion on attribute value + List expressions = filePathPlugin.findFilePathExpression(request.getXMLDocument()); + if (expressions.isEmpty()) { + return; + } + DOMNode node = request.getNode(); + DOMAttr attr = node.findAttrAt(request.getOffset()); + DOMDocument xmlDocument = request.getXMLDocument(); + for (FilePathExpression expression : expressions) { + if (expression.match(attr)) { + DOMRange attrValueRange = attr.getNodeAttrValue(); + addFileCompletionItems(xmlDocument, attrValueRange.getStart() + 1 /* increment to be after the quote */, + attrValueRange.getEnd() - 1, request.getOffset(), expression.getSeparator(), + response); + } + } + } + + @Override + public void onXMLContent(ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker) + throws Exception { + // File path completion on text node + List expressions = filePathPlugin.findFilePathExpression(request.getXMLDocument()); + if (expressions.isEmpty()) { + return; + } + DOMNode node = request.getNode(); + DOMDocument xmlDocument = request.getXMLDocument(); + for (FilePathExpression expression : expressions) { + if (expression.match(node)) { + DOMRange textRange = node; + addFileCompletionItems(xmlDocument, textRange.getStart(), textRange.getEnd(), request.getOffset(), + expression.getSeparator(), + response); + } + } + + } + + @Override + public void onDTDSystemId(String value, ICompletionRequest request, ICompletionResponse response, + CancelChecker cancelChecker) throws Exception { + // File path completion on DTD DOCTYPE SYSTEM + DOMDocument xmlDocument = request.getXMLDocument(); + DTDDeclParameter systemId = xmlDocument.getDoctype().getSystemIdNode(); + addFileCompletionItems(xmlDocument, systemId.getStart() + 1 /* increment to be after the quote */, + systemId.getEnd() - 1, request.getOffset(), null, response); + } + + private static void addFileCompletionItems(DOMDocument xmlDocument, int startOffset, int endOffset, + int completionOffset, + Character separator, ICompletionResponse response) + throws Exception { + FilePathCompletionResult result = FilePathCompletionResult.create(xmlDocument.getText(), + xmlDocument.getDocumentURI(), startOffset, endOffset, completionOffset, separator); + Path baseDir = result.getBaseDir(); + if (baseDir == null) { + return; + } + String slash = ""; + Range replaceRange = XMLPositionUtility.createRange(result.getStart(), result.getEnd(), xmlDocument); + try (DirectoryStream stream = Files.newDirectoryStream(baseDir)) { + for (Path entry : stream) { + createFilePathCompletionItem(entry.toFile(), replaceRange, response, slash); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error while getting files/directories", e); + } + } + + private static void createFilePathCompletionItem(File f, Range replaceRange, ICompletionResponse response, + String slash) { + CompletionItem item = new CompletionItem(); + String fName = FilesUtils.encodePath(f.getName()); + if (isWindows && fName.isEmpty()) { // Edge case for Windows drive letter + fName = f.getPath(); + fName = fName.substring(0, fName.length() - 1); + } + String insertText; + insertText = slash + fName; + item.setLabel(insertText); + + CompletionItemKind kind = f.isDirectory() ? CompletionItemKind.Folder : CompletionItemKind.File; + item.setKind(kind); + + item.setSortText(CompletionSortTextHelper.getSortText(kind)); + item.setFilterText(insertText); + item.setTextEdit(Either.forLeft(new TextEdit(replaceRange, insertText))); + response.addCompletionItem(item); + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionResult.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionResult.java new file mode 100644 index 000000000..727d68380 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionResult.java @@ -0,0 +1,86 @@ +package org.eclipse.lemminx.extensions.filepath.participants; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Predicate; + +import org.eclipse.lemminx.utils.FilesUtils; +import org.eclipse.lemminx.utils.StringUtils; + +public class FilePathCompletionResult { + + private static final Predicate isStartValidChar = (c) -> c != '/' && c != '\\'; + + private final int start; + + private final int end; + + private final Path baseDir; + + public FilePathCompletionResult(int start, int end, Path baseDir) { + super(); + this.start = start; + this.end = end; + this.baseDir = baseDir; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public Path getBaseDir() { + return baseDir; + } + + public static FilePathCompletionResult create(String content, String fileUri, int startNodeOffset, + int endNodeOffset, int completionOffset, Character separator) { + int start = StringUtils.findStartWord(content, completionOffset, startNodeOffset, isStartValidChar); + int end = separator == null ? endNodeOffset + : StringUtils.findEndWord(content, completionOffset, endNodeOffset, c -> c == separator); + Path baseDir = getBaseDir(content, fileUri, startNodeOffset, start); + if (!Files.exists(baseDir)) { + baseDir = null; + } + return new FilePathCompletionResult(start, end, baseDir); + } + + private static Path getBaseDir(String content, String fileUri, int start, int end) { + if (end > start) { + String basePath = content.substring(start, end); + if (!hasPathBeginning(basePath)) { + Path baseDir = FilesUtils.getPath(basePath); + if (Files.exists(baseDir)) { + return baseDir; + } + } + return FilesUtils.getPath(fileUri).getParent().resolve(basePath); + } + return FilesUtils.getPath(fileUri).getParent(); + } + + private static boolean hasPathBeginning(String currentText) { + if (currentText.startsWith("/") + || currentText.startsWith("./") + || currentText.startsWith("../") + || currentText.startsWith("..\\") + || currentText.startsWith(".\\")) { + return true; + } + return isAbsoluteWindowsPath(currentText); + } + + private static boolean isAbsoluteWindowsPath(String currentText) { + if (currentText.length() < 3) { + return false; + } + if (!Character.isLetter(currentText.charAt(0))) { + return false; + } + return currentText.charAt(1) == ':' && (currentText.charAt(2) == '\\' || currentText.charAt(2) == '/'); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathExpression.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathExpression.java new file mode 100644 index 000000000..25f716218 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathExpression.java @@ -0,0 +1,63 @@ +/******************************************************************************* +* Copyright (c) 2023 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.extensions.filepath.settings; + +import org.eclipse.lemminx.xpath.matcher.XPathMatcher; +import org.w3c.dom.Node; + +/** + * File path expression + * + * + * { + "xpath": "@path" + } + * + * + * @author Angelo ZERR + * + */ +public class FilePathExpression { + + private transient XPathMatcher pathMatcher; + + private String xpath; + + private Character separator; + + public String getXPath() { + return xpath; + } + + public Character getSeparator() { + return separator; + } + + public void setXPath(String xpath) { + this.xpath = xpath; + } + + public void setSeparator(Character separator) { + this.separator = separator; + } + + public boolean match(final Node node) { + if (xpath == null) { + return false; + } + if (pathMatcher == null) { + pathMatcher = new XPathMatcher(xpath); + } + return pathMatcher.match(node); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePaths.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePaths.java new file mode 100644 index 000000000..7d567ebab --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePaths.java @@ -0,0 +1,46 @@ +/******************************************************************************* +* Copyright (c) 2023 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.extensions.filepath.settings; + +import java.util.List; + +import org.eclipse.lemminx.settings.PathPatternMatcher; + +/** + * File paths which stores list of {@link FilePathExpression} applied + * for a give pattern. + * + * @author Angelo ZERR + * + */ +public class FilePaths extends PathPatternMatcher { + + private List expressions; + + /** + * Returns list of file path expressions. + * + * @return list of file path expressions. + */ + public List getExpressions() { + return expressions; + } + + /** + * Set list of file path expressions. + * + * @param expressions list of file path expressions. + */ + public void setExpressions(List expressions) { + this.expressions = expressions; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathsSettings.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathsSettings.java new file mode 100644 index 000000000..0d412f871 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/filepath/settings/FilePathsSettings.java @@ -0,0 +1,64 @@ +/******************************************************************************* +* Copyright (c) 2023 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.extensions.filepath.settings; + +import java.util.List; + +import org.eclipse.lemminx.utils.JSONUtility; + +/** + * File paths settings: + * + * + "xml.filepaths": [ + // File paths applied for text node for android colors.xml files + { + "pattern": "*.xml", + "expressions": [ + { + "xpath": "resources/file/text()" + } + ] + }, + // File paths applied for @path attribute for another files + { + "pattern": "*.xml", + "expressions": [ + { + "xpath": "@path" + } + ] + } +] + * + * + * + * @author Angelo ZERR + * + */ +public class FilePathsSettings { + + private List filePaths; + + public List getFilePaths() { + return filePaths; + } + + public void setFilePaths(List filePaths) { + this.filePaths = filePaths; + } + + public static FilePathsSettings getFilePathsSettings(Object initializationOptionsSettings) { + return JSONUtility.toModel(initializationOptionsSettings, FilePathsSettings.class); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/FilePathPlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/FilePathPlugin.java deleted file mode 100644 index 5897c5cc7..000000000 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/FilePathPlugin.java +++ /dev/null @@ -1,40 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2019 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.extensions.general; - -import org.eclipse.lemminx.extensions.general.completion.FilePathCompletionParticipant; -import org.eclipse.lemminx.services.extensions.IXMLExtension; -import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; -import org.eclipse.lsp4j.InitializeParams; - -/** - * FilePathPlugin - */ -public class FilePathPlugin implements IXMLExtension { - - private final FilePathCompletionParticipant completionParticipant; - - public FilePathPlugin() { - completionParticipant = new FilePathCompletionParticipant(); - } - - @Override - public void start(InitializeParams params, XMLExtensionsRegistry registry) { - registry.registerCompletionParticipant(completionParticipant); - } - - @Override - public void stop(XMLExtensionsRegistry registry) { - registry.unregisterCompletionParticipant(completionParticipant); - } - -} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/completion/FilePathCompletionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/completion/FilePathCompletionParticipant.java deleted file mode 100644 index d996c56de..000000000 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/general/completion/FilePathCompletionParticipant.java +++ /dev/null @@ -1,286 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2019 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.extensions.general.completion; - -import static org.eclipse.lemminx.utils.FilesUtils.getFilePathSlash; -import static org.eclipse.lemminx.utils.StringUtils.isEmpty; -import static org.eclipse.lemminx.utils.platform.Platform.isWindows; - -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.eclipse.lemminx.commons.BadLocationException; -import org.eclipse.lemminx.dom.DOMDocument; -import org.eclipse.lemminx.services.extensions.completion.CompletionParticipantAdapter; -import org.eclipse.lemminx.services.extensions.completion.ICompletionRequest; -import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse; -import org.eclipse.lemminx.utils.CompletionSortTextHelper; -import org.eclipse.lemminx.utils.FilesUtils; -import org.eclipse.lemminx.utils.StringUtils; -import org.eclipse.lemminx.utils.XMLPositionUtility; -import org.eclipse.lsp4j.CompletionItem; -import org.eclipse.lsp4j.CompletionItemKind; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.jsonrpc.CancelChecker; -import org.eclipse.lsp4j.jsonrpc.messages.Either; - -/** - * Extension to support completion for file, folder path in: - * - *
    - *
  • attribute value: - * - *
    - * <item path="file:///C:/folder" />
    - * <item path="file:///C:/folder file:///C:/file.txt" />
    - * <item path="/folder" />
    - * 
    - * - *
  • - *
  • DTD DOCTYPE SYSTEM - * - *
    - * <!DOCTYPE parent SYSTEM "file.dtd">
    - * 
    - * - *
  • - * - *
- * - *

- * - *

- */ -public class FilePathCompletionParticipant extends CompletionParticipantAdapter { - - private static final Logger LOGGER = Logger.getLogger(FilePathCompletionParticipant.class.getName()); - - @Override - public void onAttributeValue(String value, ICompletionRequest request, ICompletionResponse response, - CancelChecker cancelChecker) throws Exception { - // File path completion on attribute value - addCompletionItems(value, request, response, false); - } - - @Override - public void onDTDSystemId(String value, ICompletionRequest request, ICompletionResponse response, - CancelChecker cancelChecker) throws Exception { - // File path completion on DTD DOCTYPE SYSTEM - addCompletionItems(value, request, response, true); - } - - private static void addCompletionItems(String value, ICompletionRequest request, ICompletionResponse response, boolean isInDoctype) - throws Exception { - String fullValue = value; - if (isEmpty(fullValue) && !isInDoctype) { - return; - } - - DOMDocument xmlDocument = request.getXMLDocument(); - String text = xmlDocument.getText(); - - // Get value and range for file path declared inside the attribute value - // ex value="file:///C:/fold|er" - int valuePathStartOffset = xmlDocument.offsetAt(request.getReplaceRange().getStart()); - int endPathOffset = request.getOffset(); // offset after the typed character - int startPathOffset = (fullValue.length() == 0 ? 0 : StringUtils.getOffsetAfterWhitespace(fullValue, endPathOffset - valuePathStartOffset)) - + valuePathStartOffset; // first character of URI - Range filePathRange = XMLPositionUtility.createRange(startPathOffset, endPathOffset, xmlDocument); - String originalValuePath = text.substring(startPathOffset, endPathOffset); - // ex: valuePath="file:///C:/fold" - String valuePath = originalValuePath; - String slashInAttribute = getFilePathSlash(valuePath); - - boolean hasFileScheme = valuePath.startsWith(FilesUtils.FILE_SCHEME); - if (hasFileScheme) { - // remove file:// scheme - // ex: valuePath="/C:/fold" - valuePath = FilesUtils.removeFileScheme(valuePath); - if (valuePath.length() == 0 || valuePath.charAt(0) != '/') { - // use of 'file://' and the path was not absolute - return; - } - if (isWindows) { - // For Windows OS, remove the last '/' from file:/// - // ex: valuePath="C:/fold" - valuePath = valuePath.substring(1, valuePath.length()); - if (valuePath.length() == 1) { - // only '/', so list Windows Drives - Range replaceRange = adjustReplaceRange(xmlDocument, filePathRange, originalValuePath, "/"); - File[] drives = File.listRoots(); - for (File drive : drives) { - createFilePathCompletionItem(drive, replaceRange, response, "/"); - } - return; - } - } - } else if (!hasPathBeginning(valuePath) && !isInDoctype) { - // the user probably didn't intend to complete a path - return; - } - // On Linux, Mac OS replace '\\' with '/' - if (!isWindows) { - if ("\\".equals(slashInAttribute)) { // Backslash used in Unix - valuePath = valuePath.replace("\\", "/"); - } - } - - // Get IO path from the given value path - Path validAttributePath = getPath(valuePath, xmlDocument.getTextDocument().getUri()); - if (validAttributePath == null) { - return; - } - - // Get adjusted range for the completion item (insert at end, or overwrite some - // existing text in the path) - Range replaceRange = adjustReplaceRange(xmlDocument, filePathRange, originalValuePath, slashInAttribute); - createNextValidCompletionPaths(validAttributePath, slashInAttribute, replaceRange, response, null); - } - - /** - * Returns the IO Path from the given value path. - * - * @param valuePath the value path - * @param xmlFileUri the XML file URI where completion has been triggered. - * @return the IO Path from the given value path. - */ - private static Path getPath(String valuePath, String xmlFileUri) { - // the value path is the filepath URI without file:// - try { - Path validAttributePath = FilesUtils.getPath(valuePath); - if (!validAttributePath.isAbsolute()) { - // Absolute path, use the XML file URI folder as base dirctory. - Path workingDirectoryPath = FilesUtils.getPath(xmlFileUri).getParent(); - validAttributePath = workingDirectoryPath.resolve(validAttributePath).normalize(); - } - if (!".".equals(valuePath) && !valuePath.endsWith("/") && !valuePath.endsWith("\\")) { - // ex : C:/folder|/ -> in this case the path is the folder parent (C:) - validAttributePath = validAttributePath.getParent(); - } - return Files.exists(validAttributePath) ? validAttributePath : null; - } catch (Exception e) { - return null; - } - } - - /** - * Returns a Range that covers trailing content after a slash, or if it already - * ends with a slash then a Range right after it. - * - * @param xmlDocument - * @param fullRange - * @param attributeValue - * @param slash - * @return - */ - private static Range adjustReplaceRange(DOMDocument xmlDocument, Range fullRange, String attributeValue, - String slash) { - // In the case the currently typed file/directory needs to be overwritten - Position replaceStart = null; - Position currentEnd = fullRange.getEnd(); - - int startOffset; - try { - startOffset = xmlDocument.offsetAt(fullRange.getStart()); - } catch (BadLocationException e) { - return null; - } - int lastSlashIndex = attributeValue.lastIndexOf(slash); - if (lastSlashIndex > -1) { - try { - replaceStart = xmlDocument.positionAt(startOffset + lastSlashIndex); - } catch (BadLocationException e) { - return null; - } - } - Range replaceRange = new Range(); - if (replaceStart != null) { - replaceRange.setStart(replaceStart); - } else { - replaceRange.setStart(currentEnd); - } - replaceRange.setEnd(currentEnd); - return replaceRange; - } - - /** - * Creates the completion items based off the given absolute path - * - * @param pathToAttributeDirectory - * @param attributePath - * @param replaceRange - * @param response - * @param filter - */ - private static void createNextValidCompletionPaths(Path pathToAttributeDirectory, String slash, Range replaceRange, - ICompletionResponse response, FilenameFilter filter) { - try (DirectoryStream stream = Files.newDirectoryStream(pathToAttributeDirectory)) { - for (Path entry : stream) { - createFilePathCompletionItem(entry.toFile(), replaceRange, response, slash); - } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error while getting files/directories", e); - } - } - - private static void createFilePathCompletionItem(File f, Range replaceRange, ICompletionResponse response, - String slash) { - CompletionItem item = new CompletionItem(); - String fName = FilesUtils.encodePath(f.getName()); - if (isWindows && fName.isEmpty()) { // Edge case for Windows drive letter - fName = f.getPath(); - fName = fName.substring(0, fName.length() - 1); - } - String insertText; - insertText = slash + fName; - item.setLabel(insertText); - - CompletionItemKind kind = f.isDirectory() ? CompletionItemKind.Folder : CompletionItemKind.File; - item.setKind(kind); - - item.setSortText(CompletionSortTextHelper.getSortText(kind)); - item.setFilterText(insertText); - item.setTextEdit(Either.forLeft(new TextEdit(replaceRange, insertText))); - response.addCompletionItem(item); - } - - private static boolean hasPathBeginning(String currentText) { - if (currentText.startsWith("/") - || currentText.startsWith("./") - || currentText.startsWith("../") - || currentText.startsWith("..\\") - || currentText.startsWith(".\\")) { - return true; - } - return isAbsoluteWindowsPath(currentText); - } - - private static boolean isAbsoluteWindowsPath(String currentText) { - if (currentText.length() < 3) { - return false; - } - if (!Character.isLetter(currentText.charAt(0))) { - return false; - } - return currentText.charAt(1) == ':' && (currentText.charAt(2) == '\\' || currentText.charAt(2) == '/'); - } - -} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/CompletionItemDefaultsUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/CompletionItemDefaultsUtils.java index 9e6d0214e..25916ba64 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/CompletionItemDefaultsUtils.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/CompletionItemDefaultsUtils.java @@ -62,7 +62,10 @@ public static void process(CompletionResponse completionResponse, SharedSettings */ private static void setToMostCommonEditRange(List completionList, CompletionItemDefaults itemDefaults) { - Map> itemsByRange = completionList.stream() + Map> itemsByRange = completionList + .stream() + .filter(item -> item.getTextEdit() != null && item.getTextEdit().isLeft() + && item.getTextEdit().getLeft() != null) .collect(Collectors.groupingBy(item -> item.getTextEdit().getLeft().getRange())); int maxCount = 0; Range mostCommonRange = null; @@ -73,12 +76,17 @@ private static void setToMostCommonEditRange(List completionList mostCommonRange = entry.getKey(); } } + if (mostCommonRange == null) { + return; + } itemsByRange.get(mostCommonRange).forEach(item -> { item.setTextEditText(item.getTextEdit().getLeft().getNewText()); item.setTextEdit(null); }); itemDefaults.setEditRange(Either.forLeft(mostCommonRange)); - completionList = itemsByRange.values().stream().flatMap(Collection::stream) + completionList = itemsByRange.values() + .stream() + .flatMap(Collection::stream) .collect(Collectors.toList()); } @@ -91,7 +99,8 @@ private static void setToMostCommonEditRange(List completionList */ private static void setToMostCommonInsertTextFormat(List completionList, CompletionItemDefaults itemDefaults) { - Map> itemsByInsertTextFormat = completionList.stream() + Map> itemsByInsertTextFormat = completionList + .stream() .filter(item -> item.getInsertTextFormat() != null) .collect(Collectors.groupingBy(item -> item.getInsertTextFormat())); int maxCount = 0; 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 8562db439..fcac579e2 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 @@ -438,7 +438,7 @@ public static int findStartWord(String text, int offset, Predicate is * to the given min and -1 if no word. */ public static int findStartWord(String text, int offset, int min, Predicate isValidChar) { - if (offset < 0 || offset >= text.length() || !isValidChar.test(text.charAt(offset))) { + if (offset < 0 || offset >= text.length() /*|| !isValidChar.test(text.charAt(offset))*/) { return -1; } for (int i = offset - 1; i >= min; i--) { diff --git a/org.eclipse.lemminx/src/main/resources/META-INF/native-image/reflect-config.json b/org.eclipse.lemminx/src/main/resources/META-INF/native-image/reflect-config.json index daecd3a5a..facf2b564 100644 --- a/org.eclipse.lemminx/src/main/resources/META-INF/native-image/reflect-config.json +++ b/org.eclipse.lemminx/src/main/resources/META-INF/native-image/reflect-config.json @@ -481,6 +481,30 @@ "parameterTypes": [] }] }, + { + "name": "org.eclipse.lemminx.extensions.filepath.settings.FilePathExpression", + "allDeclaredFields": true, + "methods": [{ + "name": "", + "parameterTypes": [] + }] + }, + { + "name": "org.eclipse.lemminx.extensions.filepath.settings.FilePaths", + "allDeclaredFields": true, + "methods": [{ + "name": "", + "parameterTypes": [] + }] + }, + { + "name": "org.eclipse.lemminx.extensions.filepath.settings.FilePathsSettings", + "allDeclaredFields": true, + "methods": [{ + "name": "", + "parameterTypes": [] + }] + }, { "name": "org.eclipse.lemminx.settings.QuoteStyle", "allDeclaredFields": true diff --git a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension index e23229121..abdc794ec 100644 --- a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension +++ b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension @@ -6,7 +6,7 @@ org.eclipse.lemminx.extensions.xsl.XSLPlugin org.eclipse.lemminx.extensions.catalog.XMLCatalogPlugin org.eclipse.lemminx.extensions.xsi.XSISchemaPlugin org.eclipse.lemminx.extensions.prolog.PrologPlugin -org.eclipse.lemminx.extensions.general.FilePathPlugin +org.eclipse.lemminx.extensions.filepath.FilePathPlugin org.eclipse.lemminx.extensions.entities.EntitiesPlugin org.eclipse.lemminx.extensions.xmlmodel.XMLModelPlugin org.eclipse.lemminx.extensions.generators.FileContentGeneratorPlugin diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/FilePathSettingsForTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/FilePathSettingsForTest.java new file mode 100644 index 000000000..a7e285715 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/FilePathSettingsForTest.java @@ -0,0 +1,55 @@ +/******************************************************************************* +* Copyright (c) 2023 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.extensions.filepath; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.lemminx.extensions.filepath.settings.FilePathExpression; +import org.eclipse.lemminx.extensions.filepath.settings.FilePaths; +import org.eclipse.lemminx.extensions.filepath.settings.FilePathsSettings; + +/** + * {@link FilePathsSettings} for tests. + * + */ +public class FilePathSettingsForTest { + + public static FilePathsSettings createFilePathsSettings() { + FilePathsSettings filePathsSettings = new FilePathsSettings(); + filePathsSettings.setFilePaths(createFilePaths()); + return filePathsSettings; + } + + private static List createFilePaths() { + List filePaths = new ArrayList<>(); + + FilePaths path = new FilePaths(); + path.setPattern("**/*.xml"); + filePaths.add(path); + /* + * { + * "xpath": "@path" + * } + */ + FilePathExpression attrPath = new FilePathExpression(); + attrPath.setXPath("@path"); + FilePathExpression textPath = new FilePathExpression(); + textPath.setXPath("path/text()"); + + path.setExpressions(Arrays.asList(attrPath, textPath)); + + return filePaths; + } + +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/general/FilePathCompletionTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionTest.java similarity index 75% rename from org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/general/FilePathCompletionTest.java rename to org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionTest.java index 749586bc7..4f19e72a8 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/general/FilePathCompletionTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/filepath/participants/FilePathCompletionTest.java @@ -9,7 +9,7 @@ * Contributors: * Red Hat Inc. - initial API and implementation *******************************************************************************/ -package org.eclipse.lemminx.extensions.general; +package org.eclipse.lemminx.extensions.filepath.participants; import static org.eclipse.lemminx.XMLAssert.c; import static org.eclipse.lemminx.XMLAssert.te; @@ -17,7 +17,10 @@ import org.eclipse.lemminx.AbstractCacheBasedTest; import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.extensions.filepath.FilePathSettingsForTest; +import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lemminx.utils.FilesUtils; import org.eclipse.lemminx.utils.platform.Platform; import org.eclipse.lsp4j.CompletionItem; @@ -36,31 +39,38 @@ public class FilePathCompletionTest extends AbstractCacheBasedTest { private static final String userDirBackSlash = userDir.replace("/", "\\"); private static final String userDirForwardSlash = userDir.replace("\\", "/"); + @Test + public void testFilePathCompletionWithEmptyValue() throws BadLocationException { + String xml = ""; + CompletionItem[] items = getCompletionItemList(0, 9, 9, "folderA", "folderB", "NestedA"); + testCompletionFor(xml, items); + } + @Test public void testFilePathCompletion() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 10, 11, "folderA", "folderB", "NestedA"); + CompletionItem[] items = getCompletionItemList(0, 11, 11, "folderA", "folderB", "NestedA"); testCompletionFor(xml, items); } @Test public void testFilePathCompletionBackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 10, 11, "folderA", "folderB", "NestedA"); + CompletionItem[] items = getCompletionItemList(0, 11, 11, "folderA", "folderB", "NestedA"); testCompletionFor(xml, items); } @Test public void testFilePathCompletionFolderA() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 18, 19, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, items); } @Test public void testFilePathCompletionFolderABackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 18, 19, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, items); } @@ -79,9 +89,8 @@ public void testFilePathCompletionFolderBBackSlash() throws BadLocationException @Test public void testFilePathCompletionFolderBAbsolutePath() throws BadLocationException { String filePath = userDirForwardSlash + "/src/test/resources/filePathCompletion/folderB/"; // C:/.../src/test... - int filePathLength = filePath.length(); String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 9 + filePathLength - 1, 9 + filePathLength, "xsdB1.xsd", + CompletionItem[] items = getCompletionItemList(0, 102, 102, "xsdB1.xsd", "xmlB1.xml"); testCompletionFor(xml, 2, items); } @@ -92,10 +101,8 @@ public void testFilePathCompletionFolderBAbsolutePathBackSlash() throws BadLocat return; } String filePath = userDirBackSlash + "\\src\\test\\resources\\filePathCompletion\\folderB\\"; // C:\...\src\test... - int filePathLength = filePath.length(); String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 9 + filePathLength - 1, 9 + filePathLength, "xsdB1.xsd", - "xmlB1.xml"); + CompletionItem[] items = getCompletionItemList(0, 102, 102, "xsdB1.xsd", "xmlB1.xml"); testCompletionFor(xml, 2, items); } @@ -103,9 +110,8 @@ public void testFilePathCompletionFolderBAbsolutePathBackSlash() throws BadLocat public void testFilePathCompletionFolderBAbsolutePathWithFileScheme() throws BadLocationException { String filePath = (isWindows ? FilesUtils.FILE_SCHEME + "/" : FilesUtils.FILE_SCHEME) + userDirForwardSlash + "/src/test/resources/filePathCompletion/folderB/"; - int filePathLength = filePath.length(); String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 9 + filePathLength - 1, 9 + filePathLength, "xsdB1.xsd", + CompletionItem[] items = getCompletionItemList(0, 110, 110, "xsdB1.xsd", "xmlB1.xml"); testCompletionFor(xml, 2, items); } @@ -113,28 +119,28 @@ public void testFilePathCompletionFolderBAbsolutePathWithFileScheme() throws Bad @Test public void testFilePathCompletionNestedA() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 18, 19, "NestedB"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "NestedB"); testCompletionFor(xml, 1, items); } @Test public void testFilePathCompletionNestedABackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 18, 19, "NestedB"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "NestedB"); testCompletionFor(xml, 1, items); } @Test public void testFilePathCompletionNestedBIncomplete() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 26, 30, "nestedXSD.xsd"); + CompletionItem[] items = getCompletionItemList(0, 27, 30, "nestedXSD.xsd"); testCompletionFor(xml, 1, items); } @Test public void testFilePathCompletionNestedBIncompleteBackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 26, 30, "nestedXSD.xsd"); + CompletionItem[] items = getCompletionItemList(0, 27, 30, "nestedXSD.xsd"); testCompletionFor(xml, 1, items); } @@ -150,16 +156,6 @@ public void testFilePathCompletionExtraTextInValueBackSlash() throws BadLocation testCompletionFor(xml, 0); } - @Test - public void testFilePathCompletionExtraTextInValueAbsolute() throws BadLocationException { - String filePath = userDirForwardSlash + "/src/test/resources/filePathCompletion/NestedA/NestedB/"; - int filePathLength = filePath.length(); - String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 29 + filePathLength - 1, 29 + filePathLength, - "nestedXSD.xsd"); - testCompletionFor(xml, 1, items); - } - @Test public void testFilePathCompletionExtraTextInValueAbsoluteBackSlash() throws BadLocationException { String filePath = userDirBackSlash + "\\src\\test\\resources\\filePathCompletion\\NestedA\\NestedB\\"; @@ -182,28 +178,28 @@ public void testFilePathCompletionBadFolderBackSlash() throws BadLocationExcepti @Test public void testFilePathCompletionStartWithDotDot() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 38, 39, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 39, 39, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, 2, items); } @Test public void testFilePathCompletionStartWithDotDotBackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 38, 39, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 39, 39, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, 2, items); } @Test public void testFilePathCompletionStartWithDot() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 18, 19, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, 2, items); } @Test public void testFilePathCompletionStartWithDotBackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 18, 19, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, 2, items); } @@ -228,43 +224,50 @@ public void testFilePathCompletionNotValue() throws BadLocationException { @Test public void testFilePathCompletionDTD() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 23, 24, "folderA", "folderB", "NestedA"); + CompletionItem[] items = getCompletionItemList(0, 24, 24, "folderA", "folderB", "NestedA"); testCompletionFor(xml, items); } + @Test + public void testFilePathCompletionDTDFollowingBySlash() throws BadLocationException { + String xml = ""; + CompletionItem[] items = getCompletionItemList(0, 24, 25, "folderA", "folderB", "NestedA"); + testCompletionFor(xml, items); + } + @Test public void testFilePathCompletionDTDBackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 23, 24, "folderA", "folderB", "NestedA"); + CompletionItem[] items = getCompletionItemList(0, 24, 24, "folderA", "folderB", "NestedA"); testCompletionFor(xml, items); } @Test public void testFilePathCompletionDTDFolderA() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 31, 32, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 32, 32, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, items); } @Test public void testFilePathCompletionDTDFolderABackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 31, 32, "xsdA1.xsd", "xsdA2.xsd"); + CompletionItem[] items = getCompletionItemList(0, 32, 32, "xsdA1.xsd", "xsdA2.xsd"); testCompletionFor(xml, items); } @Test public void testFilePathCompletionDTDFolderB() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 31, 32, "xsdB1.xsd", "xmlB1.xml"); + CompletionItem[] items = getCompletionItemList(0, 32, 32, "xsdB1.xsd", "xmlB1.xml"); testCompletionFor(xml, 2, items); } @Test public void testFilePathCompletionDTDFolderBBackSlash() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("\\", 0, 31, 32, "xsdB1.xsd", "xmlB1.xml"); + CompletionItem[] items = getCompletionItemList(0, 32, 32, "xsdB1.xsd", "xmlB1.xml"); testCompletionFor(xml, 2, items); } @@ -283,14 +286,14 @@ public void testFilePathNoCompletionMissingSystemId() throws BadLocationExceptio @Test public void testFilePathCompletionWithSpacesFolder() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 18, 19, "a@b", "with%20spaces"); + CompletionItem[] items = getCompletionItemList(0, 19, 19, "a@b", "with%20spaces"); testCompletionFor(xml, 2, items); } @Test public void testFilePathCompletionInsideSpecialChars() throws BadLocationException { String xml = ""; - CompletionItem[] items = getCompletionItemList("/", 0, 11, 12, "a@b", "with%20spaces"); + CompletionItem[] items = getCompletionItemList(0, 12, 12, "a@b", "with%20spaces"); String fileURI = "file://" + userDirForwardSlash + "/src/test/resources/filePathCompletion/folderC/a@b/foo.xml"; XMLAssert.testCompletionFor(xml, null, fileURI, 2, items); } @@ -307,6 +310,18 @@ public void testFilePathCompletionWithBrokenAbsoluteWindowsPath() throws BadLoca testCompletionFor(xml, 0); } + // Test with multiple file path + + @Test + public void testFilePathCompletionExtraTextInValueAbsolute() throws BadLocationException { + String filePath = userDirForwardSlash + "/src/test/resources/filePathCompletion/NestedA/NestedB/"; + int filePathLength = filePath.length(); + String xml = ""; + CompletionItem[] items = getCompletionItemList(0, 29 + filePathLength - 1, 29 + filePathLength, + "nestedXSD.xsd"); + testCompletionFor(xml, 1, items); + } + private static void testCompletionFor(String xml, CompletionItem... expectedItems) throws BadLocationException { testCompletionFor(xml, null, expectedItems); } @@ -314,17 +329,19 @@ private static void testCompletionFor(String xml, CompletionItem... expectedItem private static void testCompletionFor(String xml, Integer expectedItemCount, CompletionItem... expectedItems) throws BadLocationException { String fileURI = "file://" + userDirForwardSlash + "/src/test/resources/filePathCompletion/main.xml"; - XMLAssert.testCompletionFor(xml, null, fileURI, expectedItemCount, expectedItems); + XMLAssert.testCompletionFor(new XMLLanguageService(), xml, null, ls -> { + ls.doSave(new SettingsSaveContext(FilePathSettingsForTest.createFilePathsSettings())); + + }, fileURI, null, true, expectedItems); } - private static CompletionItem[] getCompletionItemList(String slash, int line, int startChar, int endChar, + private static CompletionItem[] getCompletionItemList(int line, int startChar, int endChar, String... fileOrFolderNames) { - String s = slash; int fOfSize = fileOrFolderNames.length; CompletionItem[] items = new CompletionItem[fOfSize]; for (int i = 0; i < fOfSize; i++) { - String fOf = s + fileOrFolderNames[i]; + String fOf = fileOrFolderNames[i]; items[i] = c(fOf, te(line, startChar, line, endChar, fOf), fOf); }