diff --git a/ide/csl.api/src/org/netbeans/modules/csl/navigation/BreadCrumbsTask.java b/ide/csl.api/src/org/netbeans/modules/csl/navigation/BreadCrumbsTask.java index 397043d745d9..964840400f94 100644 --- a/ide/csl.api/src/org/netbeans/modules/csl/navigation/BreadCrumbsTask.java +++ b/ide/csl.api/src/org/netbeans/modules/csl/navigation/BreadCrumbsTask.java @@ -206,7 +206,14 @@ public TaskFactoryImpl() { @Override protected Collection createTasks(Language language, Snapshot snapshot) { - return Collections.singletonList(new BreadCrumbsTask()); + if (language.getStructure() != null) { + // Language#hasStructureScanner has a broken/unusable + // implementation, so support is deduced from the (non-)presence + // of a structure scanner. + return List.of(new BreadCrumbsTask()); + } else { + return List.of(); + } } } diff --git a/ide/lsp.client/licenseinfo.xml b/ide/lsp.client/licenseinfo.xml new file mode 100644 index 000000000000..63d1496bfb22 --- /dev/null +++ b/ide/lsp.client/licenseinfo.xml @@ -0,0 +1,56 @@ + + + + + src/org/netbeans/modules/lsp/client/bindings/icons/attribute.png + src/org/netbeans/modules/lsp/client/bindings/icons/class.png + src/org/netbeans/modules/lsp/client/bindings/icons/color.gif + src/org/netbeans/modules/lsp/client/bindings/icons/constant.png + src/org/netbeans/modules/lsp/client/bindings/icons/constructor.png + src/org/netbeans/modules/lsp/client/bindings/icons/empty.png + src/org/netbeans/modules/lsp/client/bindings/icons/enummember.png + src/org/netbeans/modules/lsp/client/bindings/icons/enum.png + src/org/netbeans/modules/lsp/client/bindings/icons/event.png + src/org/netbeans/modules/lsp/client/bindings/icons/field.png + src/org/netbeans/modules/lsp/client/bindings/icons/file.png + src/org/netbeans/modules/lsp/client/bindings/icons/folder.gif + src/org/netbeans/modules/lsp/client/bindings/icons/function.png + src/org/netbeans/modules/lsp/client/bindings/icons/interface.png + src/org/netbeans/modules/lsp/client/bindings/icons/keyword.png + src/org/netbeans/modules/lsp/client/bindings/icons/method.png + src/org/netbeans/modules/lsp/client/bindings/icons/module.png + src/org/netbeans/modules/lsp/client/bindings/icons/operator.png + src/org/netbeans/modules/lsp/client/bindings/icons/property.png + src/org/netbeans/modules/lsp/client/bindings/icons/reference.png + src/org/netbeans/modules/lsp/client/bindings/icons/snippet.png + src/org/netbeans/modules/lsp/client/bindings/icons/struct.png + src/org/netbeans/modules/lsp/client/bindings/icons/text.png + src/org/netbeans/modules/lsp/client/bindings/icons/typeparameter.gif + src/org/netbeans/modules/lsp/client/bindings/icons/unit.png + src/org/netbeans/modules/lsp/client/bindings/icons/value.png + src/org/netbeans/modules/lsp/client/bindings/icons/variable.gif + src/org/netbeans/modules/lsp/client/resources/error_16.png + src/org/netbeans/modules/lsp/client/resources/warning_16.png + + + + diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java index 58cd0a995c0e..57509cded5f2 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java @@ -90,7 +90,7 @@ public void run(List servers, FileObject file) { capa -> Utils.isEnabled(capa.getDocumentSymbolProvider()), () -> new DocumentSymbolParams(new TextDocumentIdentifier(Utils.toURI(file))), (server, params) -> server.getTextDocumentService().documentSymbol(params), - (server, result) -> allSymbols.add(Pair.of(server, result.stream().map(this::toDocumentSymbol).toList()))); + (server, result) -> allSymbols.add(Pair.of(server, result == null ? List.of() : result.stream().map(this::toDocumentSymbol).toList()))); this.rootElement = new RootBreadcrumbsElementImpl(file, doc, allSymbols); diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CodeActions.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CodeActions.java index 6faf6676a1ba..2f5281fa84c1 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CodeActions.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CodeActions.java @@ -22,10 +22,13 @@ import java.util.Collections; import java.util.List; import javax.swing.text.JTextComponent; +import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionContext; import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.netbeans.api.editor.mimelookup.MimeRegistration; import org.netbeans.modules.editor.NbEditorUtilities; import org.netbeans.modules.lsp.client.LSPBindings; @@ -63,17 +66,23 @@ public List create(Lookup context) { Utils.createPosition(component.getDocument(), component.getSelectionEnd())), new CodeActionContext(Collections.emptyList())), (server, params) -> server.getTextDocumentService().codeAction(params), - (server, result) -> result.forEach(cmd -> output.add(new CodeGenerator() { - @Override - public String getDisplayName() { - return cmd.isLeft() ? cmd.getLeft().getTitle() : cmd.getRight().getTitle(); - } + (server, result) -> { + if (result != null) { + for (Either cmd : result) { + output.add(new CodeGenerator() { + @Override + public String getDisplayName() { + return cmd.isLeft() ? cmd.getLeft().getTitle() : cmd.getRight().getTitle(); + } - @Override - public void invoke() { - Utils.applyCodeAction(server, cmd); - } - }))); + @Override + public void invoke() { + Utils.applyCodeAction(server, cmd); + } + }); + } + } + }); return output; } diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Formatter.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Formatter.java index e941b3e8b296..f68adfcc619e 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Formatter.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Formatter.java @@ -119,7 +119,10 @@ private void documentFormat(FileObject fo, LSPBindings bindings) throws BadLocat IndentUtils.isExpandTabs(ctx.document()))); List edits = new ArrayList<>(); try { - edits.addAll(bindings.getTextDocumentService().formatting(dfp).get()); + List editInputs = bindings.getTextDocumentService().formatting(dfp).get(); + if (editInputs != null) { + edits.addAll(editInputs); + } } catch (InterruptedException | ExecutionException ex) { LOG.log(Level.INFO, String.format("LSP document format failed for {0}", fo), diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LanguageClientImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LanguageClientImpl.java index 57df7f5b33ea..410fe835fcb3 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LanguageClientImpl.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LanguageClientImpl.java @@ -65,6 +65,7 @@ import org.eclipse.lsp4j.jsonrpc.Endpoint; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageClient; +import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.api.progress.*; import org.netbeans.modules.lsp.client.LSPBindings; import org.netbeans.modules.lsp.client.Utils; @@ -98,6 +99,11 @@ public class LanguageClientImpl implements LanguageClient, Endpoint { private static final Logger LOG = Logger.getLogger(LanguageClientImpl.class.getName()); private static final RequestProcessor WORKER = new RequestProcessor(LanguageClientImpl.class.getName(), 1, false, false); + @StaticResource + private static final String ERROR_ICON = "org/netbeans/modules/lsp/client/resources/error_16.png"; + @StaticResource + private static final String WARNING_ICON = "org/netbeans/modules/lsp/client/resources/warning_16.png"; + private LSPBindings bindings; private boolean allowCodeActions; @@ -161,24 +167,26 @@ public void notifyProgress(ProgressParams params) { }); } - @Override + @Override public void publishDiagnostics(PublishDiagnosticsParams pdp) { - try { - FileObject file = URLMapper.findFileObject(new URI(pdp.getUri()).toURL()); - EditorCookie ec = file != null ? file.getLookup().lookup(EditorCookie.class) : null; - Document doc = ec != null ? ec.getDocument() : null; - if (doc == null) { - return ; //ignore... + WORKER.execute(() -> { + try { + FileObject file = URLMapper.findFileObject(new URI(pdp.getUri()).toURL()); + EditorCookie ec = file != null ? file.getLookup().lookup(EditorCookie.class) : null; + Document doc = ec != null ? ec.getDocument() : null; + if (doc == null) { + return; //ignore... + } + assert file != null; + List diags = pdp.getDiagnostics().stream().map(d -> { + LazyFixList fixList = allowCodeActions ? new DiagnosticFixList(doc, pdp.getUri(), d) : ErrorDescriptionFactory.lazyListForFixes(Collections.emptyList()); + return ErrorDescriptionFactory.createErrorDescription(severityMap.get(d.getSeverity()), d.getMessage(), fixList, file, Utils.getOffset(doc, d.getRange().getStart()), Utils.getOffset(doc, d.getRange().getEnd())); + }).collect(Collectors.toList()); + HintsController.setErrors(doc, LanguageClientImpl.class.getName(), diags); + } catch (URISyntaxException | MalformedURLException ex) { + LOG.log(Level.FINE, null, ex); } - assert file != null; - List diags = pdp.getDiagnostics().stream().map(d -> { - LazyFixList fixList = allowCodeActions ? new DiagnosticFixList(doc, pdp.getUri(), d) : ErrorDescriptionFactory.lazyListForFixes(Collections.emptyList()); - return ErrorDescriptionFactory.createErrorDescription(severityMap.get(d.getSeverity()), d.getMessage(), fixList, file, Utils.getOffset(doc, d.getRange().getStart()), Utils.getOffset(doc, d.getRange().getEnd())); - }).collect(Collectors.toList()); - HintsController.setErrors(doc, LanguageClientImpl.class.getName(), diags); - } catch (URISyntaxException | MalformedURLException ex) { - LOG.log(Level.FINE, null, ex); - } + }); } private static final Map severityMap = new EnumMap<>(DiagnosticSeverity.class); @@ -211,11 +219,11 @@ public void showMessage(MessageParams params) { StatusDisplayer.getDefault().setStatusText(params.getMessage()); return ; case Warning: - icon = ImageUtilities.loadImageIcon("org/netbeans/modules/lsp/client/resources/warning.png", false); + icon = ImageUtilities.loadImageIcon(WARNING_ICON, false); category = Category.WARNING; break; case Error: - icon = ImageUtilities.loadImageIcon("org/netbeans/modules/lsp/client/resources/error_16.png", false); + icon = ImageUtilities.loadImageIcon(ERROR_ICON, false); category = Category.ERROR; break; } @@ -305,6 +313,12 @@ public void notify(String method, Object parameter) { LOG.log(Level.WARNING, "Received unhandled notification: {0}: {1}", new Object[] {method, parameter}); } + @Override + public CompletableFuture refreshDiagnostics() { + bindings.getOpenedFiles().forEach(LSPBindings::scheduleBackgroundTasks); + return CompletableFuture.completedFuture(null); + } + private final class DiagnosticFixList implements LazyFixList { private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java index 88cd6a65d9d8..ebc6493dc45f 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java @@ -74,7 +74,7 @@ public void run(FileObject file) { String uri = Utils.toURI(file); List> symbols = bindings.getTextDocumentService().documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(uri))).get(); - setKeys(symbols); + setKeys(symbols == null ? List.of() : symbols); expandAll(); } catch (ExecutionException ex) { LOG.log(Level.FINE, null, ex); diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/SemanticHighlight.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/SemanticHighlight.java index 2d2b7022d88b..a461eb1ac159 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/SemanticHighlight.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/SemanticHighlight.java @@ -81,7 +81,12 @@ private void convertTokenHighlights(LSPBindings server, FileObject file, FontColorSettings[] fcs, OffsetsBag target) { - List data = tokens.getData(); + List data; + if (tokens != null) { + data = tokens.getData(); + } else { + data = List.of(); + } int lastLine = 0; int lastColumn = 0; int offset = 0; diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/refactoring/Refactoring.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/refactoring/Refactoring.java index 6c521df6e630..56c2f2acbafb 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/refactoring/Refactoring.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/refactoring/Refactoring.java @@ -120,32 +120,34 @@ public Problem fastCheckParameters() { @Override public Problem prepare(RefactoringElementsBag refactoringElements) { BiConsumer> handleResult = (server, usages) -> { - for (Location l : usages) { - if (isCanceled()) { - break; - } - FileObject file = Utils.fromURI(l.getUri()); - if (file != null) { - PositionBounds bounds; - try { - CloneableEditorSupport es = file.getLookup().lookup(CloneableEditorSupport.class); - EditorCookie ec = file.getLookup().lookup(EditorCookie.class); - StyledDocument doc = ec.openDocument(); - - bounds = new PositionBounds(es.createPositionRef(Utils.getOffset(doc, l.getRange().getStart()), Position.Bias.Forward), - es.createPositionRef(Utils.getOffset(doc, l.getRange().getEnd()), Position.Bias.Forward)); - } catch (IOException ex) { - Exceptions.printStackTrace(ex); - bounds = null; + if (usages != null) { + for (Location l : usages) { + if (isCanceled()) { + break; } - LineCookie lc = file.getLookup().lookup(LineCookie.class); - Line startLine = lc.getLineSet().getCurrent(l.getRange().getStart().getLine()); - String lineText = startLine.getText(); - int highlightEnd = Math.min(lineText.length(), l.getRange().getEnd().getCharacter()); - String annotatedLine = lineText.substring(0, l.getRange().getStart().getCharacter()) + + FileObject file = Utils.fromURI(l.getUri()); + if (file != null) { + PositionBounds bounds; + try { + CloneableEditorSupport es = file.getLookup().lookup(CloneableEditorSupport.class); + EditorCookie ec = file.getLookup().lookup(EditorCookie.class); + StyledDocument doc = ec.openDocument(); + + bounds = new PositionBounds(es.createPositionRef(Utils.getOffset(doc, l.getRange().getStart()), Position.Bias.Forward), + es.createPositionRef(Utils.getOffset(doc, l.getRange().getEnd()), Position.Bias.Forward)); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + bounds = null; + } + LineCookie lc = file.getLookup().lookup(LineCookie.class); + Line startLine = lc.getLineSet().getCurrent(l.getRange().getStart().getLine()); + String lineText = startLine.getText(); + int highlightEnd = Math.min(lineText.length(), l.getRange().getEnd().getCharacter()); + String annotatedLine = lineText.substring(0, l.getRange().getStart().getCharacter()) + "" + lineText.substring(l.getRange().getStart().getCharacter(), highlightEnd) + "" + lineText.substring(highlightEnd); - refactoringElements.add(query, new LSPRefactoringElementImpl(annotatedLine, file, bounds)); + refactoringElements.add(query, new LSPRefactoringElementImpl(annotatedLine, file, bounds)); + } } } }; @@ -191,110 +193,112 @@ public Problem fastCheckParameters() { @Override public Problem prepare(RefactoringElementsBag refactoringElements) { BiConsumer handleResult = (server, edit) -> { - try { - List> documentChanges = edit.getDocumentChanges(); - ModificationResult result = new ModificationResult(); - Map> file2Diffs = new HashMap<>(); - Map newURI2Old = new HashMap<>(); - Map newFileURI2Content = new HashMap<>(); - - if (documentChanges != null) { - for (Either part : documentChanges) { - if (isCanceled()) { - break; - } - if (part.isLeft()) { - String uri = part.getLeft().getTextDocument().getUri(); - uri = newURI2Old.getOrDefault(uri, uri); - FileObject file = Utils.fromURI(uri); - - if (file != null) { - for (TextEdit te : part.getLeft().getEdits()) { - Difference diff = textEdit2Difference(file, te); - file2Diffs.computeIfAbsent(file, f -> new ArrayList<>()) - .add(diff); - } - } else if (newFileURI2Content.containsKey(uri)) { - FileObject temp = FileUtil.createMemoryFileSystem().getRoot().createData("temp.txt"); - try (OutputStream out = temp.getOutputStream()) { - out.write(newFileURI2Content.get(uri).getBytes()); //TODO: encoding - native, OK? - } - List diffs = new ArrayList<>(); - for (TextEdit te : part.getLeft().getEdits()) { - diffs.add(textEdit2Difference(temp, te)); - } - ModificationResult tempResult = new ModificationResult(); - tempResult.addDifferences(temp, diffs); - newFileURI2Content.put(uri, tempResult.getResultingSource(temp)); - } else { - //XXX: problem... + if(edit != null) { + try { + List> documentChanges = edit.getDocumentChanges(); + ModificationResult result = new ModificationResult(); + Map> file2Diffs = new HashMap<>(); + Map newURI2Old = new HashMap<>(); + Map newFileURI2Content = new HashMap<>(); + + if (documentChanges != null) { + for (Either part : documentChanges) { + if (isCanceled()) { + break; } - } else { - switch (part.getRight().getKind()) { - case ResourceOperationKind.Rename: { - RenameFile rename = (RenameFile) part.getRight(); - FileObject file = Utils.fromURI(rename.getOldUri()); - refactoringElements.addFileChange(refactoring, new LSPRenameFile(file, rename.getNewUri())); - newURI2Old.put(rename.getNewUri(), rename.getOldUri()); - break; - } - case ResourceOperationKind.Delete: { - DeleteFile delete = (DeleteFile) part.getRight(); - FileObject file = Utils.fromURI(delete.getUri()); - refactoringElements.addFileChange(refactoring, new LSPDeleteFile(file)); - break; + if (part.isLeft()) { + String uri = part.getLeft().getTextDocument().getUri(); + uri = newURI2Old.getOrDefault(uri, uri); + FileObject file = Utils.fromURI(uri); + + if (file != null) { + for (TextEdit te : part.getLeft().getEdits()) { + Difference diff = textEdit2Difference(file, te); + file2Diffs.computeIfAbsent(file, f -> new ArrayList<>()) + .add(diff); + } + } else if (newFileURI2Content.containsKey(uri)) { + FileObject temp = FileUtil.createMemoryFileSystem().getRoot().createData("temp.txt"); + try (OutputStream out = temp.getOutputStream()) { + out.write(newFileURI2Content.get(uri).getBytes()); //TODO: encoding - native, OK? + } + List diffs = new ArrayList<>(); + for (TextEdit te : part.getLeft().getEdits()) { + diffs.add(textEdit2Difference(temp, te)); + } + ModificationResult tempResult = new ModificationResult(); + tempResult.addDifferences(temp, diffs); + newFileURI2Content.put(uri, tempResult.getResultingSource(temp)); + } else { + //XXX: problem... } - case ResourceOperationKind.Create: { - CreateFile create = (CreateFile) part.getRight(); - String uri = create.getUri(); - newFileURI2Content.put(uri, ""); - break; + } else { + switch (part.getRight().getKind()) { + case ResourceOperationKind.Rename: { + RenameFile rename = (RenameFile) part.getRight(); + FileObject file = Utils.fromURI(rename.getOldUri()); + refactoringElements.addFileChange(refactoring, new LSPRenameFile(file, rename.getNewUri())); + newURI2Old.put(rename.getNewUri(), rename.getOldUri()); + break; + } + case ResourceOperationKind.Delete: { + DeleteFile delete = (DeleteFile) part.getRight(); + FileObject file = Utils.fromURI(delete.getUri()); + refactoringElements.addFileChange(refactoring, new LSPDeleteFile(file)); + break; + } + case ResourceOperationKind.Create: { + CreateFile create = (CreateFile) part.getRight(); + String uri = create.getUri(); + newFileURI2Content.put(uri, ""); + break; + } + default: + addProblem(new Problem(true, "Unknown file operation: " + part.getRight().getKind())); + break; } - default: - addProblem(new Problem(true, "Unknown file operation: " + part.getRight().getKind())); - break; } } - } - } else { - for (Entry> fileAndChanges : edit.getChanges().entrySet()) { - if (isCanceled()) { - break; - } - //TODO: errors: - FileObject file = Utils.fromURI(fileAndChanges.getKey()); + } else { + for (Entry> fileAndChanges : edit.getChanges().entrySet()) { + if (isCanceled()) { + break; + } + //TODO: errors: + FileObject file = Utils.fromURI(fileAndChanges.getKey()); - for (TextEdit te : fileAndChanges.getValue()) { - Difference diff = textEdit2Difference(file, te); - file2Diffs.computeIfAbsent(file, f -> new ArrayList<>()) - .add(diff); + for (TextEdit te : fileAndChanges.getValue()) { + Difference diff = textEdit2Difference(file, te); + file2Diffs.computeIfAbsent(file, f -> new ArrayList<>()) + .add(diff); + } } } - } - if (isCanceled()) { - addProblem(new Problem(false, Bundle.TXT_Canceled())); - } else { + if (isCanceled()) { + addProblem(new Problem(false, Bundle.TXT_Canceled())); + } else { - file2Diffs.entrySet() - .forEach(e -> { - e.getValue() - .forEach(diff -> refactoringElements.add(refactoring, DiffElement.create(diff, e.getKey(), result))); - result.addDifferences(e.getKey(), e.getValue()); - }); + file2Diffs.entrySet() + .forEach(e -> { + e.getValue() + .forEach(diff -> refactoringElements.add(refactoring, DiffElement.create(diff, e.getKey(), result))); + result.addDifferences(e.getKey(), e.getValue()); + }); - newFileURI2Content.entrySet() - .forEach(e -> { - refactoringElements.add(refactoring, new LSPCreateFile(e.getKey(), e.getValue())); - }); - refactoringElements.registerTransaction(new RefactoringCommit(Collections.singletonList(result))); + newFileURI2Content.entrySet() + .forEach(e -> { + refactoringElements.add(refactoring, new LSPCreateFile(e.getKey(), e.getValue())); + }); + refactoringElements.registerTransaction(new RefactoringCommit(Collections.singletonList(result))); - if (isCanceled()) { - addProblem(new Problem(false, Bundle.TXT_Canceled())); + if (isCanceled()) { + addProblem(new Problem(false, Bundle.TXT_Canceled())); + } } + } catch ( IOException ex) { + addProblem(new Problem(true, ex.getLocalizedMessage())); } - } catch ( IOException ex) { - addProblem(new Problem(true, ex.getLocalizedMessage())); } }; diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/resources/warning_16.png b/ide/lsp.client/src/org/netbeans/modules/lsp/client/resources/warning_16.png new file mode 100644 index 000000000000..42c67a1901f1 Binary files /dev/null and b/ide/lsp.client/src/org/netbeans/modules/lsp/client/resources/warning_16.png differ diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/resources/warning_16.svg b/ide/lsp.client/src/org/netbeans/modules/lsp/client/resources/warning_16.svg new file mode 100644 index 000000000000..e5932382e489 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/resources/warning_16.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/rust/rust.cargo/nbproject/project.xml b/rust/rust.cargo/nbproject/project.xml index 693c95386684..045e96136c1f 100644 --- a/rust/rust.cargo/nbproject/project.xml +++ b/rust/rust.cargo/nbproject/project.xml @@ -51,12 +51,12 @@ - org.netbeans.libs.tomlj + org.netbeans.libs.tomljava - 1 - 1.2 + 5 + 4.0 diff --git a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoTOMLParser.java b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoTOMLParser.java index aadfaeba4bdd..4241e4ba5882 100644 --- a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoTOMLParser.java +++ b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/CargoTOMLParser.java @@ -18,12 +18,9 @@ */ package org.netbeans.modules.rust.cargo.impl; -import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -31,16 +28,12 @@ import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; +import net.vieiro.toml.TOML; +import net.vieiro.toml.TOMLParser; import org.netbeans.modules.rust.cargo.api.CargoTOML; import org.netbeans.modules.rust.cargo.api.RustPackage; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; -import org.tomlj.Toml; -import org.tomlj.TomlArray; -import org.tomlj.TomlParseError; -import org.tomlj.TomlParseResult; -import org.tomlj.TomlTable; /** * @@ -63,24 +56,23 @@ public static void parseCargoToml(FileObject cargoTomlFile, CargoTOML cargotoml) } long start = System.currentTimeMillis(); // As per the specification, .toml files are always UTF-8 - try (final BufferedReader fileContent = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { - TomlParseResult parseResult = Toml.parse(fileContent); - List errors = parseResult.errors().stream().collect(Collectors.toList()); - if (!errors.isEmpty()) { + try (InputStream cargoInputStream = cargoTomlFile.getInputStream()) { + TOML parseResult = TOMLParser.parseFromInputStream(cargoInputStream); + if (!parseResult.getErrors().isEmpty()) { final String fileName = file.getAbsolutePath(); - errors.forEach(e -> { - LOG.warning(String.format("Error parsing '%s': '%s'", fileName, e.getMessage())); // NOI18N + parseResult.getErrors().forEach(e -> { + LOG.warning(String.format("Error parsing '%s': '%s'", fileName, e)); // NOI18N }); throw new IOException(String.format("Errors parsing '%s'. See log for details", fileName)); // NOI18N } - String packageName = parseResult.getString("package.name"); // NOI18N - String version = parseResult.getString("package.version"); // NOI18N - String edition = parseResult.getString("package.edition"); // NOI18N + String packageName = parseResult.getString("package/name").orElse(null); // NOI18N + String version = parseResult.getString("package/version").orElse(null); // NOI18N + String edition = parseResult.getString("package/edition").orElse(null); // NOI18N edition = edition == null ? "2015" : edition; - String rustVersion = parseResult.getString("package.rust-version"); // NOI18N - String description = parseResult.getString("package.description"); // NOI18N - String documentation = parseResult.getString("package.documentation"); // NOI18N - String homepage = parseResult.getString("package.homepage"); + String rustVersion = parseResult.getString("package/rust-version").orElse(null); // NOI18N + String description = parseResult.getString("package/description").orElse(null); // NOI18N + String documentation = parseResult.getString("package/documentation").orElse(null); // NOI18N + String homepage = parseResult.getString("package/homepage").orElse(null); // TODO: Read more stuff... // TODO: Fire property change only if required. @@ -111,17 +103,18 @@ public static void parseCargoToml(FileObject cargoTomlFile, CargoTOML cargotoml) } // Workspaces https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html - TomlArray members = parseResult.getArray("workspace.members"); + @SuppressWarnings("unchecked") + List members = parseResult.getArray("workspace/members").orElse(null); if (members != null) { TreeMap workspacesByName = new TreeMap<>(); FileObject root = cargotoml.getFileObject().getParent(); for (int i = 0; i < members.size(); i++) { - String memberName = members.getString(i); + String memberName = members.get(i); FileObject memberCargoFO = root.getFileObject(memberName); if (memberCargoFO != null) { memberCargoFO = memberCargoFO.getFileObject("Cargo.toml"); // NOI18N } - if (memberCargoFO != null) { + if (memberCargoFO != null && ! cargoTomlFile.equals(memberCargoFO)) { CargoTOML memberCargo = new CargoTOML(memberCargoFO); workspacesByName.put(memberName, memberCargo); } @@ -135,8 +128,8 @@ public static void parseCargoToml(FileObject cargoTomlFile, CargoTOML cargotoml) LOG.info(String.format("Parsing '%s' took %5.2g ms.", file.getAbsolutePath(), (end - start) / 1000.0)); //NOI18N } - private static final List getDependencies(CargoTOML cargotoml, TomlParseResult parseResult, String propertyKey) { - TomlTable dependencies = parseResult.getTable(propertyKey); + private static List getDependencies(CargoTOML cargotoml, TOML parseResult, String propertyKey) { + Map dependencies = parseResult.getTable(propertyKey).orElse(null); if (dependencies == null || dependencies.isEmpty()) { return Collections.emptyList(); } @@ -147,16 +140,17 @@ private static final List getDependencies(CargoTOML cargotoml, Toml if (value instanceof String) { String stringValue = (String) value; packages.add(RustPackage.withNameAndVersion(cargotoml, key, stringValue)); - } else if (value instanceof TomlTable) { + } else if (value instanceof Map) { String dependencyName = key.replaceAll("^dependencies\\.", ""); // NOI18N - TomlTable dependencyInfo = (TomlTable) value; - String version = dependencyInfo.getString("version"); // NOI18N - String git = dependencyInfo.getString("git"); // NOI18N + @SuppressWarnings("unchecked") + Map dependencyInfo = (Map) value; + String version = (String) dependencyInfo.get("version"); // NOI18N + String git = (String) dependencyInfo.get("git"); // NOI18N if (version != null) { // Examples: // [dependencies] // some-crate = { version = "1.0", registry = "my-registry" } - Boolean optional = dependencyInfo.getBoolean("optional"); // NOI18N + Boolean optional = (Boolean) dependencyInfo.get("optional"); // NOI18N RustPackage rp = RustPackage.withNameAndVersion(cargotoml, dependencyName, version, optional); packages.add(rp); } else if (git != null) { @@ -164,7 +158,7 @@ private static final List getDependencies(CargoTOML cargotoml, Toml // [dependencies] // regex = { git = "https://github.com/rust-lang/regex" } // regex = { git = "https://github.com/rust-lang/regex", branch = "next" } - String branch = dependencyInfo.getString("branch"); + String branch = (String) dependencyInfo.get("branch"); RustPackage rp = RustPackage.withGit(cargotoml, dependencyName, git, branch); packages.add(rp); } diff --git a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyAction.java b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyAction.java index 0e6192424498..c31d85d04f06 100644 --- a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyAction.java +++ b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyAction.java @@ -104,6 +104,7 @@ public void actionPerformed(ActionEvent e) { break; } if (DialogDisplayer.getDefault().notify(wiz) == WizardDescriptor.FINISH_OPTION) { + @SuppressWarnings("unchecked") List packages = (List) wiz.getProperty(RustAddDependencyWizardPanel1.PROP_SELECTED_PACKAGES); List names = packages.stream().map((p) -> String.format("%s@%s", p.getName(), p.getVersion())).collect(Collectors.toList()); switch(dependencyType) { diff --git a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyWizardPanel1.java b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyWizardPanel1.java index 788f63af0d5d..e5adc7f0e0ca 100644 --- a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyWizardPanel1.java +++ b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustAddDependencyWizardPanel1.java @@ -42,6 +42,7 @@ public class RustAddDependencyWizardPanel1 implements WizardDescriptor.Panel packages = (List) wiz.getProperty(PROP_PACKAGES); component.setPackages(packages); } diff --git a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustRemoveDependencyAction.java b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustRemoveDependencyAction.java index ecbcd0652d2c..dd8d161838ab 100644 --- a/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustRemoveDependencyAction.java +++ b/rust/rust.cargo/src/org/netbeans/modules/rust/cargo/impl/nodes/actions/dependencies/RustRemoveDependencyAction.java @@ -42,6 +42,7 @@ public class RustRemoveDependencyAction extends AbstractAction { private final RustPackage rustPackage; private final RustProjectDependenciesNode.DependencyType dependencyType; + @SuppressWarnings({"this-escape"}) public RustRemoveDependencyAction(CargoTOML cargotoml, RustPackage rustPackage, RustProjectDependenciesNode.DependencyType dependencyType) { super(CargoCommand.CARGO_REMOVE.getDisplayName()); putValue(Action.SHORT_DESCRIPTION, CargoCommand.CARGO_REMOVE.getDescription()); diff --git a/rust/rust.grammar/licenseinfo.xml b/rust/rust.grammar/licenseinfo.xml index 4852540b3791..4741b1647e36 100644 --- a/rust/rust.grammar/licenseinfo.xml +++ b/rust/rust.grammar/licenseinfo.xml @@ -20,23 +20,14 @@ --> - - - src/org/netbeans/modules/rust/grammar/structure/resources/structure-macro.png - src/org/netbeans/modules/rust/grammar/structure/resources/structure-enum.png - src/org/netbeans/modules/rust/grammar/structure/resources/structure-struct.png - src/org/netbeans/modules/rust/grammar/structure/resources/structure-impl.png - src/org/netbeans/modules/rust/grammar/structure/resources/structure-function.png - src/org/netbeans/modules/rust/grammar/structure/resources/structure-module.png + src/org/netbeans/modules/rust/grammar/structure/resources/enum.png + src/org/netbeans/modules/rust/grammar/structure/resources/function.png + src/org/netbeans/modules/rust/grammar/structure/resources/interface.png + src/org/netbeans/modules/rust/grammar/structure/resources/module.png + src/org/netbeans/modules/rust/grammar/structure/resources/snippet.png + src/org/netbeans/modules/rust/grammar/structure/resources/struct.png + src/org/netbeans/modules/rust/grammar/structure/resources/trait.png diff --git a/rust/rust.grammar/nbproject/project.properties b/rust/rust.grammar/nbproject/project.properties index 2ad5789cf71f..0c0c4ac904e0 100644 --- a/rust/rust.grammar/nbproject/project.properties +++ b/rust/rust.grammar/nbproject/project.properties @@ -15,6 +15,6 @@ # specific language governing permissions and limitations # under the License. -javac.source=1.8 +javac.release=21 javac.compilerargs=-Xlint -Xlint:-serial build.generated.sources.dir=${build.dir}/generated-sources diff --git a/rust/rust.grammar/nbproject/project.xml b/rust/rust.grammar/nbproject/project.xml index 3a8db0587598..a3837bcdf62e 100644 --- a/rust/rust.grammar/nbproject/project.xml +++ b/rust/rust.grammar/nbproject/project.xml @@ -61,6 +61,15 @@ 1.20 + + org.netbeans.modules.editor + + + + 3 + 1.119 + + org.netbeans.modules.editor.fold @@ -70,6 +79,15 @@ 1.65 + + org.netbeans.modules.editor.lib2 + + + + 1 + 2.52 + + org.netbeans.modules.editor.mimelookup @@ -96,6 +114,24 @@ 1.1 + + org.netbeans.modules.lsp.client + + + + 0 + 1.24 + + + + org.netbeans.modules.options.api + + + + 1 + 1.77 + + org.netbeans.modules.parsing.api @@ -105,6 +141,23 @@ 9.27 + + org.netbeans.modules.rust.options + + + + 1.11 + + + + org.netbeans.spi.editor.hints + + + + 0 + 1.74 + + org.openide.filesystems @@ -113,6 +166,14 @@ 9.33 + + org.openide.modules + + + + 7.79 + + org.openide.util diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/LayerProviderImpl.java b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/LayerProviderImpl.java new file mode 100644 index 000000000000..30b4d534b0d8 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/LayerProviderImpl.java @@ -0,0 +1,67 @@ +/* + * 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.rust.grammar; + +import java.net.URL; +import java.util.Collection; +import java.util.logging.Logger; +import org.netbeans.modules.rust.options.api.RustAnalyzerOptions; +import org.openide.filesystems.Repository.LayerProvider; +import org.openide.util.lookup.ServiceProvider; + +/** + * The rust.grammer module can provide services directly or using rust-analyzer + * (the rust lsp). If the user configured rust-analyzer, that should get + * priority as it also can provide code completion and improved structure + * scanning. + * + * For this to work, the normal NetBeans behavior to generate layer entries from + * annotations (MimeRegistration of LSP, LanguageRegistration) has to be + * customized as the two options can't coexist completely. + * + * Therefore the layer entries from org.netbeans.modules.rust.grammar.RustLanguageConfig + * and org.netbeans.modules.rust.grammar.lsp.RustLSP are removed from the + * generated-layer.xml (build/classes/META-INF/generated-layer.xml), the + * annotations are commented out and the entries from the generated layer are + * checked whether they were created by one of the mentioned classes (see XML + * comments for reference) and are distributed into: + * + * - layer.xml holds everything that applies to both LSP and non-LSP mode + * - lsp-layer.xml holds everything specific to the LSP + * - non-lsp-layer.xml hold everything specific to the non-LSP run + * + * `layer.xml` is loaded by the module system unconditionally. The two variants + * `lsp-layer.xml` and `non-lsp-layer.xml` are loaded using this class. + */ +@ServiceProvider(service = LayerProvider.class) +public class LayerProviderImpl extends LayerProvider { + + private static final Logger LOG = Logger.getLogger(LayerProviderImpl.class.getName()); + + @Override + protected void registerLayers(Collection context) { + URL layerUrl; + if (RustAnalyzerOptions.getRustAnalyzerLocation(false, false) != null) { + layerUrl = LayerProviderImpl.class.getResource("lsp-layer.xml"); + } else { + layerUrl = LayerProviderImpl.class.getResource("non-lsp-layer.xml"); + } + context.add(layerUrl); + } +} diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/RustLanguageConfig.java b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/RustLanguageConfig.java index e8b6cf564901..07664647883a 100644 --- a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/RustLanguageConfig.java +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/RustLanguageConfig.java @@ -23,13 +23,13 @@ import org.netbeans.modules.csl.api.DeclarationFinder; import org.netbeans.modules.csl.api.StructureScanner; import org.netbeans.modules.csl.spi.DefaultLanguageConfig; -import org.netbeans.modules.csl.spi.LanguageRegistration; import org.netbeans.modules.parsing.spi.Parser; +import org.netbeans.modules.rust.options.api.RustAnalyzerOptions; -/** - * - */ -@LanguageRegistration(mimeType = "text/x-rust", useMultiview = true) +// See LayerProviderImpl for information about layer handling and when this +// needs to be uncommented +// +//@LanguageRegistration(mimeType = "text/x-rust", useMultiview = true) public class RustLanguageConfig extends DefaultLanguageConfig { private static final Language RUST_LANGUAGE = new RustLanguage().language(); @@ -46,12 +46,12 @@ public String getDisplayName() { @Override public StructureScanner getStructureScanner() { - return new RustStructureScanner(); + return RustAnalyzerOptions.getRustAnalyzerLocation(false, false) == null ? new RustStructureScanner() : null; } @Override public boolean hasStructureScanner() { - return true; + return RustAnalyzerOptions.getRustAnalyzerLocation(false, false) == null; } @Override @@ -66,7 +66,7 @@ public String getLineCommentPrefix() { @Override public DeclarationFinder getDeclarationFinder() { - return new RustDeclarationFinder(); + return RustAnalyzerOptions.getRustAnalyzerLocation(false, false) == null ? new RustDeclarationFinder() : null; } } diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/layer.xml b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/layer.xml index 0fb1e98858ca..353bc8cc918a 100644 --- a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/layer.xml +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/layer.xml @@ -50,6 +50,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp-layer.xml b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp-layer.xml new file mode 100644 index 000000000000..c23e12d00189 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp-layer.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp/RustLSP.java b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp/RustLSP.java new file mode 100644 index 000000000000..0a8bdda40cb2 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp/RustLSP.java @@ -0,0 +1,55 @@ +/* + * 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.rust.grammar.lsp; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.modules.lsp.client.spi.LanguageServerProvider; +import org.netbeans.modules.rust.options.api.RustAnalyzerOptions; +import org.openide.util.Lookup; + +// See LayerProviderImpl for information about layer handling and when this +// needs to be uncommented +// +//@MimeRegistration(mimeType = "text/x-rust", service = LanguageServerProvider.class) +public class RustLSP implements LanguageServerProvider { + + private static final Logger LOG = Logger.getLogger(RustLSP.class.getName()); + + @Override + public LanguageServerDescription startServer(Lookup lookup) { + Path rustAnalyzerPath = RustAnalyzerOptions.getRustAnalyzerLocation(true, true); + if(rustAnalyzerPath == null || ! Files.isExecutable(rustAnalyzerPath)) { + return null; + } + try { + Process p = new ProcessBuilder(new String[]{rustAnalyzerPath.toAbsolutePath().toString()}) + .directory(rustAnalyzerPath.getParent().toFile()) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + return LanguageServerDescription.create(p.getInputStream(), p.getOutputStream(), p); + } catch (IOException ex) { + LOG.log(Level.FINE, null, ex); + return null; + } + } +} diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp/UnconfiguredHint.java b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp/UnconfiguredHint.java new file mode 100644 index 000000000000..dcfa157dd995 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/lsp/UnconfiguredHint.java @@ -0,0 +1,91 @@ +/* + * 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.rust.grammar.lsp; + +import java.beans.PropertyChangeEvent; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.netbeans.api.editor.EditorRegistry; +import org.netbeans.api.options.OptionsDisplayer; +import org.netbeans.modules.editor.NbEditorUtilities; +import org.netbeans.modules.rust.options.api.RustAnalyzerOptions; + +import org.netbeans.spi.editor.hints.ChangeInfo; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; +import org.netbeans.spi.editor.hints.Fix; +import org.netbeans.spi.editor.hints.HintsController; +import org.netbeans.spi.editor.hints.Severity; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.modules.OnStart; + + +@OnStart +public class UnconfiguredHint implements Runnable { + + private static final Set RUST_MIME_TYPES = Collections.singleton("text/x-rust"); + + @Override + public void run() { + EditorRegistry.addPropertyChangeListener((PropertyChangeEvent evt) -> { + updateFocused(); + }); + updateFocused(); + } + + private void updateFocused() { + JTextComponent selectedComponent = EditorRegistry.focusedComponent(); + if (selectedComponent == null) { + return; + } + Document doc = selectedComponent.getDocument(); + FileObject file = NbEditorUtilities.getFileObject(doc); + if (file == null || !RUST_MIME_TYPES.contains(FileUtil.getMIMEType(file))) { + return; + } + List errors = new ArrayList<>(); + Path rustLS = RustAnalyzerOptions.getRustAnalyzerLocation(true, false); + if (rustLS == null) { + errors.add(ErrorDescriptionFactory.createErrorDescription(Severity.WARNING, "For improved Rust support please install and configure rust-analyzer", Collections.singletonList(new ConfigureRustAnalyzer()), doc, 0)); + } + HintsController.setErrors(doc, UnconfiguredHint.class.getName(), errors); + } + + private static final class ConfigureRustAnalyzer implements Fix { + + @Override + public String getText() { + return "Configure rust-analyzer"; + } + + @Override + public ChangeInfo implement() throws Exception { + OptionsDisplayer.getDefault().open("Rust/RustAnalyzer"); + return null; + } + + } + +} diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/non-lsp-layer.xml b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/non-lsp-layer.xml new file mode 100644 index 000000000000..86985859b2a7 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/non-lsp-layer.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/RustStructureItem.java b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/RustStructureItem.java index 3a3c3ae138c9..d51b94ea5f31 100644 --- a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/RustStructureItem.java +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/RustStructureItem.java @@ -42,20 +42,21 @@ private static final String getIconBase(RustASTNode node) { switch (node.getKind()) { case CRATE: case ENUM: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-enum.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/enum.png"; // NOI18N case FUNCTION: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-function.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/function.png"; // NOI18N case STRUCT: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-struct.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/struct.png"; // NOI18N case IMPL: + return "org/netbeans/modules/rust/grammar/structure/resources/trait.png"; // NOI18N case TRAIT: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-impl.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/interface.png"; // NOI18N case MACRO: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-macro.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/snippet.png"; // NOI18N case MODULE: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-module.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/module.png"; // NOI18N default: - return "org/netbeans/modules/rust/grammar/structure/resources/structure-struct.png"; // NOI18N + return "org/netbeans/modules/rust/grammar/structure/resources/structure.png"; // NOI18N } } diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/enum.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/enum.png new file mode 100644 index 000000000000..f54c89b9cfb4 Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/enum.png differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/enum.svg b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/enum.svg new file mode 100644 index 000000000000..cd782acdb33b --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/enum.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/function.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/function.png new file mode 100644 index 000000000000..7be8a378a374 Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/function.png differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/function.svg b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/function.svg new file mode 100644 index 000000000000..3188ced38012 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/function.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/interface.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/interface.png new file mode 100644 index 000000000000..ae582f77a865 Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/interface.png differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/interface.svg b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/interface.svg new file mode 100644 index 000000000000..670c9e62f565 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/interface.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/module.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/module.png new file mode 100644 index 000000000000..505f4b7f8123 Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/module.png differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/snippet.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/snippet.png new file mode 100644 index 000000000000..b56c19cee5d5 Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/snippet.png differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/snippet.svg b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/snippet.svg new file mode 100644 index 000000000000..92950a1d3a84 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/snippet.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/struct.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/struct.png new file mode 100644 index 000000000000..220e6f385e0a Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/struct.png differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/struct.svg b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/struct.svg new file mode 100644 index 000000000000..8c0a9dca7298 --- /dev/null +++ b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/struct.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-enum.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-enum.png deleted file mode 100644 index 624f23b9d000..000000000000 Binary files a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-enum.png and /dev/null differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-function.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-function.png deleted file mode 100644 index 9145c1c5ba5e..000000000000 Binary files a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-function.png and /dev/null differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-impl.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-impl.png deleted file mode 100644 index d3286da9a4a7..000000000000 Binary files a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-impl.png and /dev/null differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-macro.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-macro.png deleted file mode 100644 index ecfce237e375..000000000000 Binary files a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-macro.png and /dev/null differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-module.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-module.png deleted file mode 100644 index 65dd9681ca96..000000000000 Binary files a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-module.png and /dev/null differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-struct.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-struct.png deleted file mode 100644 index d4b4000f1ed9..000000000000 Binary files a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/structure-struct.png and /dev/null differ diff --git a/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/trait.png b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/trait.png new file mode 100644 index 000000000000..9cc56d48e2dd Binary files /dev/null and b/rust/rust.grammar/src/org/netbeans/modules/rust/grammar/structure/resources/trait.png differ diff --git a/rust/rust.options/src/org/netbeans/modules/rust/options/api/RustAnalyzerOptions.java b/rust/rust.options/src/org/netbeans/modules/rust/options/api/RustAnalyzerOptions.java new file mode 100644 index 000000000000..60ef1f197ca5 --- /dev/null +++ b/rust/rust.options/src/org/netbeans/modules/rust/options/api/RustAnalyzerOptions.java @@ -0,0 +1,50 @@ +/* + * 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.rust.options.api; + +import java.nio.file.Path; +import org.netbeans.modules.rust.options.impl.RustAnalyzerOptionsImpl; + +/** + * Returns the options for rust-analyzer + */ +public final class RustAnalyzerOptions { + + /** + * Returns the Path where rust-analyzer is installed, or null. + * + * @param verifying If true then the path is checked for validity (the path + * exists and is executable) and if it is incorrect then a notification is + * shown to the user. + * @param showNotificationIfVerifyFail If true then a user notifiation is + * generated if rust-analyzer is not executeable + * @return The Path where rust-analyzer is installed, or null. + */ + public static final Path getRustAnalyzerLocation(boolean verifying, boolean showNotificationIfVerifyFail) { + return RustAnalyzerOptionsImpl.getRustAnalyzerLocation(verifying, true); + } + + /** + * Opens the rust-analyzer options panel. + */ + public static void showRustAnalyzerOptions() { + RustAnalyzerOptionsImpl.showRustAnalyzerOptions(); + } + +} diff --git a/rust/rust.options/src/org/netbeans/modules/rust/options/impl/RustAnalyzerOptionsImpl.java b/rust/rust.options/src/org/netbeans/modules/rust/options/impl/RustAnalyzerOptionsImpl.java new file mode 100644 index 000000000000..739be4768ca7 --- /dev/null +++ b/rust/rust.options/src/org/netbeans/modules/rust/options/impl/RustAnalyzerOptionsImpl.java @@ -0,0 +1,124 @@ +/* + * 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.rust.options.impl; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.prefs.Preferences; +import javax.swing.ImageIcon; +import javax.swing.SwingUtilities; +import org.netbeans.api.options.OptionsDisplayer; +import org.netbeans.modules.rust.project.api.RustProjectAPI; +import org.openide.awt.NotificationDisplayer; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; +import org.openide.util.NbPreferences; + +/** + * RustAnalyzerOptions implementation. + */ +public final class RustAnalyzerOptionsImpl { + + private static final String RUST_ANALYZER_LOCATION_KEY = "rust-analyzer-location"; // NOI18N + + private static Preferences getPreferences() { + return NbPreferences.forModule(RustAnalyzerOptionsImpl.class); + } + + /** + * Returns the full path to the "rust-analyzer" executable, or null if none + * exists. + * + * @param verify true to verify that the path is indeed executable + * @param showNotificationIfVerifyFail true to generate notification if rust analyzer is not executeable + * @return The full path to the rust-analyzer executable, or null. + */ + public static Path getRustAnalyzerLocation(boolean verify, boolean showNotificationIfVerifyFail) { + String rustAnalyzer = getPreferences().get(RUST_ANALYZER_LOCATION_KEY, null); + // Check if rust-analyzer is valid, if not then reset from preferences + if (rustAnalyzer != null && !rustAnalyzer.trim().isEmpty() && verify) { + File rustAnalyzerExecutable = new File(rustAnalyzer); + if (rustAnalyzerExecutable.canExecute()) { + return Paths.get(rustAnalyzer); + } + // Reset from prefernces + deleteRustAnalyzerLocation(); + rustAnalyzer = null; + // Warn the user if rust-analyzer cannot be found + if (showNotificationIfVerifyFail) { + showRustAnalyzerNotFoundNotification(); + } + } + return rustAnalyzer == null ? null : Paths.get(rustAnalyzer); + } + + /** + * Removes the previously saved rust-analyzer location. + */ + public static void deleteRustAnalyzerLocation() { + getPreferences().remove(RUST_ANALYZER_LOCATION_KEY); + } + + /** + * Sets a new rust-analyzer location. It is ignored if this is not a valid + * rust-analyzer location. + * + * @param location The location (possibly an absolute path). + */ + public static void setRustAnalyzerLocation(String location) { + if (location == null || location.trim().isEmpty()) { + deleteRustAnalyzerLocation(); + } else { + File rustAnalyzer = new File(location); + if (rustAnalyzer.canExecute()) { + getPreferences().put(RUST_ANALYZER_LOCATION_KEY, rustAnalyzer.getAbsolutePath()); + } + } + } + + /** + * Opens the "Rust" options dialog, focused in the "rust-analyzer" tab. + */ + public static void showRustAnalyzerOptions() { + SwingUtilities.invokeLater(() -> { + OptionsDisplayer.getDefault().open("Rust/RustAnalyser"); // NOI18N + }); + } + + @NbBundle.Messages({ + "MISSING_RUSTANALYZER_TITLE=rust-analyzer command was not found.", + "MISSING_RUSTANALYZER_DETAILS=rust-analyzer could not be found in your PATH. Please select the rust-analyzer executable location" + }) + public static void showRustAnalyzerNotFoundNotification() { + NotificationDisplayer.Priority priority = NotificationDisplayer.Priority.HIGH; + String title = Bundle.MISSING_RUSTANALYZER_TITLE(); + String details = Bundle.MISSING_RUSTANALYZER_DETAILS(); + ImageIcon icon = new ImageIcon(ImageUtilities.loadImage(RustProjectAPI.ICON)); + NotificationDisplayer.getDefault().notify( + title, + icon, + details, + actionEvent -> showRustAnalyzerOptions(), + priority + ); + } + +} diff --git a/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/Bundle.properties b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/Bundle.properties new file mode 100644 index 000000000000..0a9b93959a97 --- /dev/null +++ b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/Bundle.properties @@ -0,0 +1,27 @@ +# 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. +RustAnalyzerPanel.cmdGetVersion.text=Get version +RustAnalyzerPanel.cmdBrowse.toolTipText=Browse rust-analyzer in your filesystem +RustAnalyzerPanel.cmdBrowse.text=Browse... +RustAnalyzerPanel.filterRustAnalyser=rust-analyser binary +RustAnalyzerPanel.infoPanel.text=\n \n\n \n \n

About

\n

\n rust-analyzer is a LSP server for the Rust programming language.\n

\n

\n LSP server provide IDE independend language support. This includes\n semantic analysis, error checking, formatting and code completion.\n NetBeans can make use of such the rust-analyzer LSP to provide\n language services.\n

\n

Download

\n

\n Please follow the instructions on the website of the project:\n https://rust-analyzer.github.io/.\n

\n

Notice

\n

Switching from LSP/to LSP requires a restart of the IDE.

\n \n\n +RustAnalyzerPanel.restartTitle=Restart IDE +RustAnalyzerPanel.restartDetails=Click here to restart IDE and apply your rust-analyzer settings change. +RustAnalyzerPanel.lblRustAnalyzerPath.text=rust-analyzer executable location: +RustAnalyzerPanel.txtRustAnalyzerPath.toolTipText=The full path pointing to rust-analyzer executable +RustAnalyzerPanel.txtRustAnalyzerPath.text= +RustAnalyzerPanel.lblRustAnalyzerVersion.text= diff --git a/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerOptionsPanelController.java b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerOptionsPanelController.java new file mode 100644 index 000000000000..3bb3b2284240 --- /dev/null +++ b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerOptionsPanelController.java @@ -0,0 +1,111 @@ +/* + * 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.rust.options.rustanalyzer; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; + +@OptionsPanelController.SubRegistration( + id="RustAnalyzer", + location = "Rust", + displayName = "#AdvancedOption_DisplayName_RustAnalyzer", + keywords = "#AdvancedOption_Keywords_RustAnalyzer", + keywordsCategory = "Rust/RustAnalyzer", + position = 1500 +) +@org.openide.util.NbBundle.Messages({ + "AdvancedOption_DisplayName_RustAnalyzer=rust-analyzer", + "AdvancedOption_Keywords_RustAnalyzer=rust analyser rust-analyzer" +}) +public final class RustAnalyzerOptionsPanelController extends OptionsPanelController { + + private RustAnalyzerPanel panel; + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private boolean changed; + + @Override + public void update() { + getPanel().load(); + changed = false; + } + + @Override + public void applyChanges() { + SwingUtilities.invokeLater(() -> { + getPanel().store(); + changed = false; + }); + } + + @Override + public void cancel() { + // need not do anything special, if no changes have been persisted yet + } + + @Override + public boolean isValid() { + return getPanel().valid(); + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public HelpCtx getHelpCtx() { + return null; // new HelpCtx("...ID") if you have a help set + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + pcs.addPropertyChangeListener(l); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + pcs.removePropertyChangeListener(l); + } + + private RustAnalyzerPanel getPanel() { + if (panel == null) { + panel = new RustAnalyzerPanel(this); + } + return panel; + } + + void changed() { + if (!changed) { + changed = true; + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } + +} diff --git a/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerPanel.form b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerPanel.form new file mode 100644 index 000000000000..e7ac43da735d --- /dev/null +++ b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerPanel.form @@ -0,0 +1,139 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerPanel.java b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerPanel.java new file mode 100644 index 000000000000..6ba3edce2212 --- /dev/null +++ b/rust/rust.options/src/org/netbeans/modules/rust/options/rustanalyzer/RustAnalyzerPanel.java @@ -0,0 +1,373 @@ +/* + * 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.rust.options.rustanalyzer; + +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.swing.JFileChooser; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; +import javax.swing.filechooser.FileFilter; +import org.netbeans.modules.rust.options.impl.RustAnalyzerOptionsImpl; +import org.openide.LifecycleManager; +import org.openide.awt.Notification; +import org.openide.awt.NotificationDisplayer; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +final class RustAnalyzerPanel extends javax.swing.JPanel implements DocumentListener { + + private final RustAnalyzerOptionsPanelController controller; + private final ComponentListener resizeListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + updateInfoPaneWidth(); + } + }; + private final HyperlinkListener hyperlinkListener = (HyperlinkEvent he) -> { + try { + if (he.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + URI uri = he.getURL().toURI(); + Desktop.getDesktop().browse(uri); + } + } catch (IOException | URISyntaxException ex) { + ex.printStackTrace(); + } + }; + private JFileChooser fileChooser; + private SwingWorker versionWorker; + + RustAnalyzerPanel(RustAnalyzerOptionsPanelController controller) { + this.controller = controller; + initComponents(); + txtRustAnalyzerPath.getDocument().addDocumentListener(this); + infoPanel.setEditable(false); + infoPanel.addHyperlinkListener(hyperlinkListener); + } + + private void updateInfoPaneWidth() { + int width = getParent().getWidth(); + setMaximumSize(new Dimension(width, Integer.MAX_VALUE)); + setPreferredSize(new Dimension(width, super.getMinimumSize().height)); + validate(); + } + + @Override + public void addNotify() { + super.addNotify(); + SwingUtilities.getUnwrappedParent(this).addComponentListener(resizeListener); + updateInfoPaneWidth(); + } + + @Override + public void removeNotify() { + SwingUtilities.getUnwrappedParent(this).removeComponentListener(resizeListener); + super.removeNotify(); + } + + private SwingWorker newWorker() { + return new SwingWorker() { + @Override + protected String doInBackground() throws Exception { + String rustAnalyser = txtRustAnalyzerPath.getText(); + return getVersionOf(rustAnalyser); + } + + @Override + protected void done() { + versionWorker = null; + String version; + try { + version = get(); + lblRustAnalyzerVersion.setText(version); + } catch (Exception ex) { + lblRustAnalyzerVersion.setText(""); + } + } + }; + } + + private String getVersionOf(String aPossibleRustAnalyserPath) throws Exception { + File rustAnalyser = Paths.get(aPossibleRustAnalyserPath).toFile(); + if (rustAnalyser.isFile() && rustAnalyser.canExecute()) { + return getVersionOf(rustAnalyser); + } + return null; + } + + private String getVersionOf(File rustAnalyser) throws Exception { + Process p = Runtime.getRuntime().exec(new String[]{rustAnalyser.getAbsolutePath(), "--version"}); + int ok = p.waitFor(); + if (ok == 0) { + String version = null; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + do { + String line = reader.readLine(); + if (line == null) { + break; + } + version = line; + } while (true); + } + p.destroy(); + return version; + } + return null; + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + lblRustAnalyzerPath = new javax.swing.JLabel(); + txtRustAnalyzerPath = new javax.swing.JTextField(); + cmdBrowse = new javax.swing.JButton(); + cmdGetVersion = new javax.swing.JButton(); + lblRustAnalyzerVersion = new javax.swing.JLabel(); + infoPanel = new javax.swing.JTextPane(); + + setLayout(new java.awt.GridBagLayout()); + + lblRustAnalyzerPath.setLabelFor(txtRustAnalyzerPath); + org.openide.awt.Mnemonics.setLocalizedText(lblRustAnalyzerPath, org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.lblRustAnalyzerPath.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8); + add(lblRustAnalyzerPath, gridBagConstraints); + + txtRustAnalyzerPath.setText(org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.txtRustAnalyzerPath.text")); // NOI18N + txtRustAnalyzerPath.setToolTipText(org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.txtRustAnalyzerPath.toolTipText")); // NOI18N + txtRustAnalyzerPath.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtRustAnalyzerPathActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 0); + add(txtRustAnalyzerPath, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(cmdBrowse, org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.cmdBrowse.text")); // NOI18N + cmdBrowse.setToolTipText(org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.cmdBrowse.toolTipText")); // NOI18N + cmdBrowse.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmdBrowseActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 0); + add(cmdBrowse, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(cmdGetVersion, org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.cmdGetVersion.text")); // NOI18N + cmdGetVersion.setEnabled(false); + cmdGetVersion.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmdGetVersionActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 8); + add(cmdGetVersion, gridBagConstraints); + + lblRustAnalyzerVersion.setFont(lblRustAnalyzerVersion.getFont().deriveFont(lblRustAnalyzerVersion.getFont().getStyle() | java.awt.Font.BOLD)); + org.openide.awt.Mnemonics.setLocalizedText(lblRustAnalyzerVersion, org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.lblRustAnalyzerVersion.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8); + add(lblRustAnalyzerVersion, gridBagConstraints); + + infoPanel.setContentType("text/html"); // NOI18N + infoPanel.setText(org.openide.util.NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.infoPanel.text")); // NOI18N + infoPanel.setOpaque(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(infoPanel, gridBagConstraints); + }// //GEN-END:initComponents + + private void txtRustAnalyzerPathActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtRustAnalyzerPathActionPerformed + controller.changed(); + }//GEN-LAST:event_txtRustAnalyzerPathActionPerformed + + private void cmdBrowseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmdBrowseActionPerformed + + showFileChooser(); + + }//GEN-LAST:event_cmdBrowseActionPerformed + + private void cmdGetVersionActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmdGetVersionActionPerformed + if (versionWorker == null) { + versionWorker = newWorker(); + versionWorker.execute(); + } + }//GEN-LAST:event_cmdGetVersionActionPerformed + + void showFileChooser() { + if (fileChooser == null) { + fileChooser = new JFileChooser(System.getProperty("user.home")); // NOI18N + FileFilter rustAnalyserFilter = new FileFilter() { + @Override + public boolean accept(File f) { + return f.isDirectory() || (f.isFile() && f.exists() && f.canExecute() && f.getName().startsWith("rust-analyzer")); + } + + @Override + public String getDescription() { + return NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.filterRustAnalyser"); + } + }; + fileChooser.addChoosableFileFilter(rustAnalyserFilter); + fileChooser.addChoosableFileFilter(fileChooser.getAcceptAllFileFilter()); + fileChooser.setFileFilter(rustAnalyserFilter); + fileChooser.setDialogType(JFileChooser.OPEN_DIALOG); + fileChooser.setFileHidingEnabled(false); + } + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + int result = fileChooser.showDialog(this, null); + if (result == JFileChooser.APPROVE_OPTION) { + txtRustAnalyzerPath.setText(fileChooser.getSelectedFile().getAbsolutePath()); + } + } + + void load() { + Path rustAnalyzerLocation = RustAnalyzerOptionsImpl.getRustAnalyzerLocation(true, false); + txtRustAnalyzerPath.setText(rustAnalyzerLocation == null ? "" : rustAnalyzerLocation.toString()); + } + + void store() { + String path = txtRustAnalyzerPath.getText(); + Path origPath = RustAnalyzerOptionsImpl.getRustAnalyzerLocation(false, false); + RustAnalyzerOptionsImpl.setRustAnalyzerLocation(path.trim().isEmpty() ? null : path); + Path rustAnalyzerLocation = RustAnalyzerOptionsImpl.getRustAnalyzerLocation(true, false); + txtRustAnalyzerPath.setText(rustAnalyzerLocation == null ? "" : rustAnalyzerLocation.toString()); + if( + (nullOrBlank(path) && origPath != null) + || (!nullOrBlank(path) && origPath == null) + ) { + askForRestart(); + } + } + + private boolean nullOrBlank(String input) { + return input == null || input.trim().isEmpty(); + } + + boolean valid() { + String path = txtRustAnalyzerPath.getText(); + if(path.trim().isEmpty()) { + return true; + } + File rustAnalyzer = new File(txtRustAnalyzerPath.getText()); + return rustAnalyzer.exists() && rustAnalyzer.isFile() && rustAnalyzer.canExecute(); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cmdBrowse; + private javax.swing.JButton cmdGetVersion; + private javax.swing.JTextPane infoPanel; + private javax.swing.JLabel lblRustAnalyzerPath; + private javax.swing.JLabel lblRustAnalyzerVersion; + private javax.swing.JTextField txtRustAnalyzerPath; + // End of variables declaration//GEN-END:variables + + @Override + public void insertUpdate(DocumentEvent e) { + documentChanged(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + documentChanged(e); + } + + @Override + public void changedUpdate(DocumentEvent e) { + documentChanged(e); + } + + private void documentChanged(DocumentEvent e) { + controller.changed(); + File file = Paths.get(txtRustAnalyzerPath.getText()).toFile(); + boolean executable = file.exists() && file.canExecute(); + cmdGetVersion.setEnabled(executable); + } + + private Notification restartNotification; + + private void askForRestart() { + if(restartNotification != null) { + restartNotification.clear(); + } + restartNotification = NotificationDisplayer.getDefault().notify( + NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.restartTitle"), + ImageUtilities.loadImageIcon( "org/netbeans/core/windows/resources/restart.png", true ), //NOI18N + NbBundle.getMessage(RustAnalyzerPanel.class, "RustAnalyzerPanel.restartDetails"), + e -> { + if(restartNotification != null) { + restartNotification.clear(); + restartNotification = null; + } + LifecycleManager.getDefault().markForRestart(); + LifecycleManager.getDefault().exit(); + }, + NotificationDisplayer.Priority.NORMAL, NotificationDisplayer.Category.INFO); + } +} diff --git a/rust/rust.sources/src/org/netbeans/modules/rust/sources/rs/RustFileDataObject.java b/rust/rust.sources/src/org/netbeans/modules/rust/sources/rs/RustFileDataObject.java index 816d17c8666e..fb5057f97063 100644 --- a/rust/rust.sources/src/org/netbeans/modules/rust/sources/rs/RustFileDataObject.java +++ b/rust/rust.sources/src/org/netbeans/modules/rust/sources/rs/RustFileDataObject.java @@ -99,6 +99,17 @@ path = "Loaders/text/x-rust/Actions", id = @ActionID(category = "System", id = "org.openide.actions.PropertiesAction"), position = 1400 + ), + @ActionReference( + path = "Editors/text/x-rust/Popup", + id = @ActionID(category = "Refactoring", id = "org.netbeans.modules.refactoring.api.ui.WhereUsedAction"), + position = 1400 + ), + @ActionReference( + path = "Editors/text/x-rust/Popup", + id = @ActionID(category = "Refactoring", id = "org.netbeans.modules.refactoring.api.ui.RenameAction"), + position = 1500, + separatorAfter = 1550 ) })