Skip to content

Commit

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

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Nov 28, 2023
1 parent def27a7 commit 79784b8
Show file tree
Hide file tree
Showing 16 changed files with 870 additions and 454 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,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:
*
* <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);
}
}

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,112 @@
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) {
boolean isMultiFilePath = separator != null;
Predicate<Character> isStartValidChar2 = isStartValidChar;
int endPathOffset = endNodeOffset;
if (isMultiFilePath) {
// multiple file path
isStartValidChar2 = c -> c != separator && isStartValidChar.test(c);
endPathOffset = StringUtils.findEndWord(content, completionOffset, endNodeOffset, c -> c != separator);
if (endPathOffset == -1) {
endPathOffset = endNodeOffset;
}
}
int startPathOffset = StringUtils.findStartWord(content, completionOffset, startNodeOffset, isStartValidChar2);

int startBaseDirOffset = startNodeOffset;
if (isMultiFilePath && !isStartValidChar.test(content.charAt(startPathOffset - 1))) {
int tmp = StringUtils.findStartWord(content, completionOffset, startNodeOffset, c -> c != separator);
if (tmp != -1) {
startBaseDirOffset = tmp;
}
}

Path baseDir = getBaseDir(content, fileUri, startBaseDirOffset, startPathOffset);
if (baseDir == null || !Files.exists(baseDir)) {
baseDir = null;
}
return new FilePathCompletionResult(startPathOffset, endPathOffset, 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)) {
try {
Path baseDir = FilesUtils.getPath(basePath);
if (Files.exists(baseDir)) {
return baseDir;
}
} catch (Exception e) {

}
}
try {
return FilesUtils.getPath(fileUri).getParent().resolve(basePath);
} catch (Exception e) {
return null;
}
}
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) == '/');
}

}
Loading

0 comments on commit 79784b8

Please sign in to comment.