blob: 583fd2e9441e469ac355c72d1551ca19cdd9c2ad [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.java.lsp.server.text;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.util.TreePath;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
import com.vladsch.flexmark.parser.Parser;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.prefs.Preferences;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.StyledDocument;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentHighlight;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.netbeans.api.editor.document.LineDocument;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.ModificationResult;
import org.netbeans.api.java.source.SourceUtils;
import org.netbeans.api.java.source.support.ReferencesCount;
import org.netbeans.api.java.source.ui.ElementJavadoc;
import org.netbeans.api.java.source.ui.ElementOpen;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.editor.java.GoToSupport;
import org.netbeans.modules.editor.java.GoToSupport.Context;
import org.netbeans.modules.editor.java.GoToSupport.GoToTarget;
import org.netbeans.modules.editor.java.Utilities;
import org.netbeans.modules.java.completion.JavaCompletionTask;
import org.netbeans.modules.java.completion.JavaCompletionTask.Options;
import org.netbeans.modules.java.completion.JavaDocumentationTask;
import org.netbeans.modules.java.editor.base.semantic.MarkOccurrencesHighlighterBase;
import org.netbeans.modules.java.editor.options.MarkOccurencesSettings;
import org.netbeans.modules.java.hints.infrastructure.CreatorBasedLazyFixList;
import org.netbeans.modules.java.hints.infrastructure.ErrorHintsProvider;
import org.netbeans.modules.java.hints.spiimpl.JavaFixImpl;
import org.netbeans.modules.java.hints.spiimpl.hints.HintsInvoker;
import org.netbeans.modules.java.hints.spiimpl.options.HintsSettings;
import org.netbeans.modules.java.source.ElementHandleAccessor;
import org.netbeans.modules.java.source.ui.ElementOpenAccessor;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.editor.hints.LazyFixList;
import org.netbeans.spi.java.hints.JavaFix;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.modules.Places;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.RequestProcessor;
/**
*
* @author lahvac
*/
public class TextDocumentServiceImpl implements TextDocumentService, LanguageClientAware {
private static final RequestProcessor BACKGROUND_TASKS = new RequestProcessor(TextDocumentServiceImpl.class.getName(), 1, false, false);
private final Map<String, Document> openedDocuments = new HashMap<>();
private final Map<String, RequestProcessor.Task> diagnosticTasks = new HashMap<>();
private LanguageClient client;
public TextDocumentServiceImpl() {
}
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
try {
String uri = params.getTextDocument().getUri();
FileObject file = fromUri(uri);
EditorCookie ec = file.getLookup().lookup(EditorCookie.class);
Document doc = ec.openDocument();
int caret = getOffset(doc, params.getPosition());
JavaCompletionTask<CompletionItem> task = JavaCompletionTask.create(caret, new ItemFactoryImpl(uri), EnumSet.noneOf(Options.class), () -> false);
ParserManager.parse(Collections.singletonList(Source.create(doc)), task);
List<CompletionItem> result = task.getResults();
for (Iterator<CompletionItem> it = result.iterator(); it.hasNext();) {
CompletionItem item = it.next();
if (item == null) {
it.remove();
}
}
return CompletableFuture.completedFuture(Either.<List<CompletionItem>, CompletionList>forRight(new CompletionList(result)));
} catch (IOException | ParseException ex) {
throw new IllegalStateException(ex);
}
}
public static final class CompletionData {
public String uri;
public String kind;
public String[] elementHandle;
public CompletionData() {
}
public CompletionData(String uri, String kind, String[] elementHandle) {
this.uri = uri;
this.kind = kind;
this.elementHandle = elementHandle;
}
@Override
public String toString() {
return "CompletionData{" + "uri=" + uri + ", kind=" + kind + ", elementHandle=" + elementHandle + '}';
}
}
@Override
public void connect(LanguageClient client) {
this.client = client;
}
private static class ItemFactoryImpl implements JavaCompletionTask.ItemFactory<CompletionItem> {
private final String uri;
public ItemFactoryImpl(String uri) {
this.uri = uri;
}
private static final Set<String> SUPPORTED_ELEMENT_KINDS = new HashSet<>(Arrays.asList("PACKAGE", "CLASS", "INTERFACE", "ENUM", "ANNOTATION_TYPE", "METHOD", "CONSTRUCTOR", "INSTANCE_INIT", "STATIC_INIT", "FIELD", "ENUM_CONSTANT", "TYPE_PARAMETER", "MODULE"));
private void setCompletionData(CompletionItem ci, Element el) {
if (SUPPORTED_ELEMENT_KINDS.contains(el.getKind().name())) {
ci.setData(new CompletionData(uri, el.getKind().name(), SourceUtils.getJVMSignature(ElementHandle.create(el))));
}
}
@Override
public CompletionItem createKeywordItem(String kwd, String postfix, int substitutionOffset, boolean smartType) {
CompletionItem item = new CompletionItem(kwd);
item.setKind(CompletionItemKind.Keyword);
return item;
}
@Override
public CompletionItem createPackageItem(String pkgFQN, int substitutionOffset, boolean inPackageStatement) {
return null; //TODO: fill
}
@Override
public CompletionItem createTypeItem(CompilationInfo info, TypeElement elem, DeclaredType type, int substitutionOffset, ReferencesCount referencesCount, boolean isDeprecated, boolean insideNew, boolean addTypeVars, boolean addSimpleName, boolean smartType, boolean autoImportEnclosingType) {
CompletionItem item = new CompletionItem(elem.getSimpleName().toString());
item.setKind(elementKind2CompletionItemKind(elem.getKind()));
setCompletionData(item, elem);
return item;
}
@Override
public CompletionItem createTypeItem(ElementHandle<TypeElement> handle, EnumSet<ElementKind> kinds, int substitutionOffset, ReferencesCount referencesCount, Source source, boolean insideNew, boolean addTypeVars, boolean afterExtends) {
return null; //TODO: fill
}
@Override
public CompletionItem createArrayItem(CompilationInfo info, ArrayType type, int substitutionOffset, ReferencesCount referencesCount, Elements elements) {
return null; //TODO: fill
}
@Override
public CompletionItem createTypeParameterItem(TypeParameterElement elem, int substitutionOffset) {
CompletionItem item = new CompletionItem(elem.getSimpleName().toString());
item.setKind(elementKind2CompletionItemKind(elem.getKind()));
return item;
}
@Override
public CompletionItem createVariableItem(CompilationInfo info, VariableElement elem, TypeMirror type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean smartType, int assignToVarOffset) {
CompletionItem item = new CompletionItem(elem.getSimpleName().toString());
item.setKind(elementKind2CompletionItemKind(elem.getKind()));
setCompletionData(item, elem);
return item;
}
@Override
public CompletionItem createVariableItem(CompilationInfo info, String varName, int substitutionOffset, boolean newVarName, boolean smartType) {
CompletionItem item = new CompletionItem(varName);
item.setKind(CompletionItemKind.Variable);
return item;
}
@Override
public CompletionItem createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef) {
Iterator<? extends VariableElement> it = elem.getParameters().iterator();
Iterator<? extends TypeMirror> tIt = type.getParameterTypes().iterator();
StringBuilder label = new StringBuilder();
String sep = "";
label.append(elem.getSimpleName().toString());
label.append("(");
while(it.hasNext() && tIt.hasNext()) {
TypeMirror tm = tIt.next();
if (tm == null) {
break;
}
label.append(sep);
label.append(Utilities.getTypeName(info, tm, false, elem.isVarArgs() && !tIt.hasNext()).toString());
label.append(' ');
label.append(it.next().getSimpleName().toString());
sep = ", ";
}
label.append(") : ");
TypeMirror retType = type.getReturnType();
label.append(Utilities.getTypeName(info, retType, false).toString());
CompletionItem item = new CompletionItem(label.toString());
item.setKind(elementKind2CompletionItemKind(elem.getKind()));
item.setInsertText(elem.getSimpleName().toString());
setCompletionData(item, elem);
return item;
}
@Override
public CompletionItem createThisOrSuperConstructorItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean isDeprecated, String name) {
CompletionItem item = new CompletionItem(name);
item.setKind(CompletionItemKind.Field);
setCompletionData(item, elem);
return item;
}
@Override
public CompletionItem createOverrideMethodItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean implement) {
CompletionItem item = new CompletionItem(elem.getSimpleName().toString() + " - override");
item.setKind(elementKind2CompletionItemKind(elem.getKind()));
setCompletionData(item, elem);
return item;
}
@Override
public CompletionItem createGetterSetterMethodItem(CompilationInfo info, VariableElement elem, TypeMirror type, int substitutionOffset, String name, boolean setter) {
return null; //TODO: fill
}
@Override
public CompletionItem createDefaultConstructorItem(TypeElement elem, int substitutionOffset, boolean smartType) {
return null; //TODO: fill
}
@Override
public CompletionItem createParametersItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean isDeprecated, int activeParamIndex, String name) {
return null; //TODO: fill
}
@Override
public CompletionItem createAnnotationItem(CompilationInfo info, TypeElement elem, DeclaredType type, int substitutionOffset, ReferencesCount referencesCount, boolean isDeprecated) {
return null; //TODO: fill
}
@Override
public CompletionItem createAttributeItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean isDeprecated) {
return null; //TODO: fill
}
@Override
public CompletionItem createAttributeValueItem(CompilationInfo info, String value, String documentation, TypeElement element, int substitutionOffset, ReferencesCount referencesCount) {
return null; //TODO: fill
}
@Override
public CompletionItem createStaticMemberItem(CompilationInfo info, DeclaredType type, Element memberElem, TypeMirror memberType, boolean multipleVersions, int substitutionOffset, boolean isDeprecated, boolean addSemicolon) {
return null; //TODO: fill
}
@Override
public CompletionItem createStaticMemberItem(ElementHandle<TypeElement> handle, String name, int substitutionOffset, boolean addSemicolon, ReferencesCount referencesCount, Source source) {
return null; //TODO: fill
}
@Override
public CompletionItem createChainedMembersItem(CompilationInfo info, List<? extends Element> chainedElems, List<? extends TypeMirror> chainedTypes, int substitutionOffset, boolean isDeprecated, boolean addSemicolon) {
return null; //TODO: fill
}
@Override
public CompletionItem createInitializeAllConstructorItem(CompilationInfo info, boolean isDefault, Iterable<? extends VariableElement> fields, ExecutableElement superConstructor, TypeElement parent, int substitutionOffset) {
return null; //TODO: fill
}
private static CompletionItemKind elementKind2CompletionItemKind(ElementKind kind) {
switch (kind) {
case PACKAGE:
return CompletionItemKind.Folder;
case ENUM:
return CompletionItemKind.Enum;
case CLASS:
return CompletionItemKind.Class;
case ANNOTATION_TYPE:
return CompletionItemKind.Interface;
case INTERFACE:
return CompletionItemKind.Interface;
case ENUM_CONSTANT:
return CompletionItemKind.EnumMember;
case FIELD:
return CompletionItemKind.Field;
case PARAMETER:
return CompletionItemKind.Variable;
case LOCAL_VARIABLE:
return CompletionItemKind.Variable;
case EXCEPTION_PARAMETER:
return CompletionItemKind.Variable;
case METHOD:
return CompletionItemKind.Method;
case CONSTRUCTOR:
return CompletionItemKind.Constructor;
case TYPE_PARAMETER:
return CompletionItemKind.TypeParameter;
case RESOURCE_VARIABLE:
return CompletionItemKind.Variable;
case MODULE:
return CompletionItemKind.Module;
case STATIC_INIT:
case INSTANCE_INIT:
case OTHER:
default:
return CompletionItemKind.Text;
}
}
}
private static final RequestProcessor JAVADOC_WORKER = new RequestProcessor(TextDocumentServiceImpl.class.getName() + ".javadoc", 1);
@Override
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem ci) {
JsonObject rawData = (JsonObject) ci.getData();
if (rawData == null) {
return CompletableFuture.completedFuture(ci);
}
CompletionData data = new Gson().fromJson(rawData, CompletionData.class);
try {
FileObject file = fromUri(data.uri);
EditorCookie ec = file.getLookup().lookup(EditorCookie.class);
Document doc = ec.openDocument();
ElementHandle<Element> handle = ElementHandleAccessor.getInstance().create(ElementKind.valueOf(data.kind), data.elementHandle);
JavaDocumentationTask<Future<String>> task = JavaDocumentationTask.create(-1, handle, new JavaDocumentationTask.DocumentationFactory<Future<String>>() {
@Override
public Future<String> create(CompilationInfo compilationInfo, Element element, Callable<Boolean> cancel) {
return ElementJavadoc.create(compilationInfo, element, cancel).getTextAsync();
}
}, () -> false);
ParserManager.parse(Collections.singletonList(Source.create(doc)), task);
Future<String> futureJavadoc = task.getDocumentation();
CompletableFuture<CompletionItem> result = new CompletableFuture<CompletionItem>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return futureJavadoc.cancel(mayInterruptIfRunning) && super.cancel(mayInterruptIfRunning);
}
};
JAVADOC_WORKER.post(() -> {
try {
String javadoc = futureJavadoc.get();
MarkupContent markup = new MarkupContent();
markup.setKind("markdown");
markup.setValue(html2MD(javadoc));
ci.setDocumentation(markup);
result.complete(ci);
} catch (ExecutionException | InterruptedException ex) {
result.completeExceptionally(ex);
}
});
return result;
} catch (IOException | ParseException ex) {
CompletableFuture<CompletionItem> result = new CompletableFuture<CompletionItem>();
result.completeExceptionally(ex);
return result;
}
}
public static String html2MD(String html) {
return FlexmarkHtmlConverter.builder().build().convert(html).replaceAll("<br />[ \n]*$", "");
}
@Override
public CompletableFuture<Hover> hover(TextDocumentPositionParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<List<? extends Location>> definition(TextDocumentPositionParams params) {
JavaSource js = getSource(params.getTextDocument().getUri());
GoToTarget[] target = new GoToTarget[1];
LineMap[] lm = new LineMap[1];
try {
js.runUserActionTask(cc -> {
cc.toPhase(JavaSource.Phase.RESOLVED);
Document doc = cc.getSnapshot().getSource().getDocument(true);
int offset = getOffset(doc, params.getPosition());
Context context = GoToSupport.resolveContext(cc, doc, offset, false, false);
if (context == null) {
return ;
}
target[0] = GoToSupport.computeGoToTarget(cc, context, offset);
lm[0] = cc.getCompilationUnit().getLineMap();
}, true);
} catch (IOException ex) {
//TODO: include stack trace:
client.logMessage(new MessageParams(MessageType.Error, ex.getMessage()));
}
List<Location> result = new ArrayList<>();
if (target[0] != null && target[0].success) {
if (target[0].offsetToOpen < 0) {
Object[] openInfo = ElementOpenAccessor.getInstance().getOpenInfo(target[0].cpInfo, target[0].elementToOpen, new AtomicBoolean());
if (openInfo != null) {
FileObject file = (FileObject) openInfo[0];
int start = (int) openInfo[1];
int end = (int) openInfo[2];
result.add(new Location(toUri(file),
new Range(createPosition(lm[0], start),
createPosition(lm[0], end))));
}
} else {
Position pos = createPosition(js.getFileObjects().iterator().next(), target[0].offsetToOpen);
result.add(new Location(params.getTextDocument().getUri(),
new Range(pos, pos)));
}
}
return CompletableFuture.completedFuture(result);
}
@Override
public CompletableFuture<List<? extends Location>> references(ReferenceParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams params) {
class MOHighligther extends MarkOccurrencesHighlighterBase {
@Override
protected void process(CompilationInfo arg0, Document arg1, SchedulerEvent arg2) {
throw new UnsupportedOperationException("Should not be called.");
}
@Override
public List<int[]> processImpl(CompilationInfo info, Preferences node, Document doc, int caretPosition) {
return super.processImpl(info, node, doc, caretPosition);
}
}
Preferences node = MarkOccurencesSettings.getCurrentNode();
JavaSource js = getSource(params.getTextDocument().getUri());
List<DocumentHighlight> result = new ArrayList<>();
try {
js.runUserActionTask(cc -> {
cc.toPhase(JavaSource.Phase.RESOLVED);
Document doc = cc.getSnapshot().getSource().getDocument(true);
int offset = getOffset(doc, params.getPosition());
List<int[]> spans = new MOHighligther().processImpl(cc, node, doc, offset);
if (spans != null) {
for (int[] span : spans) {
result.add(new DocumentHighlight(new Range(createPosition(cc.getCompilationUnit(), span[0]),
createPosition(cc.getCompilationUnit(), span[1]))));
}
}
}, true);
} catch (IOException ex) {
//TODO: include stack trace:
client.logMessage(new MessageParams(MessageType.Error, ex.getMessage()));
}
return CompletableFuture.completedFuture(result);
}
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(DocumentSymbolParams params) {
JavaSource js = getSource(params.getTextDocument().getUri());
List<Either<SymbolInformation, DocumentSymbol>> result = new ArrayList<>();
try {
js.runUserActionTask(cc -> {
cc.toPhase(JavaSource.Phase.RESOLVED);
for (Element tel : cc.getTopLevelElements()) {
DocumentSymbol ds = element2DocumentSymbol(cc, tel);
if (ds != null)
result.add(Either.forRight(ds));
}
}, true);
} catch (IOException ex) {
//TODO: include stack trace:
client.logMessage(new MessageParams(MessageType.Error, ex.getMessage()));
}
return CompletableFuture.completedFuture(result);
}
private DocumentSymbol element2DocumentSymbol(CompilationInfo info, Element el) throws BadLocationException {
TreePath path = info.getTrees().getPath(el);
if (path == null)
return null;
long start = info.getTrees().getSourcePositions().getStartPosition(path.getCompilationUnit(), path.getLeaf());
long end = info.getTrees().getSourcePositions().getEndPosition(path.getCompilationUnit(), path.getLeaf());
if (end == (-1))
return null;
Range range = new Range(createPosition(info.getCompilationUnit(), (int) start),
createPosition(info.getCompilationUnit(), (int) end));
List<DocumentSymbol> children = new ArrayList<>();
for (Element c : el.getEnclosedElements()) {
DocumentSymbol ds = element2DocumentSymbol(info, c);
if (ds != null) {
children.add(ds);
}
}
return new DocumentSymbol(el.getSimpleName().toString(), elementKind2SymbolKind(el.getKind()), range, range, null, children);
}
private static SymbolKind elementKind2SymbolKind(ElementKind kind) {
switch (kind) {
case PACKAGE:
return SymbolKind.Package;
case ENUM:
return SymbolKind.Enum;
case CLASS:
return SymbolKind.Class;
case ANNOTATION_TYPE:
return SymbolKind.Interface;
case INTERFACE:
return SymbolKind.Interface;
case ENUM_CONSTANT:
return SymbolKind.EnumMember;
case FIELD:
return SymbolKind.Field; //TODO: constant
case PARAMETER:
return SymbolKind.Variable;
case LOCAL_VARIABLE:
return SymbolKind.Variable;
case EXCEPTION_PARAMETER:
return SymbolKind.Variable;
case METHOD:
return SymbolKind.Method;
case CONSTRUCTOR:
return SymbolKind.Constructor;
case TYPE_PARAMETER:
return SymbolKind.TypeParameter;
case RESOURCE_VARIABLE:
return SymbolKind.Variable;
case MODULE:
return SymbolKind.Module;
case STATIC_INIT:
case INSTANCE_INIT:
case OTHER:
default:
return SymbolKind.File; //XXX: what here?
}
}
@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
Document doc = openedDocuments.get(params.getTextDocument().getUri());
if (doc == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
Map<String, ErrorDescription> id2Errors = (Map<String, ErrorDescription>) doc.getProperty("lsp-errors");
if (id2Errors == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
JavaSource js = JavaSource.forDocument(doc);
List<Either<Command, CodeAction>> result = new ArrayList<>();
for (Diagnostic diag : params.getContext().getDiagnostics()) {
ErrorDescription err = id2Errors.get(diag.getCode());
if (err == null) {
client.logMessage(new MessageParams(MessageType.Log, "Cannot resolve error, code: " + diag.getCode()));
continue;
}
LazyFixList lfl = err.getFixes();
if (lfl instanceof CreatorBasedLazyFixList) {
try {
js.runUserActionTask(cc -> {
cc.toPhase(JavaSource.Phase.RESOLVED);
((CreatorBasedLazyFixList) lfl).compute(cc, new AtomicBoolean());
}, true);
} catch (IOException ex) {
//TODO: include stack trace:
client.logMessage(new MessageParams(MessageType.Error, ex.getMessage()));
}
}
List<Fix> fixes = lfl.getFixes();
//TODO: ordering
for (Fix f : fixes) {
if (f instanceof JavaFixImpl) {
try {
LineMap[] lm = new LineMap[1];
ModificationResult changes = js.runModificationTask(wc -> {
wc.toPhase(JavaSource.Phase.RESOLVED);
Map<FileObject, byte[]> resourceContentChanges = new HashMap<FileObject, byte[]>();
JavaFix jf = ((JavaFixImpl) f).jf;
JavaFixImpl.Accessor.INSTANCE.process(jf, wc, true, resourceContentChanges, /*Ignored in editor:*/new ArrayList<>());
lm[0] = wc.getCompilationUnit().getLineMap();
});
//TODO: full, correct and safe edit production:
List<? extends ModificationResult.Difference> diffs = changes.getDifferences(changes.getModifiedFileObjects().iterator().next());
List<TextEdit> edits = new ArrayList<>();
for (ModificationResult.Difference diff : diffs) {
String newText = diff.getNewText();
edits.add(new TextEdit(new Range(createPosition(lm[0], diff.getStartPosition().getOffset()),
createPosition(lm[0], diff.getEndPosition().getOffset())),
newText != null ? newText : ""));
}
TextDocumentEdit te = new TextDocumentEdit(new VersionedTextDocumentIdentifier(params.getTextDocument().getUri(),
-1),
edits);
CodeAction action = new CodeAction(f.getText());
action.setDiagnostics(Collections.singletonList(diag));
action.setKind(CodeActionKind.QuickFix);
action.setEdit(new WorkspaceEdit(Collections.singletonList(te)));
result.add(Either.forRight(action));
} catch (IOException ex) {
//TODO: include stack trace:
client.logMessage(new MessageParams(MessageType.Error, ex.getMessage()));
}
}
}
}
return CompletableFuture.completedFuture(result);
}
@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<CodeLens> resolveCodeLens(CodeLens arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CompletableFuture<WorkspaceEdit> rename(RenameParams arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void didOpen(DidOpenTextDocumentParams params) {
try {
FileObject file = fromUri(params.getTextDocument().getUri());
EditorCookie ec = file.getLookup().lookup(EditorCookie.class);
Document doc = ec.openDocument();
openedDocuments.put(params.getTextDocument().getUri(), doc);
String text = params.getTextDocument().getText();
try {
doc.remove(0, doc.getLength());
doc.insertString(0, text, null);
} catch (BadLocationException ex) {
//TODO: include stack trace:
client.logMessage(new MessageParams(MessageType.Error, ex.getMessage()));
}
runDiagnoticTasks(params.getTextDocument().getUri());
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public void didChange(DidChangeTextDocumentParams params) {
Document doc = openedDocuments.get(params.getTextDocument().getUri());
NbDocument.runAtomic((StyledDocument) doc, () -> {
for (TextDocumentContentChangeEvent change : params.getContentChanges()) {
try {
int start = getOffset(doc, change.getRange().getStart());
int end = getOffset(doc, change.getRange().getEnd());
doc.remove(start, end - start);
doc.insertString(start, change.getText(), null);
} catch (BadLocationException ex) {
throw new IllegalStateException(ex);
}
}
});
runDiagnoticTasks(params.getTextDocument().getUri());
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
openedDocuments.remove(params.getTextDocument().getUri());
}
@Override
public void didSave(DidSaveTextDocumentParams arg0) {
//TODO: nothing for now?
}
private void runDiagnoticTasks(String uri) {
//XXX: cancelling/deferring the tasks!
diagnosticTasks.computeIfAbsent(uri, u -> {
return BACKGROUND_TASKS.create(() -> {
computeDiags(u, (info, doc) -> {
ErrorHintsProvider ehp = new ErrorHintsProvider();
return ehp.computeErrors(info, doc, "text/x-java"); //TODO: mimetype?
}, "errors", false);
BACKGROUND_TASKS.create(() -> {
computeDiags(u, (info, doc) -> {
return new HintsInvoker(HintsSettings.getGlobalSettings(), new AtomicBoolean()).computeHints(info);
}, "hints", true);
}).schedule(DELAY);
});
}).schedule(DELAY);
}
private static final int DELAY = 500;
private void computeDiags(String uri, ProduceErrors produceErrors, String keyPrefix, boolean update) {
try {
FileObject file = fromUri(uri);
EditorCookie ec = file.getLookup().lookup(EditorCookie.class);
Document doc = ec.openDocument();
ParserManager.parse(Collections.singletonList(Source.create(doc)), new UserTask() {
@Override
public void run(ResultIterator it) throws Exception {
CompilationController cc = CompilationController.get(it.getParserResult());
cc.toPhase(JavaSource.Phase.RESOLVED);
Map<String, ErrorDescription> id2Errors = new HashMap<>();
List<Diagnostic> diags = new ArrayList<>();
int idx = 0;
List<ErrorDescription> errors = produceErrors.computeErrors(cc, doc);
if (errors == null) {
errors = Collections.emptyList();
}
for (ErrorDescription err : errors) {
Diagnostic diag = new Diagnostic(new Range(createPosition(cc.getCompilationUnit(), err.getRange().getBegin().getOffset()),
createPosition(cc.getCompilationUnit(), err.getRange().getEnd().getOffset())),
err.getDescription());
switch (err.getSeverity()) {
case ERROR: diag.setSeverity(DiagnosticSeverity.Error); break;
case VERIFIER:
case WARNING: diag.setSeverity(DiagnosticSeverity.Warning); break;
case HINT: diag.setSeverity(DiagnosticSeverity.Hint); break;
default: diag.setSeverity(DiagnosticSeverity.Information); break;
}
String id = keyPrefix + ":" + idx + "-" + err.getId();
diag.setCode(id);
id2Errors.put(id, err);
diags.add(diag);
}
doc.putProperty("lsp-errors-" + keyPrefix, id2Errors);
doc.putProperty("lsp-errors-diags-" + keyPrefix, diags);
Map<String, ErrorDescription> mergedId2Errors = new HashMap<>();
List<Diagnostic> mergedDiags = new ArrayList<>();
for (String k : ERROR_KEYS) {
Map<String, ErrorDescription> prevErrors = (Map<String, ErrorDescription>) doc.getProperty("lsp-errors-" + k);
if (prevErrors != null) {
mergedId2Errors.putAll(prevErrors);
}
List<Diagnostic> prevDiags = (List<Diagnostic>) doc.getProperty("lsp-errors-diags-" + k);
if (prevDiags != null) {
mergedDiags.addAll(prevDiags);
}
}
doc.putProperty("lsp-errors", mergedId2Errors);
doc.putProperty("lsp-errors-diags", mergedDiags);
client.publishDiagnostics(new PublishDiagnosticsParams(uri, mergedDiags));
}
});
} catch (IOException | ParseException ex) {
throw new IllegalStateException(ex);
}
}
private static final String[] ERROR_KEYS = {"errors", "hints"};
private interface ProduceErrors {
public List<ErrorDescription> computeErrors(CompilationInfo info, Document doc) throws IOException;
}
private JavaSource getSource(String fileUri) {
Document doc = openedDocuments.get(fileUri);
if (doc == null) {
try {
FileObject file = fromUri(fileUri);
return JavaSource.forFileObject(file);
} catch (MalformedURLException ex) {
return null;
}
} else {
return JavaSource.forDocument(doc);
}
}
public static Position createPosition(CompilationUnitTree cut, int offset) {
return createPosition(cut.getLineMap(), offset);
}
public static Position createPosition(LineMap lm, int offset) {
return new Position((int) lm.getLineNumber(offset) - 1,
(int) lm.getColumnNumber(offset) - 1);
}
public static Position createPosition(FileObject file, int offset) {
try {
EditorCookie ec = file.getLookup().lookup(EditorCookie.class);
StyledDocument doc = ec.openDocument();
int line = NbDocument.findLineNumber(doc, offset);
int column = NbDocument.findLineColumn(doc, offset);
return new Position(line, column);
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
public static int getOffset(Document doc, Position pos) {
return LineDocumentUtils.getLineStartFromIndex((LineDocument) doc, pos.getLine()) + pos.getCharacter();
}
private static String toUri(FileObject file) {
if (FileUtil.isArchiveArtifact(file)) {
//VS code cannot open jar:file: URLs, workaround:
File cacheDir = Places.getCacheSubfile("java-server");
File segments = new File(cacheDir, "segments");
Properties props = new Properties();
try (InputStream in = new FileInputStream(segments)) {
props.load(in);
} catch (IOException ex) {
//OK, may not exist yet
}
FileObject archive = FileUtil.getArchiveFile(file);
String archiveString = archive.toURL().toString();
File foundSegment = null;
for (String segment : props.stringPropertyNames()) {
if (archiveString.equals(props.getProperty(segment))) {
foundSegment = new File(cacheDir, segment);
break;
}
}
if (foundSegment == null) {
int i = 0;
while (props.getProperty("s" + i) != null)
i++;
foundSegment = new File(cacheDir, "s" + i);
props.put("s" + i, archiveString);
try (OutputStream in = new FileOutputStream(segments)) {
props.store(in, "");
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
File cache = new File(foundSegment, FileUtil.getRelativePath(FileUtil.getArchiveRoot(archive), file));
cache.getParentFile().mkdirs();
try (OutputStream out = new FileOutputStream(cache)) {
out.write(file.asBytes());
return cache.toURI().toString();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
return file.toURI().toString();
}
//TODO: move to a separate Utils class:
public static FileObject fromUri(String uri) throws MalformedURLException {
File cacheDir = Places.getCacheSubfile("java-server");
URI uriUri = URI.create(uri);
URI relative = cacheDir.toURI().relativize(uriUri);
if (relative != null && new File(cacheDir, relative.toString()).canRead()) {
String segmentAndPath = relative.toString();
int slash = segmentAndPath.indexOf('/');
String segment = segmentAndPath.substring(0, slash);
String path = segmentAndPath.substring(slash + 1);
File segments = new File(cacheDir, "segments");
Properties props = new Properties();
try (InputStream in = new FileInputStream(segments)) {
props.load(in);
String archiveUri = props.getProperty(segment);
FileObject archive = URLMapper.findFileObject(URI.create(archiveUri).toURL());
archive = archive != null ? FileUtil.getArchiveRoot(archive) : null;
FileObject file = archive != null ? archive.getFileObject(path) : null;
if (file != null) {
return file;
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
return URLMapper.findFileObject(URI.create(uri).toURL());
}
}