Skip to content

Commit

Permalink
XML Files support with settings
Browse files Browse the repository at this point in the history
Fixes #1464

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Nov 27, 2023
1 parent def27a7 commit acdea21
Show file tree
Hide file tree
Showing 14 changed files with 693 additions and 332 deletions.
Original file line number Diff line number Diff line change
@@ -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<FilePathExpression> findFilePathExpression(DOMDocument xmlDocument) {
FilePathsSettings settings = getFilePathsSettings();
if (settings == null) {
return Collections.emptyList();
}

List<FilePaths> filePathsDef = settings.getFilePaths();
if (filePathsDef == null) {
return Collections.emptyList();
}
List<FilePathExpression> expressions = new ArrayList<>();
for (FilePaths filePaths : filePathsDef) {
if (filePaths.matches(xmlDocument.getDocumentURI())) {
expressions.addAll(filePaths.getExpressions());
}
}
return expressions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*******************************************************************************
* 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.FilenameFilter;
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:
*
* <ul>
* <li>attribute value:
*
* <pre>
* &lt;item path="file:///C:/folder" /&gt;
* &lt;item path="file:///C:/folder file:///C:/file.txt" /&gt;
* &lt;item path="/folder" /&gt;
* </pre>
*
* </li>
* <li>DTD DOCTYPE SYSTEM
*
* <pre>
* &lt;!DOCTYPE parent SYSTEM "file.dtd"&gt;
* </pre>
*
* </li>
*
* </ul>
*
* <p>
*
* </p>
*/
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<FilePathExpression> 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<FilePathExpression> 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<Path> 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);
}
// Get IO path from the given value path
/*
* Path validAttributePath = getPath(valuePath, xmlDocument.getDocumentURI());
* if (validAttributePath == null) {
* return;
* }
*
* // Get adjusted range for the completion item (insert at end, or overwrite
* some
* // existing text in the path)
* Range replaceRange = request.getReplaceRange(); //
* ;//adjustReplaceRange(xmlDocument, filePathRange,
* // originalValuePath, slashInAttribute);
* createNextValidCompletionPaths(validAttributePath, "", 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()) {
// Relative path, use the XML file URI folder as base directory.
Path workingDirectoryPath = FilesUtils.getPath(xmlFileUri).getParent();
validAttributePath = workingDirectoryPath.resolve(validAttributePath).normalize();
} else 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;
}
}

/**
* 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<Path> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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<Character> 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 = FilesUtils.getPath(fileUri).getParent();
if (start > startNodeOffset) {
String basePath = content.substring(startNodeOffset, start);
baseDir = baseDir.resolve(basePath);
}
if (!Files.exists(baseDir)) {
baseDir = null;
}
return new FilePathCompletionResult(start, end, baseDir);
}

}
Loading

0 comments on commit acdea21

Please sign in to comment.