diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/imports/JavaFixAllImports.java b/java/java.editor/src/org/netbeans/modules/java/editor/imports/JavaFixAllImports.java index 80f9064fddb2..400f94ebbba2 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/imports/JavaFixAllImports.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/imports/JavaFixAllImports.java @@ -104,7 +104,6 @@ public static JavaFixAllImports getDefault() { /** Creates a new instance of JavaFixAllImports */ private JavaFixAllImports() { } - public void fixAllImports(final FileObject fo, final JTextComponent target) { final AtomicBoolean cancel = new AtomicBoolean(); final JavaSource javaSource = JavaSource.forFileObject(fo); @@ -202,7 +201,7 @@ List getImports() { } } - private static void performFixImports(WorkingCopy wc, ImportData data, CandidateDescription[] selections, boolean removeUnusedImports) throws IOException { + public static void performFixImports(WorkingCopy wc, ImportData data, CandidateDescription[] selections, boolean removeUnusedImports) throws IOException { //do imports: Set toImport = new HashSet(); Map useFQNsFor = new HashMap(); @@ -263,7 +262,7 @@ private static void performFixImports(WorkingCopy wc, ImportData data, Candidate } } - private static ImportData computeImports(CompilationInfo info) { + public static ImportData computeImports(CompilationInfo info) { ComputeImports imps = new ComputeImports(info); Pair>, Map>> candidates = imps.computeCandidates(); @@ -351,7 +350,7 @@ private static ImportData computeImports(CompilationInfo info) { return data; } - static final class ImportData { + public static final class ImportData { public final String[] simpleNames; public final CandidateDescription[][] variants; public final CandidateDescription[] defaults; @@ -440,7 +439,7 @@ public void actionPerformed(ActionEvent e) { d.dispose(); } - static final class CandidateDescription { + public static final class CandidateDescription { public final String displayName; public final Icon icon; public final ElementHandle toImport; diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/CodeActionsProvider.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/CodeActionsProvider.java index 5a167e313edc..226c4094b454 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/CodeActionsProvider.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/CodeActionsProvider.java @@ -49,7 +49,7 @@ * @author Dusan Balek */ public abstract class CodeActionsProvider { - + public static final String FIX_IMPORTS_KIND = "source.fixImports"; public static final String CODE_GENERATOR_KIND = "source.generate"; public static final String CODE_ACTIONS_PROVIDER_CLASS = "providerClass"; public static final String DATA = "data"; diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/FixImportsCodeAction.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/FixImportsCodeAction.java new file mode 100644 index 000000000000..9484504fdadd --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/FixImportsCodeAction.java @@ -0,0 +1,185 @@ +/* + * 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.protocol; + +import com.google.gson.JsonPrimitive; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import net.java.html.json.Function; +import net.java.html.json.Model; +import net.java.html.json.ModelOperation; +import net.java.html.json.Property; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.netbeans.api.htmlui.HTMLDialog; +import org.netbeans.api.java.source.CompilationController; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.modules.java.editor.imports.JavaFixAllImports; +import org.netbeans.modules.java.editor.imports.JavaFixAllImports.CandidateDescription; +import org.netbeans.modules.java.editor.imports.JavaFixAllImports.ImportData; +import org.netbeans.modules.java.lsp.server.Utils; +import org.netbeans.modules.parsing.api.ResultIterator; +import org.openide.filesystems.FileObject; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author shimadan + */ +@ServiceProvider(service = CodeActionsProvider.class, position = 91) +public class FixImportsCodeAction extends CodeActionsProvider { + @Override + @NbBundle.Messages({ + "DN_FixImports=Fix Imports...",}) + public List getCodeActions(NbCodeLanguageClient client, ResultIterator resultIterator, CodeActionParams params) throws Exception { + List only = params.getContext().getOnly(); + if (only == null || !only.contains(CodeActionKind.Source)) { + return Collections.emptyList(); + } + CompilationController info = resultIterator.getParserResult() != null ? CompilationController.get(resultIterator.getParserResult()) : null; + if (info == null) { + return Collections.emptyList(); + } + String uri = Utils.toUri(info.getFileObject()); + return Collections.singletonList(createCodeAction(client, Bundle.DN_FixImports(), FIX_IMPORTS_KIND, uri, null)); + } + + @Override + public CompletableFuture resolve(NbCodeLanguageClient client, CodeAction codeAction, Object data) { + CompletableFuture future = new CompletableFuture<>(); + try { + String uri = ((JsonPrimitive) data).getAsString(); + FileObject file = Utils.fromUri(uri); + JavaSource js = JavaSource.forFileObject(file); + if (js == null) { + throw new IOException("Cannot get JavaSource for: " + uri); + } + final AtomicReference missingImports = new AtomicReference(); + js.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + missingImports.set(JavaFixAllImports.computeImports(cc)); + }, true); + future = showFixImportsDialog(missingImports.get()).thenApply(selections -> { + List edits; + try { + edits = TextDocumentServiceImpl.modify2TextEdits(js, wc -> { + wc.toPhase(JavaSource.Phase.RESOLVED); + JavaFixAllImports.performFixImports(wc, missingImports.get(), selections, false); + + }); + if (!edits.isEmpty()) { + codeAction.setEdit(new WorkspaceEdit(Collections.singletonMap(uri, edits))); + } + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + + } + + return codeAction; + }); + } catch (IOException | IllegalArgumentException ex) { + future.completeExceptionally(ex); + } + return future; + } + + private CompletableFuture showFixImportsDialog(ImportData Impdata) { + CompletableFuture selections = new CompletableFuture<>(); + Pages.showFixImportsDialog(Impdata, selections); + return selections; + } + + @HTMLDialog(url = "ui/FixImports.html", resources = "FixImports.css") + static final HTMLDialog.OnSubmit showFixImportsDialog(ImportData missingImports, CompletableFuture selectedCandidates) { + FixImportsUI model = new FixImportsUI(); + ImportDataUI[] imports = IntStream.range(0, missingImports.simpleNames.length) + .mapToObj(i -> new ImportDataUI( + missingImports.simpleNames[i], + missingImports.defaults[i].displayName, + Stream.of(missingImports.variants[i]).map((candidate)->candidate.displayName).toArray(String[]::new) + )) + .toArray(ImportDataUI[]::new); + model.withImports(imports).assignData(missingImports, selectedCandidates); + model.applyBindings(); + return (id) -> { + if ("accept".equals(id)) { + model.completeSelectedCandidates(); + }else{ + model.cancel(); + } + return true; + }; + } + + @Model(className = "FixImportsUI", targetId = "", instance = true, builder = "with", + properties = { + @Property(name = "imports", type = ImportDataUI.class, array = true) + }) + static final class FixImportsControl { + + private CompletableFuture selectedCandidates; + private ImportData missingImports; + + @ModelOperation + void assignData(FixImportsUI ui, ImportData missingImports, CompletableFuture selectedCandidates) { + this.selectedCandidates = selectedCandidates; + this.missingImports = missingImports; + } + + @ModelOperation + @Function + void completeSelectedCandidates(FixImportsUI ui) { + List imports = ui.getImports(); + CandidateDescription[] choosen = IntStream.range(0, imports.size()) + .mapToObj(i -> Stream.of(missingImports.variants[i]) + .filter((variant) -> variant.displayName.equals(imports.get(i).getSelectedCandidateFQN())) + .findFirst() + .get() + ) + .toArray(CandidateDescription[]::new); + selectedCandidates.complete(choosen); + } + @ModelOperation + @Function + void cancel(){ + selectedCandidates.cancel(true); + } + } + + @Model(className = "ImportDataUI", instance = true, properties = { + @Property(name = "simpleName", type = String.class), + @Property(name = "selectedCandidateFQN", type = String.class), + @Property(name = "candidatesFQN", type = String.class, array = true) + + }) + static final class ImportDataControl { + + } + +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OrganizeImportsCodeAction.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OrganizeImportsCodeAction.java index 42b3676c5658..ca7641d97736 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OrganizeImportsCodeAction.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OrganizeImportsCodeAction.java @@ -30,6 +30,7 @@ import org.eclipse.lsp4j.WorkspaceEdit; import org.netbeans.api.java.source.CompilationController; import org.netbeans.api.java.source.JavaSource; +import org.netbeans.modules.java.editor.imports.JavaFixAllImports; import org.netbeans.modules.java.hints.OrganizeImports; import org.netbeans.modules.java.lsp.server.Utils; import org.netbeans.modules.parsing.api.ResultIterator; @@ -73,7 +74,7 @@ public CompletableFuture resolve(NbCodeLanguageClient client, CodeAc } List edits = TextDocumentServiceImpl.modify2TextEdits(js, wc -> { wc.toPhase(JavaSource.Phase.RESOLVED); - OrganizeImports.doOrganizeImports(wc, null, false); + OrganizeImports.doOrganizeImports(wc, null, false); }); if (!edits.isEmpty()) { codeAction.setEdit(new WorkspaceEdit(Collections.singletonMap(uri, edits))); diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ui/FixImports.html b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ui/FixImports.html new file mode 100644 index 000000000000..5c0a3d71b1ba --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ui/FixImports.html @@ -0,0 +1,56 @@ + + + + + + + + + + Fix Imports... + + + +
+ +
+ + + +
+
+
+
+
+ +
+
+ +
+
+
+
+ + +
+ + diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ui/fixImports.css b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ui/fixImports.css new file mode 100644 index 000000000000..23a46ebd3c1a --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ui/fixImports.css @@ -0,0 +1,209 @@ +/* + + 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. + +*/ + +html, body { + height: 100%; + padding: 0; + margin: 0; +} +body { + display: flex; + flex-direction: column; + overflow-x: scroll; +} +input[type="text"], select { + width: 100%; + font-family: var(--vscode-editor-font-family) !important; + border: var(--vscode-input-border); + margin-right: 6px !important; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + box-sizing: border-box; +} +input:focus, select:focus { + outline-color: var(--vscode-focusBorder); +} +input[type="text"] { + padding: 5px 4px !important; +} +input[type="checkbox"] { + width: 1.2em; + min-width: 1.2em; + height: 1.2em; + min-height: 1.2em; + cursor: pointer; + filter: opacity(80%); +} +input[type="checkbox"]:disabled { + filter: brightness(90%); +} +input[type="checkbox"]:checked { + filter: opacity(100%); +} +select { + padding: 4px !important; +} +button { + cursor: pointer; +} +label { + display: block; + margin-bottom: 5px; + user-select: none; +} +.vscode-font { + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + text-decoration: none; +} +.vscode-editor-font { + font-family: var(--vscode-editor-font-family) !important; + /*font-size: var(--vscode-editor-font-size);*/ +} +.button-text { + margin-left: 5px; + vertical-align: text-top; +} +.regular-button { + border: none; + padding: 4px 10px 4px 10px; + margin: 5px; + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); +} +.regular-button:hover, .regular-button:focus { + background: var(--vscode-button-hoverBackground); +} +.regular-button:focus { + outline: 1px solid var(--vscode-button-hoverBackground); + outline-offset: 2px; +} +.silent-button { + border: none; + padding: 4px 10px 4px 10px; + margin: 5px; + color: var(--vscode-input-foreground); + background-color: var(--vscode-list-hoverBackground); +} +.silent-button:hover { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-hoverBackground); +} +.silent-button:focus { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-hoverBackground); + outline: 1px solid var(--vscode-button-hoverBackground); + outline-offset: 2px; +} +.action-button { + border: none; + padding: 4px 10px 4px 10px; + margin: 2px; + color: var(--vscode-input-foreground); + background-color: var(--vscode-list-hoverBackground); +} + +.action-button:hover { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-hoverBackground); +} + +.action-button:focus, .action-button:hover:focus { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-hoverBackground); + outline: 1px solid var(--vscode-button-hoverBackground); + outline-offset: 2px; +} +.checkbox-label { + align-self: center; + display: initial; + cursor: pointer; + margin: 0px; + margin-left: 3px; +} +.params-caption { + margin-bottom: 8px; +} +.filler { + width: 53px; +} +.preview-panel { + margin: 0px; + padding: 10px; + font-family: var(--vscode-editor-font-family) !important; + background-color: var(--vscode-sideBar-background); + cursor: default; +} +.section { + padding: 20px 20px 0px; +} +.section1 { + padding: 0px 10px; +} +.section2 { + padding: 0px 10px; + margin-top: -6px; +} +.section3 { + padding: 0px 20px 0px; +} +.section-newline { + padding: 0px 20px 0px; +} +.section-nested { + padding: 5px 40px; +} +.vdivider { + margin-bottom: 20px; +} +.vdivider2 { + margin-bottom: 6px; +} +.hdivider { + margin-left: 6px; +} +.row { + padding: 3px 10px; +} +.row:hover { + background-color: var(--vscode-list-hoverBackground); +} +.row:focus-within { + background-color: var(--vscode-tab-unfocusedInactiveModifiedBorder); +} +.flex { + display: flex; +} +.flex-grow { + flex-grow: 1; + display: block; +} +.flex-equal { + flex: 1 1 0; + display: block; +} +.flex-vscrollable { + overflow-x: hidden; + overflow-y: auto; +} +.align-right { + margin-left: auto; +} diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java index 39c69f6c251e..17e665acbb3a 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java @@ -3781,6 +3781,80 @@ public CompletableFuture applyEdit(ApplyWorkspaceEdi fileChanges.get(1).getRange()); assertEquals("List", fileChanges.get(1).getNewText()); } + + public void testSourceActionFixImports() throws Exception { + File src = new File(getWorkDir(), "a/Test.java"); + src.getParentFile().mkdirs(); + try (Writer w = new FileWriter(new File(src.getParentFile().getParentFile(), ".test-project"))) { + } + String code = "package a;\n" + + "public class Test {\n" + + "private final List names = new ArrayList<>();\n"+ + "}\n"; + try (Writer w = new FileWriter(src)) { + w.write(code); + } + CountDownLatch indexingComplete = new CountDownLatch(1); + Launcher serverLauncher = createClientLauncherWithLogging(new TestCodeLanguageClient() { + @Override + public void showMessage(MessageParams params) { + if (Server.INDEXING_COMPLETED.equals(params.getMessage())) { + indexingComplete.countDown(); + } else { + throw new UnsupportedOperationException("Unexpected message."); + } + } + @Override + public CompletableFuture showHtmlPage(HtmlPageParams params) { + FixImportsUI ui = MockHtmlViewer.assertDialogShown(params.getId(), FixImportsUI.class); + ui.completeSelectedCandidates(); + return CompletableFuture.completedFuture(null); + } + + }, client.getInputStream(), client.getOutputStream()); + serverLauncher.startListening(); + LanguageServer server = serverLauncher.getRemoteProxy(); + InitializeParams initParams = new InitializeParams(); + initParams.setWorkspaceFolders(List.of(new WorkspaceFolder(getWorkDir().toURI().toString()))); + server.initialize(initParams).get(); + indexingComplete.await(); + String uri = src.toURI().toString(); + server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "java", 0, code))); + VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(src.toURI().toString(), 1); + + CodeActionParams codeActionParams = new CodeActionParams( + id, + new Range(new Position(0, 0), + new Position(0, 0)), + new CodeActionContext(Arrays.asList(), Arrays.asList(CodeActionKind.Source)) + ); + + List> codeActions = server.getTextDocumentService() + .codeAction(codeActionParams) + .get(); + Optional fixImports + = codeActions.stream() + .filter(Either::isRight) + .map(Either::getRight) + .filter(a -> Bundle.DN_FixImports().equals(a.getTitle())) + .findAny(); + assertTrue(fixImports.isPresent()); + CodeAction resolvedCodeAction = server.getTextDocumentService() + .resolveCodeAction(fixImports.get()) + .get(); + + assertNotNull(resolvedCodeAction); + WorkspaceEdit edit = resolvedCodeAction.getEdit(); + assertNotNull(edit); + assertEquals(1, edit.getChanges().size()); + List fileChanges = edit.getChanges().get(tripleSlashUri(uri)); + assertNotNull(fileChanges); + assertEquals(1, fileChanges.size()); + assertEquals(new Range(new Position(1, 0), + new Position(1, 0)), + fileChanges.get(0).getRange()); + assertEquals("\nimport java.util.ArrayList;\nimport java.util.List;\n\n", fileChanges.get(0).getNewText()); + } public void testRenameDocumentChangesCapabilitiesRenameOp() throws Exception { doTestRename(init -> {