Skip to content

Commit

Permalink
File completion in attribute value (#410)
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 #345

Signed-off-by: Nikolas Komonen <[email protected]>
  • Loading branch information
NikolasKomonen authored and fbricon committed Jun 10, 2019
1 parent 636bdcf commit b8e5a7a
Show file tree
Hide file tree
Showing 21 changed files with 946 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
*******************************************************************************/
package org.eclipse.lsp4xml.commons;

import static org.eclipse.lsp4xml.utils.OSUtils.isWindows;

import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -19,16 +21,17 @@
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.
* This implementation waits for periods of inactivity to start querying the
* PIDs.
*/
public final class ParentProcessWatcher implements Runnable, Function<MessageConsumer, MessageConsumer>{
public final class ParentProcessWatcher implements Runnable, Function<MessageConsumer, MessageConsumer> {

private static final Logger LOGGER = Logger.getLogger(ParentProcessWatcher.class.getName());
private static final boolean isJava1x = System.getProperty("java.version").startsWith("1.");
Expand All @@ -38,9 +41,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;

private static final long INACTIVITY_DELAY_SECS = 30 *1000;
private static final long INACTIVITY_DELAY_SECS = 30 * 1000;
private static final int POLL_DELAY_SECS = 10;
private volatile long lastActivityTime;
private final ProcessLanguageServer server;
Expand All @@ -50,14 +51,14 @@ public final class ParentProcessWatcher implements Runnable, Function<MessageCon
public interface ProcessLanguageServer extends LanguageServer {

long getParentProcessId();

void exit(int exitCode);
}
public ParentProcessWatcher(ProcessLanguageServer server ) {

public ParentProcessWatcher(ProcessLanguageServer server) {
this.server = server;
service = Executors.newScheduledThreadPool(1);
task = service.scheduleWithFixedDelay(this, POLL_DELAY_SECS, POLL_DELAY_SECS, TimeUnit.SECONDS);
task = service.scheduleWithFixedDelay(this, POLL_DELAY_SECS, POLL_DELAY_SECS, TimeUnit.SECONDS);
}

@Override
Expand All @@ -70,8 +71,8 @@ public void run() {
}

/**
* Checks whether the parent process is still running.
* If not, then we assume it has crashed, and we have to terminate the Java Language Server.
* Checks whether the parent process is still running. If not, then we assume it
* has crashed, and we have to terminate the Java Language Server.
*
* @return true if the parent process is still running
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public class Constants {

public static final Pattern PI_TAG_NAME = Pattern.compile("^[a-zA-Z0-9]+");

//Add comming processing instructions that are defined to have attributes as content
//Add coming processing instructions that are defined to have attributes as content
public static final Pattern PI_WITH_VARIABLES = Pattern.compile("^(xml-stylesheet)[\\s<>?]?");

public static final Pattern DOCTYPE_KIND_OPTIONS = Pattern.compile("^(PUBLIC|SYSTEM)([\\s<>\"'])");
Expand All @@ -91,4 +91,5 @@ public class Constants {

public static final Pattern DOCTYPE_NAME =
Pattern.compile("^[_:\\w][_:\\w-.\\d]*");

}
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,241 @@
/*******************************************************************************
* 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.FilesUtils.convertToWindowsPath;
import static org.eclipse.lsp4xml.utils.FilesUtils.getFilePathSlash;
import static org.eclipse.lsp4xml.utils.FilesUtils.getNormalizedPath;
import static org.eclipse.lsp4xml.utils.OSUtils.isWindows;
import static org.eclipse.lsp4xml.utils.StringUtils.isEmpty;

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

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.dom.DOMDocument;
import org.eclipse.lsp4xml.services.extensions.CompletionParticipantAdapter;
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.FilesUtils;
import org.eclipse.lsp4xml.utils.StringUtils;

/**
* FilePathCompletionParticipant
*/
public class FilePathCompletionParticipant extends CompletionParticipantAdapter {

public static final String FILE_SCHEME = "file";

@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;

String fullAttributeValue = valuePrefix;
if (isEmpty(fullAttributeValue)) {
return;
}

// Get 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 attributePath = text.substring(parsedAttributeStartOffset, completionOffset);

Position startValue = xmlDocument.positionAt(parsedAttributeStartOffset);
Position endValue = xmlDocument.positionAt(completionOffset);
fullRange = new Range(startValue, endValue);

// Try to get the URI string from the attribute value in case it has a file scheme
// header (eg: "file://")
String osSpecificAttributePath = attributePath;
boolean hasFileScheme = false;

hasFileScheme = attributePath.startsWith(FilesUtils.FILE_SCHEME);
if (hasFileScheme) {
osSpecificAttributePath = attributePath.substring(FilesUtils.FILE_SCHEME.length());
}

String slashInAttribute = getFilePathSlash(attributePath);

if (hasFileScheme) {
if (!osSpecificAttributePath.startsWith("/")) {
return; // use of 'file://' and the path was not absolute
}
if (isWindows && osSpecificAttributePath.length() == 1) { // only '/', so list Windows Drives

Range replaceRange = adjustReplaceRange(xmlDocument, fullRange, attributePath, "/");

File[] drives = File.listRoots();
for (File drive : drives) {
createFilePathCompletionItem(drive, replaceRange, response, "/");
}
return;
}
}

if(isWindows) {
osSpecificAttributePath = convertToWindowsPath(osSpecificAttributePath);
}
else if("\\".equals(slashInAttribute)) { // Backslash used in Unix
osSpecificAttributePath = osSpecificAttributePath.replace("\\", "/");
}

// Get the normalized URI string from the parent directory file if necessary
String workingDirectory = null; // The OS specific path for a working directory

if (!hasFileScheme) { //The path from the attribute value is not a uri, so we might need to reference the working directory path
String uriString = xmlDocument.getTextDocument().getUri();
URI uri = new URI(uriString);

if(!FILE_SCHEME.equals(uri.getScheme())) {
return;
}

String uriPathString = uri.getPath();
if(!uriPathString.startsWith("/")) {
return; //file uri is incorrect
}
int lastSlash = uriPathString.lastIndexOf("/");
if(lastSlash > -1) {
workingDirectory = uriPathString.substring(0, lastSlash);

if(isWindows) {
// Necessary, so that this path is readable in Windows
workingDirectory = convertToWindowsPath(workingDirectory);
}
}
}

//Try to get a correctly formatted path from the given values
Path validAttributeValuePath = getNormalizedPath(workingDirectory, osSpecificAttributePath);

if(validAttributeValuePath == null) {
return;
}

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

createNextValidCompletionPaths(validAttributeValuePath, slashInAttribute, 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(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());
return f.isDirectory() ? f.listFiles(filter) : null;
}

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





}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private ServerCapabilitiesConstants() {
public static final String WORKSPACE_CHANGE_FOLDERS_ID = UUID.randomUUID().toString();
public static final String WORKSPACE_WATCHED_FILES_ID = UUID.randomUUID().toString();

public static final CompletionOptions DEFAULT_COMPLETION_OPTIONS = new CompletionOptions(false, Arrays.asList(".", ":", "<", "\"", "=", "/", "?", "\'"));
public static final CompletionOptions DEFAULT_COMPLETION_OPTIONS = new CompletionOptions(false, Arrays.asList(".", ":", "<", "\"", "=", "/", "\\", "?", "\'"));
public static final TextDocumentSyncKind DEFAULT_SYNC_OPTION = TextDocumentSyncKind.Full;
public static final DocumentLinkOptions DEFAULT_LINK_OPTIONS = new DocumentLinkOptions(true);
}
Loading

0 comments on commit b8e5a7a

Please sign in to comment.