Skip to content

Commit

Permalink
File completion in attribute value
Browse files Browse the repository at this point in the history
Provides file completion when the user starts typing a path in an attribute value

Displays all folders and files as of now.

Fixes eclipse-lemminx#345

Signed-off-by: Nikolas Komonen <[email protected]>
  • Loading branch information
NikolasKomonen committed Jun 4, 2019
1 parent f58872a commit 185843d
Show file tree
Hide file tree
Showing 13 changed files with 622 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.common.io.Closeables;

import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.services.LanguageServer;

import com.google.common.io.Closeables;

/**
* Watches the parent process PID and invokes exit if it is no longer available.
* This implementation waits for periods of inactivity to start querying the PIDs.
Expand All @@ -38,7 +38,7 @@ public final class ParentProcessWatcher implements Runnable, Function<MessageCon
*/
private static final int FORCED_EXIT_CODE = 1;

private static final boolean isWindows = System.getProperty("os.name").toLowerCase().indexOf("win") >= 0;
public static final boolean isWindows = System.getProperty("os.name").toLowerCase().indexOf("win") >= 0;

private static final long INACTIVITY_DELAY_SECS = 30 *1000;
private static final int POLL_DELAY_SECS = 10;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*******************************************************************************
* 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
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/

package org.eclipse.lsp4xml.extensions.general;

import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4xml.extensions.general.completion.FilePathCompletionParticipant;
import org.eclipse.lsp4xml.services.extensions.IXMLExtension;
import org.eclipse.lsp4xml.services.extensions.XMLExtensionsRegistry;
import org.eclipse.lsp4xml.services.extensions.save.ISaveContext;

