blob: f1de03741003d0cd21d7ce1fda710892515ae498 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.lsp.client;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.StyledDocument;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CreateFile;
import org.eclipse.lsp4j.DeleteFile;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.RenameFile;
import org.eclipse.lsp4j.ResourceOperation;
import org.eclipse.lsp4j.ResourceOperationKind;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.netbeans.api.editor.document.LineDocument;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.modules.editor.indent.api.IndentUtils;
import org.openide.cookies.EditorCookie;
import org.openide.cookies.LineCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.text.Line;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
/**
*
* @author lahvac
*/
public class Utils {
public static String toURI(FileObject file) {
return file.toURI().toString().replace("file:/", "file:///");
}
public static Position createPosition(Document doc, int offset) throws BadLocationException {
return new Position(LineDocumentUtils.getLineIndex((LineDocument) doc, offset),
offset - LineDocumentUtils.getLineStart((LineDocument) doc, offset));
}
public static int getOffset(Document doc, Position pos) {
return LineDocumentUtils.getLineStartFromIndex((LineDocument) doc, pos.getLine()) + pos.getCharacter();
}
public static int getEndCharacter(Document doc, int line) {
int start = LineDocumentUtils.getLineStartFromIndex((LineDocument) doc, line);
try {
return LineDocumentUtils.getLineEnd((LineDocument) doc, start) - start;
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
return 0;
}
public static void applyWorkspaceEdit(WorkspaceEdit edit) {
if (edit.getDocumentChanges() != null) {
for (Either<TextDocumentEdit, ResourceOperation> change : edit.getDocumentChanges()) {
if (change.isLeft()) {
applyEdits(change.getLeft().getTextDocument().getUri(), change.getLeft().getEdits());
} else {
switch (change.getRight().getKind()) {
case ResourceOperationKind.Create:
try {
FileUtil.createData(new File(new URI(((CreateFile) change.getRight()).getUri())));
} catch (IOException | URISyntaxException ex) {
Exceptions.printStackTrace(ex);
}
break;
case ResourceOperationKind.Delete:
try {
URLMapper.findFileObject(new URI(((DeleteFile) change.getRight()).getUri()).toURL()).delete();
} catch (IOException | URISyntaxException ex) {
Exceptions.printStackTrace(ex);
}
break;
case ResourceOperationKind.Rename:
try {
File target = new File(new URI(((RenameFile) change.getRight()).getNewUri()));
FileObject targetFolder = FileUtil.createFolder(target.getParentFile());
FileObject source = URLMapper.findFileObject(new URI(((RenameFile) change.getRight()).getOldUri()).toURL());
DataObject od = DataObject.find(source);
//XXX: should move and rename in one go!
od.move(DataFolder.findFolder(targetFolder));
od.rename(target.getName());
} catch (IOException | URISyntaxException ex) {
Exceptions.printStackTrace(ex);
}
break;
}
}
}
} else {
for (Map.Entry<String, List<TextEdit>> e : edit.getChanges().entrySet()) {
applyEdits(e.getKey(), e.getValue());
}
}
}
private static void applyEdits(String uri, List<TextEdit> edits) {
try {
FileObject file = URLMapper.findFileObject(new URI(uri).toURL());
EditorCookie ec = file.getLookup().lookup(EditorCookie.class);
Document doc = ec != null ? ec.openDocument() : null;
if (doc == null) {
return ;
}
NbDocument.runAtomic((StyledDocument) doc, () -> {
applyEditsNoLock(doc, edits);
});
} catch (URISyntaxException | IOException ex) {
Exceptions.printStackTrace(ex);
}
}
public static void applyEditsNoLock(Document doc, List<? extends TextEdit> edits) {
applyEditsNoLock(doc, edits, null, null);
}
/**
* Apply edits to the document. The edits can be filtered to only cover
* parts of the documents.
*
* @param doc {@link Document} the edits shall be applied to
* @param edits list {@link TextEdit} to apply
* @param startLimit if not {@code null} only edits with a {@code start}
* larger than or equals to this offset are considered.
* The offset is expected to be apply to the original
* state of the document.
* @param endLimit if not {@code null} only edits with an {@code end}
* lower than this offset are considered. The offset is
* expected to be apply to the original state of the
* document.
*/
public static void applyEditsNoLock(Document doc, List<? extends TextEdit> edits, Integer startLimit, Integer endLimit) {
edits
.stream()
.sorted(rangeReverseSort)
.forEach(te -> {
try {
int start = Utils.getOffset(doc, te.getRange().getStart());
int end = Utils.getOffset(doc, te.getRange().getEnd());
if ((startLimit == null || start >= startLimit)
&& (endLimit == null || end < endLimit)) {
doc.remove(start, end - start);
doc.insertString(start, te.getNewText(), null);
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
});
}
public static void applyCodeAction(LSPBindings server, Either<Command, CodeAction> cmd) {
try {
Command command;
if (cmd.isLeft()) {
command = cmd.getLeft();
} else {
if(cmd.getRight().getEdit() != null) {
Utils.applyWorkspaceEdit(cmd.getRight().getEdit());
}
command = cmd.getRight().getCommand();
}
if (command != null) {
server.getWorkspaceService().executeCommand(new ExecuteCommandParams(command.getCommand(), command.getArguments())).get();
}
} catch (InterruptedException | ExecutionException ex) {
Exceptions.printStackTrace(ex);
}
}
public static Position computeEndPositionForRemovedText(Position startPos, String removedText) {
int endLine = startPos.getLine();
int endChar = startPos.getCharacter();
for (char c : removedText.toCharArray()) {
if (c == '\n') {
endLine++;
endChar = 0;
} else {
endChar++;
}
}
return new Position(endLine, endChar);
}
public static List<TextEdit> computeDefaultOnTypeIndent(Document doc, int changeStart, Position startPos, String newText) {
List<TextEdit> edits = new ArrayList<>();
try {
int indentLevel = IndentUtils.indentLevelSize(doc);
int lineStart = IndentUtils.lineStartOffset(doc, changeStart);
int indent = IndentUtils.lineIndent(doc, lineStart);
if (newText.equals("}") && indent == changeStart - lineStart) {
CharSequence cs = DocumentUtilities.getText(doc);
int balance = 1;
int idx = changeStart - 1;
while (idx >= 0 && balance > 0) {
switch (cs.charAt(idx)) {
case '{': balance--; break;
case '}': balance++; break;
}
idx--;
}
int newIndent;
if (balance == 0) {
newIndent = IndentUtils.lineIndent(doc, IndentUtils.lineStartOffset(doc, idx));
} else {
newIndent = 0;
}
edits.add(new TextEdit(new Range(new Position(startPos.getLine(), 0), new Position(startPos.getLine(), indent)), IndentUtils.createIndentString(doc, newIndent)));
} else if (newText.equals("\n")) {
Position insertPos = new Position(startPos.getLine() + 1, 0);
int newIndent = indent;
if (changeStart > 0 && DocumentUtilities.getText(doc, changeStart - 1, 1).charAt(0) == '{') {
newIndent += indentLevel;
}
edits.add(new TextEdit(new Range(insertPos, insertPos), IndentUtils.createIndentString(doc, newIndent)));
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
return edits;
}
public static FileObject fromURI(String targetUri) {
try {
URI target = URI.create(targetUri);
return URLMapper.findFileObject(target.toURL());
} catch (MalformedURLException ex) {
Exceptions.printStackTrace(ex);
return null;
}
}
public static void open(String targetUri, Range targetRange) {
FileObject targetFile = fromURI(targetUri);
if (targetFile != null) {
LineCookie lc = targetFile.getLookup().lookup(LineCookie.class);
//TODO: expecting lc != null!
Line line = lc.getLineSet().getCurrent(targetRange.getStart().getLine());
SwingUtilities.invokeLater(() ->
line.show(Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS, targetRange.getStart().getCharacter())
);
} else {
//TODO: beep
}
}
private static final Comparator<TextEdit> rangeReverseSort = (s1, s2) -> {
int l1 = s1.getRange().getEnd().getLine();
int l2 = s2.getRange().getEnd().getLine();
int c1 = s1.getRange().getEnd().getCharacter();
int c2 = s2.getRange().getEnd().getCharacter();
if (l1 != l2) {
return l2 - l1;
} else {
return c2 - c1;
}
};
public static boolean isTrue(Boolean b) {
return b != null && b;
}
public static boolean isEnabled(Either<Boolean, ?> settings) {
return settings != null && (settings.isLeft() ? isTrue(settings.getLeft())
: settings.getRight() != null);
}
}