blob: a14de5a624d77330f67b9c4c0b2f167aed73e335 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial implementation
* Michał Niewrzał (Rogue Wave Software Inc.)
* Lucas Bullen (Red Hat Inc.) - Get IDocument from IEditorInput
* Angelo Zerr <angelo.zerr@gmail.com> - Bug 525400 - [rename] improve rename support with ltk UI
*******************************************************************************/
package org.eclipse.lsp4e;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.RewriteSessionEditProcessor;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.DocumentChange;
import org.eclipse.ltk.core.refactoring.PerformChangeOperation;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.undo.DocumentUndoManagerRegistry;
import org.eclipse.text.undo.IDocumentUndoManager;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.part.MultiPageEditorPart;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.ITextEditor;
import com.google.gson.Gson;
/**
* Some utility methods to convert between Eclipse and LS-API types
*/
public class LSPEclipseUtils {
private LSPEclipseUtils() {
// this class shouldn't be instantiated
}
public static ITextEditor getActiveTextEditor() {
IEditorPart editorPart = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
if(editorPart instanceof ITextEditor) {
return (ITextEditor) editorPart;
} else if (editorPart instanceof MultiPageEditorPart) {
MultiPageEditorPart multiPageEditorPart = (MultiPageEditorPart) editorPart;
Object page = multiPageEditorPart.getSelectedPage();
if (page instanceof ITextEditor) {
return (ITextEditor) page;
}
}
return null;
}
public static Position toPosition(int offset, IDocument document) throws BadLocationException {
Position res = new Position();
res.setLine(document.getLineOfOffset(offset));
res.setCharacter(offset - document.getLineInformationOfOffset(offset).getOffset());
return res;
}
public static int toOffset(Position position, IDocument document) throws BadLocationException {
return document.getLineInformation(position.getLine()).getOffset() + position.getCharacter();
}
public static TextDocumentPositionParams toTextDocumentPosistionParams(URI fileUri, int offset, IDocument document)
throws BadLocationException {
Position start = toPosition(offset, document);
TextDocumentPositionParams param = new TextDocumentPositionParams();
param.setPosition(start);
param.setUri(fileUri.toString());
TextDocumentIdentifier id = new TextDocumentIdentifier();
id.setUri(fileUri.toString());
param.setTextDocument(id);
return param;
}
public static int toEclipseMarkerSeverity(DiagnosticSeverity lspSeverity) {
if (lspSeverity == null) {
// if severity is empty it is up to the client to interpret diagnostics
return IMarker.SEVERITY_ERROR;
}
switch (lspSeverity) {
case Error:
return IMarker.SEVERITY_ERROR;
case Warning:
return IMarker.SEVERITY_WARNING;
default:
return IMarker.SEVERITY_INFO;
}
}
@Nullable
public static IResource findResourceFor(@Nullable String uri) {
if (uri == null || uri.isEmpty()) {
return null;
}
String convertedUri = uri.replace("file:///", "file:/"); //$NON-NLS-1$//$NON-NLS-2$
convertedUri = convertedUri.replace("file://", "file:/"); //$NON-NLS-1$//$NON-NLS-2$
IPath path = Path.fromOSString(new File(URI.create(convertedUri)).getAbsolutePath());
IProject project = null;
for (IProject aProject : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
IPath location = aProject.getLocation();
if (location != null && location.isPrefixOf(path)
&& (project == null || project.getLocation().segmentCount() < location.segmentCount())) {
project = aProject;
}
}
if (project == null) {
return null;
}
IPath projectRelativePath = path.removeFirstSegments(project.getLocation().segmentCount());
if (projectRelativePath.isEmpty()) {
return project;
} else {
return project.findMember(projectRelativePath);
}
}
public static void applyEdit(TextEdit textEdit, IDocument document) throws BadLocationException {
document.replace(
LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document),
LSPEclipseUtils.toOffset(textEdit.getRange().getEnd(), document) - LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document),
textEdit.getNewText());
}
/**
* Method will apply all edits to document as single modification. Needs to
* be executed in UI thread.
*
* @param document
* document to modify
* @param edits
* list of LSP TextEdits
*/
public static void applyEdits(IDocument document, List<? extends TextEdit> edits) {
if (document == null || edits.isEmpty()) {
return;
}
IDocumentUndoManager manager = DocumentUndoManagerRegistry.getDocumentUndoManager(document);
if (manager != null) {
manager.beginCompoundChange();
}
MultiTextEdit edit = new MultiTextEdit();
for (TextEdit textEdit : edits) {
if (textEdit != null) {
try {
int offset = LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document);
int length = LSPEclipseUtils.toOffset(textEdit.getRange().getEnd(), document) - offset;
edit.addChild(new ReplaceEdit(offset, length, textEdit.getNewText()));
} catch (BadLocationException e) {
LanguageServerPlugin.logError(e);
}
}
}
try {
RewriteSessionEditProcessor editProcessor = new RewriteSessionEditProcessor(document, edit,
org.eclipse.text.edits.TextEdit.NONE);
editProcessor.performEdits();
} catch (MalformedTreeException | BadLocationException e) {
LanguageServerPlugin.logError(e);
}
if (manager != null) {
manager.endCompoundChange();
}
}
@Nullable
public static IDocument getDocument(@Nullable IResource resource) {
if (resource == null) {
return null;
}
ITextFileBufferManager bufferManager = FileBuffers.getTextFileBufferManager();
IDocument document = null;
ITextFileBuffer buffer = bufferManager.getTextFileBuffer(resource.getFullPath(), LocationKind.IFILE);
if (buffer != null) {
document = buffer.getDocument();
} else if (resource.getType() == IResource.FILE) {
try {
bufferManager.connect(resource.getFullPath(), LocationKind.IFILE, new NullProgressMonitor());
} catch (CoreException e) {
LanguageServerPlugin.logError(e);
return document;
}
buffer = bufferManager.getTextFileBuffer(resource.getFullPath(), LocationKind.IFILE);
if (buffer != null) {
document = buffer.getDocument();
}
}
return document;
}
public static void openInEditor(Location location, IWorkbenchPage page) {
IEditorPart part = null;
IDocument targetDocument = null;
IResource targetResource = LSPEclipseUtils.findResourceFor(location.getUri());
try {
if (targetResource != null && targetResource.getType() == IResource.FILE) {
part = IDE.openEditor(page, (IFile) targetResource);
targetDocument = FileBuffers.getTextFileBufferManager()
.getTextFileBuffer(targetResource.getFullPath(), LocationKind.IFILE).getDocument();
} else {
URI fileUri = URI.create(location.getUri()).normalize();
IFileStore fileStore = EFS.getLocalFileSystem().getStore(fileUri);
IFileInfo fetchInfo = fileStore.fetchInfo();
if (!fetchInfo.isDirectory() && fetchInfo.exists()) {
part = IDE.openEditorOnFileStore(page, fileStore);
ITextFileBuffer fileStoreTextFileBuffer = FileBuffers.getTextFileBufferManager()
.getFileStoreTextFileBuffer(fileStore);
targetDocument = fileStoreTextFileBuffer.getDocument();
}
}
} catch (PartInitException e) {
LanguageServerPlugin.logError(e);
}
try {
if (part instanceof AbstractTextEditor) {
AbstractTextEditor editor = (AbstractTextEditor) part;
int offset = LSPEclipseUtils.toOffset(location.getRange().getStart(), targetDocument);
int endOffset = LSPEclipseUtils.toOffset(location.getRange().getEnd(), targetDocument);
editor.getSelectionProvider()
.setSelection(new TextSelection(offset, endOffset > offset ? endOffset - offset : 0));
}
} catch (BadLocationException e) {
LanguageServerPlugin.logError(e);
}
}
public static IDocument getDocument(ITextEditor editor) {
try {
Method getSourceViewerMethod= AbstractTextEditor.class.getDeclaredMethod("getSourceViewer"); //$NON-NLS-1$
getSourceViewerMethod.setAccessible(true);
ITextViewer viewer = (ITextViewer) getSourceViewerMethod.invoke(editor);
return viewer.getDocument();
} catch (Exception ex) {
LanguageServerPlugin.logError(ex);
return null;
}
}
public static IDocument getDocument(IEditorInput editorInput) {
if(editorInput instanceof IFileEditorInput) {
IFileEditorInput fileEditorInput = (IFileEditorInput)editorInput;
return getDocument(fileEditorInput.getFile());
}else if(editorInput instanceof IPathEditorInput) {
IPathEditorInput pathEditorInput = (IPathEditorInput)editorInput;
return getDocument(ResourcesPlugin.getWorkspace().getRoot().getFile(pathEditorInput.getPath()));
}else if(editorInput instanceof IURIEditorInput) {
IURIEditorInput uriEditorInput = (IURIEditorInput)editorInput;
return getDocument(findResourceFor(uriEditorInput.getURI().toString()));
}
return null;
}
/**
* Applies a workspace edit. It does simply change the underlying documents.
*
* @param wsEdit
*/
public static void applyWorkspaceEdit(WorkspaceEdit wsEdit) {
CompositeChange change = toCompositeChange(wsEdit);
PerformChangeOperation changeOperation = new PerformChangeOperation(change);
try {
ResourcesPlugin.getWorkspace().run(changeOperation, new NullProgressMonitor());
} catch (CoreException e) {
LanguageServerPlugin.logError(e);
}
}
/**
* Returns a ltk {@link CompositeChange} from a lsp {@link WorkspaceEdit}.
*
* @param wsEdit
* @return a ltk {@link CompositeChange} from a lsp {@link WorkspaceEdit}.
*/
public static CompositeChange toCompositeChange(WorkspaceEdit wsEdit) {
CompositeChange change = new CompositeChange("LSP Workspace Edit"); //$NON-NLS-1$
for (java.util.Map.Entry<String, List<TextEdit>> edit : wsEdit.getChanges().entrySet()) {
String uri = edit.getKey();
IDocument document = LSPEclipseUtils.getDocument(LSPEclipseUtils.findResourceFor(uri));
for (TextEdit textEdit : edit.getValue()) {
try {
int offset = LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document);
int length = LSPEclipseUtils.toOffset(textEdit.getRange().getEnd(), document) - offset;
DocumentChange documentChange = new DocumentChange("Change in document " + uri, document); //$NON-NLS-1$
documentChange.initializeValidationData(new NullProgressMonitor());
documentChange.setEdit(new ReplaceEdit(offset, length, textEdit.getNewText()));
change.add(documentChange);
} catch (BadLocationException e) {
LanguageServerPlugin.logError(e);
}
}
}
return change;
}
public static URI toUri(IPath absolutePath) {
return toUri(absolutePath.toFile());
}
public static URI toUri(IResource resource) {
return toUri(resource.getLocation());
}
public static URI toUri(File file) {
// URI scheme specified by language server protocol and LSP
try {
return new URI("file", "", file.getAbsoluteFile().toURI().getPath(), null); //$NON-NLS-1$ //$NON-NLS-2$
} catch (URISyntaxException e) {
LanguageServerPlugin.logError(e);
return file.getAbsoluteFile().toURI();
}
}
// TODO consider using Entry/SimpleEntry instead
private static final class Pair<K, V> {
K key;
V value;
Pair(K key,V value) {
this.key = key;
this.value = value;
}
}
/**
* Very empirical and unsafe heuristic to turn unknown command arguments
* into a workspace edit...
*/
public static WorkspaceEdit createWorkspaceEdit(List<Object> commandArguments, IResource initialResource) {
WorkspaceEdit res = new WorkspaceEdit();
Map<String, List<TextEdit>> changes = new HashMap<>();
res.setChanges(changes);
Pair<IResource, List<TextEdit>> currentEntry = new Pair<>(initialResource, new ArrayList<>());
commandArguments.stream().flatMap(item -> {
if (item instanceof List) {
return ((List<?>)item).stream();
} else {
return Collections.singleton(item).stream();
}
}).forEach(arg -> {
if (arg instanceof String) {
changes.put(currentEntry.key.getLocationURI().toString(), currentEntry.value);
IResource resource = LSPEclipseUtils.findResourceFor((String)arg);
if (resource != null) {
currentEntry.key = resource;
currentEntry.value = new ArrayList<>();
}
} else if (arg instanceof WorkspaceEdit) {
changes.putAll(((WorkspaceEdit)arg).getChanges());
} else if (arg instanceof TextEdit) {
currentEntry.value.add((TextEdit)arg);
} else if (arg instanceof Map) {
Gson gson = new Gson(); // TODO? retrieve the GSon used by LS
TextEdit edit = gson.fromJson(gson.toJson(arg), TextEdit.class);
if (edit != null) {
currentEntry.value.add(edit);
}
}
});
changes.put(currentEntry.key.getLocationURI().toString(), currentEntry.value);
return res;
}
@Nullable public static IFile getFile(IDocument document) {
ITextFileBuffer buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(document);
if (buffer == null) {
return null;
}
final IPath location = buffer.getLocation();
return ResourcesPlugin.getWorkspace().getRoot().getFile(location);
}
}