/**
* 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);
}

@Override
public void doSave(ISaveContext context) {

}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*******************************************************************************
* 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
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/

package org.eclipse.lsp4xml.extensions.general.completion;

import static org.eclipse.lsp4xml.utils.StringUtils.getNormalizedPath;

import java.io.File;
import java.io.FilenameFilter;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;

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.lsp4xml.commons.BadLocationException;
import org.eclipse.lsp4xml.commons.ParentProcessWatcher;
import org.eclipse.lsp4xml.dom.DOMDocument;
import org.eclipse.lsp4xml.services.extensions.ICompletionParticipant;
import org.eclipse.lsp4xml.services.extensions.ICompletionRequest;
import org.eclipse.lsp4xml.services.extensions.ICompletionResponse;
import org.eclipse.lsp4xml.settings.SharedSettings;
import org.eclipse.lsp4xml.utils.CompletionSortTextHelper;
import org.eclipse.lsp4xml.utils.StringUtils;

/**
* FilePathCompletionParticipant
*/
public class FilePathCompletionParticipant implements ICompletionParticipant {

public static final String FILE_SCHEME = "file";

private static final FilenameFilter xsdFilter = new FilenameFilter() {

@Override
public boolean accept(File dir, String name) {
if(name.matches(".+\\.[^\\s]+$") == false) {
return true;
}
return name.toLowerCase().endsWith(".xsd");
}
};

@Override
public void onTagOpen(ICompletionRequest completionRequest, ICompletionResponse completionResponse) throws Exception {

}

@Override
public void onXMLContent(ICompletionRequest request, ICompletionResponse response) throws Exception {

}

@Override
public void onAttributeName(boolean generateValue, Range fullRange, ICompletionRequest request,
ICompletionResponse response, SharedSettings settings) throws Exception {

}

@Override
public void onAttributeValue(String valuePrefix, Range fullRange, boolean addQuotes, ICompletionRequest request,
ICompletionResponse response, SharedSettings settings) throws Exception {

DOMDocument xmlDocument = request.getXMLDocument();
String text = xmlDocument.getText();

//Get full attribute value range
// +- 1 since it includes the quotations
int documentStartOffset = xmlDocument.offsetAt(fullRange.getStart()) + 1;
int documentEndOffset = xmlDocument.offsetAt(fullRange.getEnd()) - 1;
String fullAttributeValue = text.substring(documentStartOffset, documentEndOffset);


//Get uri value and range from fullAttributeValue
int completionOffset = request.getOffset(); //offset after the typed character
int parsedAttributeStartOffset = StringUtils.getOffsetAfterWhitespace(fullAttributeValue, completionOffset - documentStartOffset) + documentStartOffset; //first character of URI
String attributeValue = text.substring(parsedAttributeStartOffset, completionOffset); //sub value of fullAttributeValue
Position startValue = xmlDocument.positionAt(parsedAttributeStartOffset);
Position endValue = xmlDocument.positionAt(completionOffset);
fullRange = new Range(startValue, endValue);


//Get the URI string from the attribute value in case it has a file scheme header (eg: "file://")
String tempCurrVal;
String fileScheme = null;
try {
URI attributeValueURI = URI.create(attributeValue);
tempCurrVal = attributeValueURI.getPath();
fileScheme = attributeValueURI.getScheme();
} catch (IllegalArgumentException e) {
tempCurrVal = attributeValue;
}
if(FILE_SCHEME.equals(fileScheme) && !(tempCurrVal.startsWith("\\") || tempCurrVal.startsWith("/"))) {
return; //use of 'file://' and the path was not absolute
}

String slash = StringUtils.getFilePathSlash(attributeValue);

//Get the normalized URI string from the parent directory file
String uriString = xmlDocument.getTextDocument().getUri();
String directoryOfFile = Paths.get(uriString).getParent().toString();
URI uri = URI.create(directoryOfFile);
String scheme = uri.getScheme();
if(!(scheme == null || FILE_SCHEME.equals(scheme))) {
return;
}
directoryOfFile = uri.getPath();
if(ParentProcessWatcher.isWindows) {
directoryOfFile = directoryOfFile.replace("/", "\\");
}

//Try to get a proper path from the given values
Path validAttributeValuePath = getNormalizedPath(directoryOfFile, tempCurrVal);

if(validAttributeValuePath == null) {
return;
}

File f = new File(validAttributeValuePath.toString());
if(f.isFile()) {
return;
}



//Get adjusted range for the completion item (insert at end, or overwrite some existing text in the path)
Range replaceRange = adjustReplaceRange(xmlDocument, fullRange, attributeValue, slash);

createNextValidCompletionPaths(validAttributeValuePath, slash, replaceRange, response, 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 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 void createNextValidCompletionPaths(Path pathToAttributeDirectory, String slash, Range replaceRange, ICompletionResponse response,
FilenameFilter filter) {

File[] proposedFiles = gatherFiles(pathToAttributeDirectory, filter);
if (proposedFiles != null) {
for (File child : proposedFiles) {
if (child != null) {
createFilePathCompletionItem(pathToAttributeDirectory, child, replaceRange, response, slash);
}
}
}
}

/**
* Returns a list of File objects that are in the given directory
* @param pathOfDirectory
* @param filter
* @return
*/
private File[] gatherFiles(Path pathOfDirectory, FilenameFilter filter) {

File f = new File(pathOfDirectory.toString());
if(f.isDirectory()) {
return f.listFiles(filter);
}
return null;
}

private void createFilePathCompletionItem(Path pathOfDirectory, File f, Range replaceRange, ICompletionResponse response, String slash) {
CompletionItem item = new CompletionItem();
String fName = f.getName();
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(new TextEdit(replaceRange, insertText));
response.addCompletionItem(item);
}





}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*******************************************************************************
* 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
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/

package org.eclipse.lsp4xml.utils;

import org.eclipse.lsp4j.CompletionItemKind;

/**
* CompletionSortText class to get sortText for CompletionItem's
*/
public class CompletionSortTextHelper {

private int i;
private final String base;

public CompletionSortTextHelper(CompletionItemKind kind) {
this.base = getSortText(kind);
i = 0;
}

public String next() {
i++;
return base + Integer.toString(i);
}

public String next(CompletionItemKind kind) {
String tempBase = getSortText(kind);
i++;
return tempBase + Integer.toString(i);
}

public static String getSortText(CompletionItemKind kind) {
switch (kind) {
case Variable:
case Property: // DOMElement
case Enum:
case EnumMember:
case Value:
return "aa";
case File:
return "ab";
case Folder:
return "ac";
default:
return "zz";
}
}

}
Loading

0 comments on commit 185843d

Please sign in to comment.