diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/PriorityQueueRun.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/PriorityQueueRun.java new file mode 100644 index 000000000000..c5f0873d09fc --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/PriorityQueueRun.java @@ -0,0 +1,273 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.openide.util.Exceptions; +import org.openide.util.RequestProcessor; + +public class PriorityQueueRun { + + private static final PriorityQueueRun INSTANCE = new PriorityQueueRun(); + private static final RequestProcessor WORKER = new RequestProcessor(PriorityQueueRun.class.getName() + "-worker", 1, false, false); + private static final RequestProcessor DELAY = new RequestProcessor(PriorityQueueRun.class.getName() + "-delay", 1, false, false); + + public static PriorityQueueRun getInstance() { + return INSTANCE; + } + + private final SortedMap>> priority2Tasks = new TreeMap<>((p1, p2) -> -p1.compareTo(p2)); + private final Set pendingDelayedTasks = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + private Priority currentPriority; + private CancelCheck currentTaskCheck; + + /** + * Get the outcome of the given cancellable task on background, using the given + * priority. Unless the returned {@code CompletableFuture} is + * {@link CompletableFuture#cancel(boolean) }-ed, the result will always be computed, + * but the provided {@code task} may (re-)run multiple times, and be cancelled multiple + * times. The tasks should account for a re-run after a cancellation. + * + * @param

the type of the data parameter of the {@code task} + * @param the result of the {@code task} + * @param priority the priority with which the task should run + * @param task the task to run - may be run multiple times, until a non-cancelled run is achieved + * @param data the arbitrary data to pass to the task + * @return a CompletableFuture the will hold the result + */ + public CompletableFuture runTask(Priority priority, CancellableTask task, P data) { + CompletableFuture result = new CompletableFuture<>(); + + synchronized (this) { + priority2Tasks.computeIfAbsent(priority, __ -> new ArrayList<>()) + .add(new TaskDescription(task, data, result)); + + scheduleNext(); + } + + return result; + } + + /** + * Get the outcome of the given cancellable task on background, using the given + * priority. Unless the returned {@code CompletableFuture} is + * {@link CompletableFuture#cancel(boolean) }-ed, the result will always be computed, + * but the provided {@code task} may (re-)run multiple times, and be cancelled multiple + * times. The tasks should account for a re-run after a cancellation. + * The {@code task} will be schedule no sooner than after the {@code delay} elapses. + * + * @param

the type of the data parameter of the {@code task} + * @param the result of the {@code task} + * @param priority the priority with which the task should run + * @param task the task to run - may be run multiple times, until a non-cancelled run is achieved + * @param data the arbitrary data to pass to the task + * @param delay the minimal time to wait before scheduling the task + * @return a CompletableFuture the will hold the result + */ + public CompletableFuture runTask(Priority priority, CancellableTask task, P data, int delay) { + CompletableFuture result = new CompletableFuture<>(); + RequestProcessor.Task[] delayedTask = new RequestProcessor.Task[1]; + delayedTask[0] = DELAY.create(() -> { + pendingDelayedTasks.remove(delayedTask[0]); + if (result.isCancelled()) { + return ; //already cancelled + } + synchronized (this) { + priority2Tasks.computeIfAbsent(priority, __ -> new ArrayList<>()) + .add(new TaskDescription(task, data, result)); + + scheduleNext(); + } + }); + + pendingDelayedTasks.add(delayedTask[0]); + delayedTask[0].schedule(delay); + + return result; + } + + private synchronized void scheduleNext() { + Priority foundPriority = null; + List> foundTasks = null; + + for (Entry>> e : priority2Tasks.entrySet()) { + if (!e.getValue().isEmpty()) { + foundPriority = e.getKey(); + foundTasks = e.getValue(); + break; + } + } + + if (foundPriority == null) { + return ; + } + + if (currentPriority == null) { + Priority thisPriority = currentPriority = foundPriority; + TaskDescription thisTask = foundTasks.remove(0); + CancelCheck thisTaskCheck = currentTaskCheck = new CancelCheck(); + + WORKER.post(() -> { + thisTask.result.whenComplete((__, exc) -> { + if (exc instanceof CancellationException) { + thisTaskCheck.cancel(); + } + }); + + Object result = null; + Throwable exception = null; + AtomicBoolean delegatedCancel = new AtomicBoolean(); + + if (!thisTask.result.isCancelled()) { + try { + result = thisTask.task.compute(thisTask.data, thisTaskCheck); + } catch (CancellationException ex) { + delegatedCancel.set(true); + } catch (Throwable t) { + exception = t; + } + } else { + //the CompletableFuture is already canceled, no need to compute anything + } + + synchronized (PriorityQueueRun.this) { + currentPriority = null; + currentTaskCheck = null; + + if (!thisTask.result.isCancelled()) { + if (exception != null) { + thisTask.result.completeExceptionally(exception); + } else if (thisTaskCheck.isCancelled() || delegatedCancel.get()) { + priority2Tasks.computeIfAbsent(thisPriority, __ -> new ArrayList<>()) + .add(0, thisTask); + } else { + thisTask.result.complete(result); + } + } + } + + scheduleNext(); + }); + } + + if (currentPriority != null && currentPriority.compareTo(foundPriority) < 0) { + //cancel the currently running task: + currentTaskCheck.cancel(); + } + } + + private static final class TaskDescription { + private final CancellableTask task; + private final P data; + private final CompletableFuture result; + + public TaskDescription(CancellableTask task, P data, CompletableFuture result) { + this.task = task; + this.data = data; + this.result = result; + } + } + + public interface CancellableTask { + public R compute(P param, CancelCheck cancel) throws Exception; + } + + public interface CancelCallback { + public void cancel(); + } + + public final class CancelCheck { + private final AtomicBoolean cancelled = new AtomicBoolean(); + private final AtomicReference cancelCallback = new AtomicReference<>(); + + private CancelCheck() {} + + public boolean isCancelled() { + return cancelled.get(); + } + + public void registerCancel(CancelCallback callback) { + cancelCallback.set(callback); + if (cancelled.get()) { + callback.cancel(); + } + } + + void cancel() { + cancelled.set(true); + + CancelCallback callback = cancelCallback.get(); + + if (callback != null) { + callback.cancel(); + } + } + } + + public enum Priority { + BELOW_LOW, + LOW, + NORMAL, + HIGH, + HIGHER; + } + + /** + * For tests: wait until all tasks are finished. + */ + public void testsWaitQueueEmpty() { + while (true) { + while (!pendingDelayedTasks.isEmpty()) { + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + } + + DELAY.post(() -> {}).waitFinished(); + WORKER.post(() -> {}).waitFinished(); + + synchronized (this) { + if (priority2Tasks.values().stream().allMatch(l -> l.isEmpty())) { + break ; + } + try { + this.wait(10); + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + //ensure any last task is finished: + WORKER.post(() -> {}).waitFinished(); + } +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java index 93de647abc5e..69baae8be162 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java @@ -69,6 +69,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.CancellationException; import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -78,6 +79,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.IntFunction; +import java.util.function.LongSupplier; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; @@ -256,6 +258,8 @@ import org.netbeans.api.lsp.StructureElement; import org.netbeans.modules.editor.indent.api.Reformat; import org.netbeans.modules.java.lsp.server.URITranslator; +import org.netbeans.modules.java.lsp.server.protocol.PriorityQueueRun.CancelCheck; +import org.netbeans.modules.java.lsp.server.protocol.PriorityQueueRun.Priority; import org.netbeans.modules.java.lsp.server.ui.AbstractJavaPlatformProviderOverride; import org.netbeans.modules.parsing.impl.SourceAccessor; import org.netbeans.modules.sampler.Sampler; @@ -308,7 +312,7 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli private static final String NETBEANS_JAVA_ON_SAVE_ORGANIZE_IMPORTS = "java.onSave.organizeImports";// NOI18N private static final String NETBEANS_CODE_COMPLETION_COMMIT_CHARS = "java.completion.commit.chars";// NOI18N private static final String URL = "url";// NOI18N - private static final String INDEX = "index";// NOI18N + private static final String ID = "id";// NOI18N private static final RequestProcessor BACKGROUND_TASKS = new RequestProcessor(TextDocumentServiceImpl.class.getName(), 1, false, false); private static final RequestProcessor WORKER = new RequestProcessor(TextDocumentServiceImpl.class.getName(), 1, false, false); @@ -321,8 +325,15 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli /** * Documents actually opened by the client. */ - private final Map diagnosticTasks = new HashMap<>(); + private final Map pendingDiagnostics = new HashMap<>(); private final LspServerState server; + private final LongSupplier idGenerator = new LongSupplier() { + private final AtomicLong counter = new AtomicLong(0); + @Override + public long getAsLong() { + return counter.getAndIncrement(); + } + }; private NbCodeLanguageClient client; TextDocumentServiceImpl(LspServerState server) { @@ -365,7 +376,7 @@ public void indexingComplete(Set indexedRoots) { } private final AtomicInteger javadocTimeout = new AtomicInteger(-1); - private List lastCompletions = null; + private Map lastCompletions = Map.of(); private static final int INITIAL_COMPLETION_SAMPLING_DELAY = 1000; private static final int DEFAULT_COMPLETION_WARNING_LENGTH = 10_000; @@ -378,7 +389,7 @@ public void indexingComplete(Set indexedRoots) { "# {1} - path to the saved sampler file", "INFO_LongCodeCompletion=Analyze completions taking longer than {0}. A sampler snapshot has been saved to: {1}" }) - public CompletableFuture, CompletionList>> completion(CompletionParams params) { + public CompletableFuture, CompletionList>> completion(CompletionParams p) { AtomicBoolean done = new AtomicBoolean(); AtomicReference samplerRef = new AtomicReference<>(); AtomicLong samplingStart = new AtomicLong(); @@ -403,155 +414,178 @@ public CompletableFuture, CompletionList>> completio } }, INITIAL_COMPLETION_SAMPLING_DELAY); - lastCompletions = new ArrayList<>(); - AtomicInteger index = new AtomicInteger(0); + Map currentCompletions = Collections.synchronizedMap(new HashMap<>()); + final CompletionList completionList = new CompletionList(); // shortcut: if the projects are not yet initialized, return empty: if (server.openedProjects().getNow(null) == null) { + synchronized (this) { + lastCompletions = currentCompletions; + } + return CompletableFuture.completedFuture(Either.forRight(completionList)); } try { - String uri = params.getTextDocument().getUri(); + String uri = p.getTextDocument().getUri(); FileObject file = fromURI(uri); if (file == null) { + synchronized (this) { + lastCompletions = currentCompletions; + } + return CompletableFuture.completedFuture(Either.forRight(completionList)); } EditorCookie ec = file.getLookup().lookup(EditorCookie.class); Document rawDoc = ec.openDocument(); if (!(rawDoc instanceof StyledDocument)) { + synchronized (this) { + lastCompletions = currentCompletions; + } + return CompletableFuture.completedFuture(Either.forRight(completionList)); } StyledDocument doc = (StyledDocument)rawDoc; List configValues = List.of(NETBEANS_JAVADOC_LOAD_TIMEOUT, NETBEANS_COMPLETION_WARNING_TIME, NETBEANS_CODE_COMPLETION_COMMIT_CHARS); - return client.getClientConfigurationManager().getConfigurations(configValues, uri).thenApply(c -> { - if (c != null && !c.isEmpty()) { - if (c.get(0).isJsonPrimitive()) { - JsonPrimitive javadocTimeSetting = c.get(0).getAsJsonPrimitive(); - - javadocTimeout.set(javadocTimeSetting.getAsInt()); - } - if (c.get(1).isJsonPrimitive()) { - JsonPrimitive samplingWarningsLengthSetting = c.get(1).getAsJsonPrimitive(); - - samplingWarningLength.set(samplingWarningsLengthSetting.getAsLong()); - } - if(c.get(2) instanceof JsonArray){ - JsonArray commitCharsJsonArray = (JsonArray) c.get(2); - codeCompletionCommitChars.set(commitCharsJsonArray.asList().stream().map(ch -> ch.toString()).collect(Collectors.toList())); - } - } - final int caret = Utils.getOffset(doc, params.getPosition()); - List items = new ArrayList<>(); - Completion.Context context = params.getContext() != null - ? new Completion.Context(Completion.TriggerKind.valueOf(params.getContext().getTriggerKind().name()), - params.getContext().getTriggerCharacter() == null || params.getContext().getTriggerCharacter().isEmpty() ? null : params.getContext().getTriggerCharacter().charAt(0)) - : null; - Preferences prefs = CodeStylePreferences.get(doc, "text/x-java").getPreferences(); - String point = prefs.get("classMemberInsertionPoint", null); - try { - prefs.put("classMemberInsertionPoint", CodeStyle.InsertionPoint.CARET_LOCATION.name()); - boolean isComplete = Completion.collect(doc, caret, context, completion -> { - CompletionItem item = new CompletionItem(completion.getLabel()); - if (completion.getLabelDetail() != null || completion.getLabelDescription() != null) { - CompletionItemLabelDetails labelDetails = new CompletionItemLabelDetails(); - labelDetails.setDetail(completion.getLabelDetail()); - labelDetails.setDescription(completion.getLabelDescription()); - item.setLabelDetails(labelDetails); + return client.getClientConfigurationManager().getConfigurations(configValues, uri).thenCompose(c -> + PriorityQueueRun.getInstance() + ., CompletionList>>runTask(Priority.HIGHER, (params, cancel) -> { + if (c != null && !c.isEmpty()) { + if (c.get(0).isJsonPrimitive()) { + JsonPrimitive javadocTimeSetting = c.get(0).getAsJsonPrimitive(); + + javadocTimeout.set(javadocTimeSetting.getAsInt()); } - if (completion.getKind() != null) { - item.setKind(CompletionItemKind.valueOf(completion.getKind().name())); - } - if (completion.getTags() != null) { - item.setTags(completion.getTags().stream().map(tag -> CompletionItemTag.valueOf(tag.name())).collect(Collectors.toList())); + if (c.get(1).isJsonPrimitive()) { + JsonPrimitive samplingWarningsLengthSetting = c.get(1).getAsJsonPrimitive(); + + samplingWarningLength.set(samplingWarningsLengthSetting.getAsLong()); } - if (completion.getDetail() != null && completion.getDetail().isDone()) { - item.setDetail(completion.getDetail().getNow(null)); + if(c.get(2) instanceof JsonArray){ + JsonArray commitCharsJsonArray = (JsonArray) c.get(2); + codeCompletionCommitChars.set(commitCharsJsonArray.asList().stream().map(ch -> ch.toString()).collect(Collectors.toList())); } - if (completion.getDocumentation() != null && completion.getDocumentation().isDone()) { - String documentation = completion.getDocumentation().getNow(null); - if (documentation != null) { - MarkupContent markup = new MarkupContent(); - markup.setKind("markdown"); - markup.setValue(html2MD(documentation)); - item.setDocumentation(markup); + } + final int caret = Utils.getOffset(doc, params.getPosition()); + List items = new ArrayList<>(); + Completion.Context context = params.getContext() != null + ? new Completion.Context(Completion.TriggerKind.valueOf(params.getContext().getTriggerKind().name()), + params.getContext().getTriggerCharacter() == null || params.getContext().getTriggerCharacter().isEmpty() ? null : params.getContext().getTriggerCharacter().charAt(0)) + : null; + Preferences prefs = CodeStylePreferences.get(doc, "text/x-java").getPreferences(); + String point = prefs.get("classMemberInsertionPoint", null); + try { + prefs.put("classMemberInsertionPoint", CodeStyle.InsertionPoint.CARET_LOCATION.name()); + boolean isComplete = Completion.collect(doc, caret, context, completion -> { + CompletionItem item = new CompletionItem(completion.getLabel()); + if (completion.getLabelDetail() != null || completion.getLabelDescription() != null) { + CompletionItemLabelDetails labelDetails = new CompletionItemLabelDetails(); + labelDetails.setDetail(completion.getLabelDetail()); + labelDetails.setDescription(completion.getLabelDescription()); + item.setLabelDetails(labelDetails); } + if (completion.getKind() != null) { + item.setKind(CompletionItemKind.valueOf(completion.getKind().name())); + } + if (completion.getTags() != null) { + item.setTags(completion.getTags().stream().map(tag -> CompletionItemTag.valueOf(tag.name())).collect(Collectors.toList())); + } + if (completion.getDetail() != null && completion.getDetail().isDone()) { + item.setDetail(completion.getDetail().getNow(null)); + } + if (completion.getDocumentation() != null && completion.getDocumentation().isDone()) { + String documentation = completion.getDocumentation().getNow(null); + if (documentation != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("markdown"); + markup.setValue(html2MD(documentation)); + item.setDocumentation(markup); + } + } + if (completion.isPreselect()) { + item.setPreselect(true); + } + item.setSortText(completion.getSortText()); + item.setFilterText(completion.getFilterText()); + item.setInsertText(completion.getInsertText()); + if (completion.getInsertTextFormat() != null) { + item.setInsertTextFormat(InsertTextFormat.valueOf(completion.getInsertTextFormat().name())); + } + org.netbeans.api.lsp.TextEdit edit = completion.getTextEdit(); + if (edit != null) { + item.setTextEdit(Either.forLeft(new TextEdit(new Range(Utils.createPosition(file, edit.getStartOffset()), Utils.createPosition(file, edit.getEndOffset())), edit.getNewText()))); + } + org.netbeans.api.lsp.Command command = completion.getCommand(); + if (command != null) { + item.setCommand(new Command(command.getTitle(), Utils.encodeCommand(command.getCommand(), client.getNbCodeCapabilities()), command.getArguments())); + } + if (completion.getAdditionalTextEdits() != null && completion.getAdditionalTextEdits().isDone()) { + List additionalTextEdits = completion.getAdditionalTextEdits().getNow(null); + if (additionalTextEdits != null && !additionalTextEdits.isEmpty()) { + item.setAdditionalTextEdits(additionalTextEdits.stream().map(ed -> { + return new TextEdit(new Range(Utils.createPosition(file, ed.getStartOffset()), Utils.createPosition(file, ed.getEndOffset())), ed.getNewText()); + }).collect(Collectors.toList())); + } + } + if (codeCompletionCommitChars.get() != null) { + item.setCommitCharacters(codeCompletionCommitChars.get()); + } + long id = idGenerator.getAsLong(); + currentCompletions.put(id, completion); + item.setData(new CompletionData(uri, id)); + items.add(item); + }); + if (!isComplete) { + completionList.setIsIncomplete(true); } - if (completion.isPreselect()) { - item.setPreselect(true); - } - item.setSortText(completion.getSortText()); - item.setFilterText(completion.getFilterText()); - item.setInsertText(completion.getInsertText()); - if (completion.getInsertTextFormat() != null) { - item.setInsertTextFormat(InsertTextFormat.valueOf(completion.getInsertTextFormat().name())); - } - org.netbeans.api.lsp.TextEdit edit = completion.getTextEdit(); - if (edit != null) { - item.setTextEdit(Either.forLeft(new TextEdit(new Range(Utils.createPosition(file, edit.getStartOffset()), Utils.createPosition(file, edit.getEndOffset())), edit.getNewText()))); - } - org.netbeans.api.lsp.Command command = completion.getCommand(); - if (command != null) { - item.setCommand(new Command(command.getTitle(), Utils.encodeCommand(command.getCommand(), client.getNbCodeCapabilities()), command.getArguments())); + } finally { + if (point != null) { + prefs.put("classMemberInsertionPoint", point); + } else { + prefs.remove("classMemberInsertionPoint"); } - if (completion.getAdditionalTextEdits() != null && completion.getAdditionalTextEdits().isDone()) { - List additionalTextEdits = completion.getAdditionalTextEdits().getNow(null); - if (additionalTextEdits != null && !additionalTextEdits.isEmpty()) { - item.setAdditionalTextEdits(additionalTextEdits.stream().map(ed -> { - return new TextEdit(new Range(Utils.createPosition(file, ed.getStartOffset()), Utils.createPosition(file, ed.getEndOffset())), ed.getNewText()); - }).collect(Collectors.toList())); + + done.set(true); + Sampler sampler = samplerRef.get(); + RUNNING_SAMPLER.compareAndExchange(sampler, null); + if (sampler != null) { + long samplingTime = (System.currentTimeMillis() - completionStart); + long minSamplingTime = Math.min(1_000, samplingWarningLength.get()); + if (samplingTime >= minSamplingTime && + samplingTime >= samplingWarningLength.get() && + samplingWarningLength.get() >= 0) { + Lookup lookup = Lookup.getDefault(); + new Thread(() -> { + Lookups.executeWith(lookup, () -> { + Path logDir = Places.getUserDirectory().toPath().resolve("var/log"); + try { + Path target = Files.createTempFile(logDir, "completion-sampler", ".npss"); + try (OutputStream out = Files.newOutputStream(target); + DataOutputStream dos = new DataOutputStream(out)) { + sampler.stopAndWriteTo(dos); + + NotifyDescriptor notifyUser = new Message(Bundle.INFO_LongCodeCompletion(samplingWarningLength.get(), target.toAbsolutePath().toString())); + + DialogDisplayer.getDefault().notifyLater(notifyUser); + } + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + }); + }).start(); } } - if (codeCompletionCommitChars.get() != null) { - item.setCommitCharacters(codeCompletionCommitChars.get()); - } - lastCompletions.add(completion); - item.setData(new CompletionData(uri, index.getAndIncrement())); - items.add(item); - }); - if (!isComplete) { - completionList.setIsIncomplete(true); - } - } finally { - if (point != null) { - prefs.put("classMemberInsertionPoint", point); - } else { - prefs.remove("classMemberInsertionPoint"); } + completionList.setItems(items); - done.set(true); - Sampler sampler = samplerRef.get(); - RUNNING_SAMPLER.compareAndExchange(sampler, null); - if (sampler != null) { - long samplingTime = (System.currentTimeMillis() - completionStart); - long minSamplingTime = Math.min(1_000, samplingWarningLength.get()); - if (samplingTime >= minSamplingTime && - samplingTime >= samplingWarningLength.get() && - samplingWarningLength.get() >= 0) { - Lookup lookup = Lookup.getDefault(); - new Thread(() -> { - Lookups.executeWith(lookup, () -> { - Path logDir = Places.getUserDirectory().toPath().resolve("var/log"); - try { - Path target = Files.createTempFile(logDir, "completion-sampler", ".npss"); - try (OutputStream out = Files.newOutputStream(target); - DataOutputStream dos = new DataOutputStream(out)) { - sampler.stopAndWriteTo(dos); - - NotifyDescriptor notifyUser = new Message(Bundle.INFO_LongCodeCompletion(samplingWarningLength.get(), target.toAbsolutePath().toString())); - - DialogDisplayer.getDefault().notifyLater(notifyUser); - } - } catch (IOException ex) { - Exceptions.printStackTrace(ex); - } - }); - }).start(); + return Either.forRight(completionList); + }, p)).whenComplete((res, exc) -> { + if (exc == null) { + synchronized (this) { + lastCompletions = currentCompletions; } + } - } - completionList.setItems(items); - return Either.forRight(completionList); - }); + }); } catch (IOException ex) { throw new IllegalStateException(ex); } @@ -559,12 +593,12 @@ public CompletableFuture, CompletionList>> completio public static final class CompletionData { public String uri; - public int index; + public long index; public CompletionData() { } - public CompletionData(String uri, int index) { + public CompletionData(String uri, long index) { this.uri = uri; this.index = index; } @@ -622,16 +656,22 @@ public void init(ClientCapabilities clientCapabilities, ServerCapabilities sever } @Override - public CompletableFuture resolveCompletionItem(CompletionItem ci) { - JsonObject rawData = (JsonObject) ci.getData(); - if (rawData != null) { - CompletionData data = new Gson().fromJson(rawData, CompletionData.class); - Completion completion = lastCompletions.get(data.index); - if (completion != null) { - FileObject file = fromURI(data.uri); - if (file != null) { - CompletableFuture result = new CompletableFuture<>(); - WORKER.post(() -> { + public CompletableFuture resolveCompletionItem(CompletionItem input) { + Map currentCompletions; + + synchronized (this) { + currentCompletions = lastCompletions; + } + + return PriorityQueueRun.getInstance() + .runTask(Priority.HIGHER, (ci, cancel) -> { + JsonObject rawData = (JsonObject) ci.getData(); + if (rawData != null) { + CompletionData data = new Gson().fromJson(rawData, CompletionData.class); + Completion completion = currentCompletions.get(data.index); + if (completion != null) { + FileObject file = fromURI(data.uri); + if (file != null) { Preferences prefs = CodeStylePreferences.get(file, "text/x-java").getPreferences(); String point = prefs.get("classMemberInsertionPoint", null); try { @@ -679,13 +719,11 @@ public CompletableFuture resolveCompletionItem(CompletionItem ci prefs.remove("classMemberInsertionPoint"); } } - result.complete(ci); - }); - return result; + } } } - } - return CompletableFuture.completedFuture(ci); + return ci; + }, input); } public static String html2MD(String html) { @@ -694,83 +732,92 @@ public static String html2MD(String html) { } @Override - public CompletableFuture hover(HoverParams params) { + public CompletableFuture hover(HoverParams p) { // shortcut: if the projects are not yet initialized, return empty: if (server.openedProjects().getNow(null) == null) { return CompletableFuture.completedFuture(null); } - String uri = params.getTextDocument().getUri(); - FileObject file = fromURI(uri); - Document rawDoc = server.getOpenedDocuments().getDocument(uri); - if (file == null || !(rawDoc instanceof StyledDocument)) { - return CompletableFuture.completedFuture(null); - } - StyledDocument doc = (StyledDocument) rawDoc; - return org.netbeans.api.lsp.Hover.getContent(doc, Utils.getOffset(doc, params.getPosition())).thenApply(content -> { - if (content != null) { - MarkupContent markup = new MarkupContent(); - markup.setKind("markdown"); - markup.setValue(html2MD(content)); - return new Hover(markup); + return PriorityQueueRun.getInstance() + .runTask(Priority.HIGH, (params, cancel) -> { + String uri = params.getTextDocument().getUri(); + FileObject file = fromURI(uri); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (file == null || !(rawDoc instanceof StyledDocument)) { + return null; } - return null; - }); + StyledDocument doc = (StyledDocument) rawDoc; + CompletableFuture future = org.netbeans.api.lsp.Hover.getContent(doc, Utils.getOffset(doc, params.getPosition())).thenApply(content -> { + if (content != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("markdown"); + markup.setValue(html2MD(content)); + return new Hover(markup); + } + return null; + }); + cancel.registerCancel(() -> future.cancel(true)); + return future.get(); + }, p); } @Override - public CompletableFuture signatureHelp(SignatureHelpParams params) { + public CompletableFuture signatureHelp(SignatureHelpParams p) { // shortcut: if the projects are not yet initialized, return empty: if (server.openedProjects().getNow(null) == null) { return CompletableFuture.completedFuture(null); } - String uri = params.getTextDocument().getUri(); - FileObject file = fromURI(uri); - Document rawDoc = server.getOpenedDocuments().getDocument(uri); - if (file == null || !(rawDoc instanceof StyledDocument)) { - return CompletableFuture.completedFuture(null); - } - StyledDocument doc = (StyledDocument) rawDoc; - List signatures = new ArrayList<>(); - AtomicInteger activeSignature = new AtomicInteger(-1); - AtomicInteger activeParameter = new AtomicInteger(-1); - org.netbeans.api.lsp.SignatureInformation.collect(doc, Utils.getOffset(doc, params.getPosition()), null, signature -> { - SignatureInformation signatureInformation = new SignatureInformation(signature.getLabel()); - List parameters = new ArrayList<>(signature.getParameters().size()); - for (int i = 0; i < signature.getParameters().size(); i++) { - org.netbeans.api.lsp.SignatureInformation.ParameterInformation parameter = signature.getParameters().get(i); - ParameterInformation parameterInformation = new ParameterInformation(parameter.getLabel()); - if (parameter.getDocumentation() != null) { + return PriorityQueueRun.getInstance() + .runTask(Priority.HIGHER, (params, cancel) -> { + String uri = params.getTextDocument().getUri(); + FileObject file = fromURI(uri); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (file == null || !(rawDoc instanceof StyledDocument)) { + return null; + } + StyledDocument doc = (StyledDocument) rawDoc; + List signatures = new ArrayList<>(); + AtomicInteger activeSignature = new AtomicInteger(-1); + AtomicInteger activeParameter = new AtomicInteger(-1); + org.netbeans.api.lsp.SignatureInformation.collect(doc, Utils.getOffset(doc, params.getPosition()), null, signature -> { + SignatureInformation signatureInformation = new SignatureInformation(signature.getLabel()); + List parameters = new ArrayList<>(signature.getParameters().size()); + for (int i = 0; i < signature.getParameters().size(); i++) { + org.netbeans.api.lsp.SignatureInformation.ParameterInformation parameter = signature.getParameters().get(i); + ParameterInformation parameterInformation = new ParameterInformation(parameter.getLabel()); + if (parameter.getDocumentation() != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("markdown"); + markup.setValue(html2MD(parameter.getDocumentation())); + parameterInformation.setDocumentation(markup); + } + parameters.add(parameterInformation); + if (signatureInformation.getActiveParameter() == null && parameter.isActive()) { + signatureInformation.setActiveParameter(i); + } + } + if (signature.getDocumentation() != null) { MarkupContent markup = new MarkupContent(); markup.setKind("markdown"); - markup.setValue(html2MD(parameter.getDocumentation())); - parameterInformation.setDocumentation(markup); - } - parameters.add(parameterInformation); - if (signatureInformation.getActiveParameter() == null && parameter.isActive()) { - signatureInformation.setActiveParameter(i); - } - } - if (signature.getDocumentation() != null) { - MarkupContent markup = new MarkupContent(); - markup.setKind("markdown"); - markup.setValue(html2MD(signature.getDocumentation())); - signatureInformation.setDocumentation(markup); - } - signatureInformation.setParameters(parameters); - if (activeSignature.get() < 0 && signature.isActive()) { - activeSignature.set(signatures.size()); - if (signatureInformation.getActiveParameter() != null) { - activeParameter.set(signatureInformation.getActiveParameter()); + markup.setValue(html2MD(signature.getDocumentation())); + signatureInformation.setDocumentation(markup); + } + signatureInformation.setParameters(parameters); + if (activeSignature.get() < 0 && signature.isActive()) { + activeSignature.set(signatures.size()); + if (signatureInformation.getActiveParameter() != null) { + activeParameter.set(signatureInformation.getActiveParameter()); + } } - } - signatures.add(signatureInformation); - }); - return CompletableFuture.completedFuture(signatures.isEmpty() ? null : new SignatureHelp(signatures, activeSignature.get(), activeParameter.get())); + signatures.add(signatureInformation); + }); + return signatures.isEmpty() ? null : new SignatureHelp(signatures, activeSignature.get(), activeParameter.get()); + }, p); } @Override - public CompletableFuture, List>> definition(DefinitionParams params) { - try { + public CompletableFuture, List>> definition(DefinitionParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.HIGH, (params, cancel) -> { String uri = params.getTextDocument().getUri(); Document rawDoc = server.getOpenedDocuments().getDocument(uri); if (rawDoc instanceof StyledDocument) { @@ -778,23 +825,24 @@ public CompletableFuture, List { + CompletableFuture, List>> future = HyperlinkLocation.resolve(doc, offset).thenApply(locs -> { return Either.forLeft(locs.stream().map(location -> { FileObject fo = location.getFileObject(); return new Location(Utils.toUri(fo), new Range(Utils.createPosition(fo, location.getStartOffset()), Utils.createPosition(fo, location.getEndOffset()))); }).collect(Collectors.toList())); }); + cancel.registerCancel(() -> future.cancel(true)); + return future.get(); } } - } catch (MalformedURLException ex) { - client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); - } - return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); + return Either.forLeft(Collections.emptyList()); + }, p); } @Override - public CompletableFuture, List>> typeDefinition(TypeDefinitionParams params) { - try { + public CompletableFuture, List>> typeDefinition(TypeDefinitionParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.HIGH, (params, cancel) -> { String uri = params.getTextDocument().getUri(); Document rawDoc = server.getOpenedDocuments().getDocument(uri); if (rawDoc instanceof StyledDocument) { @@ -802,18 +850,18 @@ public CompletableFuture, List { + CompletableFuture, List>> future = HyperlinkLocation.resolveTypeDefinition(doc, offset).thenApply(locs -> { return Either.forLeft(locs.stream().map(location -> { FileObject fo = location.getFileObject(); return new Location(Utils.toUri(fo), new Range(Utils.createPosition(fo, location.getStartOffset()), Utils.createPosition(fo, location.getEndOffset()))); }).collect(Collectors.toList())); }); + cancel.registerCancel(() -> future.cancel(true)); + return future.get(); } } - } catch (MalformedURLException ex) { - client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); - } - return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); + return Either.forLeft(Collections.emptyList()); + }, p); } @Override @@ -977,75 +1025,84 @@ private static Range toRange(PositionBounds bounds) throws IOException { } @Override - public CompletableFuture> documentHighlight(DocumentHighlightParams params) { - class MOHighligther extends MarkOccurrencesHighlighterBase { - @Override - protected void process(CompilationInfo arg0, Document arg1, SchedulerEvent arg2) { - throw new UnsupportedOperationException("Should not be called."); + public CompletableFuture> documentHighlight(DocumentHighlightParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, (params, cancel) -> { + class MOHighligther extends MarkOccurrencesHighlighterBase { + @Override + protected void process(CompilationInfo arg0, Document arg1, SchedulerEvent arg2) { + throw new UnsupportedOperationException("Should not be called."); + } + @Override + public List processImpl(CompilationInfo info, Preferences node, Document doc, int caretPosition) { + return super.processImpl(info, node, doc, caretPosition); + } } - @Override - public List processImpl(CompilationInfo info, Preferences node, Document doc, int caretPosition) { - return super.processImpl(info, node, doc, caretPosition); + + Preferences node = MarkOccurencesSettings.getCurrentNode(); + + JavaSource js = getJavaSource(params.getTextDocument().getUri()); + List result = new ArrayList<>(); + if (js == null) { + return result; } - } + try { + js.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + Document rawDoc = cc.getSnapshot().getSource().getDocument(true); + if (rawDoc instanceof StyledDocument) { + StyledDocument doc = (StyledDocument)rawDoc; + int offset = Utils.getOffset(doc, params.getPosition()); + MOHighligther highlighter = new MOHighligther(); - Preferences node = MarkOccurencesSettings.getCurrentNode(); + cancel.registerCancel(highlighter::cancel); - JavaSource js = getJavaSource(params.getTextDocument().getUri()); - List result = new ArrayList<>(); - if (js == null) { - return CompletableFuture.completedFuture(result); - } - try { - js.runUserActionTask(cc -> { - cc.toPhase(JavaSource.Phase.RESOLVED); - Document rawDoc = cc.getSnapshot().getSource().getDocument(true); - if (rawDoc instanceof StyledDocument) { - StyledDocument doc = (StyledDocument)rawDoc; - int offset = Utils.getOffset(doc, params.getPosition()); - List spans = new MOHighligther().processImpl(cc, node, doc, offset); - if (spans != null) { - for (int[] span : spans) { - result.add(new DocumentHighlight(new Range(Utils.createPosition(cc.getCompilationUnit(), span[0]), - Utils.createPosition(cc.getCompilationUnit(), span[1])))); + List spans = highlighter.processImpl(cc, node, doc, offset); + if (spans != null) { + for (int[] span : spans) { + result.add(new DocumentHighlight(new Range(Utils.createPosition(cc.getCompilationUnit(), span[0]), + Utils.createPosition(cc.getCompilationUnit(), span[1])))); + } } } - } - }, true); - } catch (IOException ex) { - //TODO: include stack trace: - client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); - } - return CompletableFuture.completedFuture(result); + }, true); + } catch (IOException ex) { + //TODO: include stack trace: + client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); + } + return result; + }, p); } @Override - public CompletableFuture>> documentSymbol(DocumentSymbolParams params) { - final CompletableFuture>> resultFuture = new CompletableFuture<>(); - - BACKGROUND_TASKS.post(() -> { - List> result = new ArrayList<>(); - String uri = params.getTextDocument().getUri(); - FileObject file = fromURI(uri); - Document rawDoc = server.getOpenedDocuments().getDocument(uri); - if (file != null && rawDoc instanceof StyledDocument) { - StyledDocument doc = (StyledDocument)rawDoc; - Collection structureProviders = MimeLookup.getLookup(DocumentUtilities.getMimeType(doc)).lookupAll(StructureProvider.class); - for (StructureProvider structureProvider : structureProviders) { - List structureElements = structureProvider.getStructure(doc); - if (!structureElements.isEmpty()) { - for (StructureElement structureElement : structureElements) { - DocumentSymbol ds = structureElement2DocumentSymbol(doc, structureElement); - if (ds != null) { - result.add(Either.forRight(ds)); - } - } - } - } - } - resultFuture.complete(result); - }); - return resultFuture; + public CompletableFuture>> documentSymbol(DocumentSymbolParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, (params, cancel) -> { + //TODO: no way to cancel a running structure task at currently + List> result = new ArrayList<>(); + String uri = params.getTextDocument().getUri(); + FileObject file = fromURI(uri); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (file != null && rawDoc instanceof StyledDocument) { + StyledDocument doc = (StyledDocument)rawDoc; + Collection structureProviders = MimeLookup.getLookup(DocumentUtilities.getMimeType(doc)).lookupAll(StructureProvider.class); + for (StructureProvider structureProvider : structureProviders) { + if (cancel.isCancelled()) { + continue; + } + List structureElements = structureProvider.getStructure(doc); + if (!structureElements.isEmpty()) { + for (StructureElement structureElement : structureElements) { + DocumentSymbol ds = structureElement2DocumentSymbol(doc, structureElement); + if (ds != null) { + result.add(Either.forRight(ds)); + } + } + } + } + } + return result; + }, p); } static DocumentSymbol structureElement2DocumentSymbol (StyledDocument doc, StructureElement el) { @@ -1073,116 +1130,129 @@ static DocumentSymbol structureElement2DocumentSymbol (StyledDocument doc, Struc return ds; } - private List lastCodeActions = null; - - @Override - public CompletableFuture>> codeAction(CodeActionParams params) { - lastCodeActions = new ArrayList<>(); - AtomicInteger index = new AtomicInteger(0); + private Map id2CodeAction = null; - // shortcut: if the projects are not yet initialized, return empty: + @Override + public CompletableFuture>> codeAction(CodeActionParams p) { if (server.openedProjects().getNow(null) == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } - String uri = params.getTextDocument().getUri(); - Document rawDoc = server.getOpenedDocuments().getDocument(uri); - if (!(rawDoc instanceof StyledDocument)) { - return CompletableFuture.completedFuture(Collections.emptyList()); - } - StyledDocument doc = (StyledDocument)rawDoc; - List> result = new ArrayList<>(); - Range range = params.getRange(); - int startOffset = Utils.getOffset(doc, range.getStart()); - int endOffset = Utils.getOffset(doc, range.getEnd()); - Predicate codeActionKindPermitted = Utils.codeActionKindFilter(params.getContext().getOnly()); - if ((startOffset == endOffset || !params.getContext().getDiagnostics().isEmpty()) && - (codeActionKindPermitted.test(CodeActionKind.QuickFix) || codeActionKindPermitted.test(CodeActionKind.RefactorRewrite))) { - final javax.swing.text.Element elem = NbDocument.findLineRootElement(doc); - int lineStartOffset = elem.getStartOffset(); - int lineEndOffset = elem.getEndOffset(); + Map currentId2CodeAction = Collections.synchronizedMap(new HashMap<>()); - ArrayList diagnostics = new ArrayList<>(params.getContext().getDiagnostics()); - if (diagnostics.isEmpty()) { - diagnostics.addAll(computeDiags(params.getTextDocument().getUri(), startOffset, ErrorProvider.Kind.HINTS, documentVersion(doc), null)); + return PriorityQueueRun.getInstance() + .>>runTask(Priority.NORMAL, (params, cancel) -> { + // shortcut: if the projects are not yet initialized, return empty: + String uri = params.getTextDocument().getUri(); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (!(rawDoc instanceof StyledDocument)) { + return List.of(); } + StyledDocument doc = (StyledDocument)rawDoc; - Map id2Errors = new HashMap<>(); - for (String key : VALID_ERROR_KEYS) { - Map diags = (Map) doc.getProperty("lsp-errors-valid-" + key); - if (diags != null) { - id2Errors.putAll(diags); + List> result = new ArrayList<>(); + Range range = params.getRange(); + int startOffset = Utils.getOffset(doc, range.getStart()); + int endOffset = Utils.getOffset(doc, range.getEnd()); + Predicate codeActionKindPermitted = Utils.codeActionKindFilter(params.getContext().getOnly()); + if ((startOffset == endOffset || !params.getContext().getDiagnostics().isEmpty()) && + (codeActionKindPermitted.test(CodeActionKind.QuickFix) || codeActionKindPermitted.test(CodeActionKind.RefactorRewrite))) { + final javax.swing.text.Element elem = NbDocument.findLineRootElement(doc); + int lineStartOffset = elem.getStartOffset(); + int lineEndOffset = elem.getEndOffset(); + + ArrayList diagnostics = new ArrayList<>(params.getContext().getDiagnostics()); + if (diagnostics.isEmpty()) { + diagnostics.addAll(computeDiags(params.getTextDocument().getUri(), startOffset, ErrorProvider.Kind.HINTS, documentVersion(doc), null, cancel)); } - } - if (!id2Errors.isEmpty()) { - for (Entry entry : id2Errors.entrySet()) { - org.netbeans.api.lsp.Diagnostic err = entry.getValue(); - if (err.getDescription() == null || err.getDescription().isEmpty()) { - continue; + + if (cancel.isCancelled()) { + return result; + } + + Map id2Errors = new HashMap<>(); + for (String key : VALID_ERROR_KEYS) { + Map diags = (Map) doc.getProperty("lsp-errors-valid-" + key); + if (diags != null) { + id2Errors.putAll(diags); } - if (err.getSeverity() == org.netbeans.api.lsp.Diagnostic.Severity.Error) { - if (err.getEndPosition().getOffset() < startOffset || err.getStartPosition().getOffset() > endOffset) { - continue; - } - } else { - if (err.getEndPosition().getOffset() < lineStartOffset || err.getStartPosition().getOffset() > lineEndOffset) { - continue; - } - int lineStart = NbDocument.findLineNumber(doc, startOffset); - int errStartLine = NbDocument.findLineNumber(doc, err.getStartPosition().getOffset()); - if(errStartLine != lineStart){ + } + if (!id2Errors.isEmpty()) { + for (Entry entry : id2Errors.entrySet()) { + org.netbeans.api.lsp.Diagnostic err = entry.getValue(); + if (err.getDescription() == null || err.getDescription().isEmpty()) { continue; } - } - Optional diag = diagnostics.stream().filter(d -> entry.getKey().equals(d.getCode().getLeft())).findFirst(); - org.netbeans.api.lsp.Diagnostic.LazyCodeActions actions = err.getActions(); - if (actions != null) { - for (org.netbeans.api.lsp.CodeAction inputAction : actions.computeCodeActions(ex -> client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())))) { - CodeAction action = new CodeAction(inputAction.getTitle()); - if (diag.isPresent()) { - action.setDiagnostics(Collections.singletonList(diag.get())); + if (err.getSeverity() == org.netbeans.api.lsp.Diagnostic.Severity.Error) { + if (err.getEndPosition().getOffset() < startOffset || err.getStartPosition().getOffset() > endOffset) { + continue; } - String codeActionKind = kind(err.getSeverity()); - if (!codeActionKindPermitted.test(codeActionKind)) { + } else { + if (err.getEndPosition().getOffset() < lineStartOffset || err.getStartPosition().getOffset() > lineEndOffset) { continue; } - action.setKind(codeActionKind); - if (inputAction.getCommand() != null) { - List commandParams = new ArrayList<>(); - - commandParams.add(params.getTextDocument().getUri()); + int lineStart = NbDocument.findLineNumber(doc, startOffset); + int errStartLine = NbDocument.findLineNumber(doc, err.getStartPosition().getOffset()); + if(errStartLine != lineStart){ + continue; + } + } + Optional diag = diagnostics.stream().filter(d -> entry.getKey().equals(d.getCode().getLeft())).findFirst(); + org.netbeans.api.lsp.Diagnostic.LazyCodeActions actions = err.getActions(); + if (actions != null) { + for (org.netbeans.api.lsp.CodeAction inputAction : actions.computeCodeActions(ex -> client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())))) { + if (cancel.isCancelled()) { + return result; + } - if (inputAction.getCommand().getArguments() != null) { - commandParams.addAll(inputAction.getCommand().getArguments()); + CodeAction action = new CodeAction(inputAction.getTitle()); + if (diag.isPresent()) { + action.setDiagnostics(Collections.singletonList(diag.get())); } + String codeActionKind = kind(err.getSeverity()); + if (!codeActionKindPermitted.test(codeActionKind)) { + continue; + } + action.setKind(codeActionKind); + if (inputAction.getCommand() != null) { + List commandParams = new ArrayList<>(); - action.setCommand(new Command(inputAction.getCommand().getTitle(), Utils.encodeCommand(inputAction.getCommand().getCommand(), client.getNbCodeCapabilities()), commandParams)); - } - if (inputAction instanceof LazyCodeAction && ((LazyCodeAction) inputAction).getLazyEdit() != null) { - lastCodeActions.add((LazyCodeAction) inputAction); - Map data = new HashMap<>(); - data.put(URL, uri); - data.put(INDEX, index.getAndIncrement()); - action.setData(data); - } else if (inputAction.getEdit() != null) { - action.setEdit(Utils.workspaceEditFromApi(inputAction.getEdit(), uri, client)); + commandParams.add(params.getTextDocument().getUri()); + + if (inputAction.getCommand().getArguments() != null) { + commandParams.addAll(inputAction.getCommand().getArguments()); + } + + action.setCommand(new Command(inputAction.getCommand().getTitle(), Utils.encodeCommand(inputAction.getCommand().getCommand(), client.getNbCodeCapabilities()), commandParams)); + } + if (inputAction instanceof LazyCodeAction && ((LazyCodeAction) inputAction).getLazyEdit() != null) { + long id = idGenerator.getAsLong(); + currentId2CodeAction.put(id, (LazyCodeAction) inputAction); + Map data = new HashMap<>(); + data.put(URL, uri); + data.put(ID, id); + action.setData(data); + } else if (inputAction.getEdit() != null) { + action.setEdit(Utils.workspaceEditFromApi(inputAction.getEdit(), uri, client)); + } + result.add(Either.forRight(action)); } - result.add(Either.forRight(action)); } } } } - } - final CompletableFuture>> resultFuture = new CompletableFuture<>(); - Source source = Source.create(doc); - BACKGROUND_TASKS.post(() -> { + Source source = Source.create(doc); try { ParserManager.parse(Collections.singleton(source), new UserTask() { @Override public void run(ResultIterator resultIterator) throws Exception { //code generators: for (CodeActionsProvider codeGenerator : Lookup.getDefault().lookupAll(CodeActionsProvider.class)) { + if (cancel.isCancelled()) { + return ; + } + Set supportedCodeActionKinds = codeGenerator.getSupportedCodeActionKinds(); if (supportedCodeActionKinds != null && supportedCodeActionKinds.stream() @@ -1208,16 +1278,29 @@ public void run(ResultIterator resultIterator) throws Exception { ex.printStackTrace(pw); } client.logMessage(new MessageParams(MessageType.Error, w.toString())); - } finally { - resultFuture.complete(result); + } + + return result; + }, p).whenComplete((res, exc) -> { + if (exc == null) { + synchronized (this) { + id2CodeAction = currentId2CodeAction; + } } }); - return resultFuture; } @Override public CompletableFuture resolveCodeAction(CodeAction unresolved) { + //note: not simply possible to rewrite to PriorityQueueRun, as FixImportsCodeAction will attempt to show a dialog + Map currentId2CodeAction; + + synchronized (this) { + currentId2CodeAction = id2CodeAction; + } + CompletableFuture future = new CompletableFuture<>(); + BACKGROUND_TASKS.post(() -> { JsonObject data = (JsonObject) unresolved.getData(); if (data != null) { @@ -1235,8 +1318,8 @@ public CompletableFuture resolveCodeAction(CodeAction unresolved) { return; } } - } else if (data.has(URL) && data.has(INDEX)) { - LazyCodeAction inputAction = lastCodeActions.get(data.getAsJsonPrimitive(INDEX).getAsInt()); + } else if (data.has(URL) && data.has(ID)) { + LazyCodeAction inputAction = currentId2CodeAction.get(data.getAsJsonPrimitive(ID).getAsLong()); if (inputAction != null) { try { unresolved.setEdit(Utils.workspaceEditFromApi(inputAction.getLazyEdit().get(), data.getAsJsonPrimitive(URL).getAsString(), client)); @@ -2110,29 +2193,48 @@ private void runDiagnosticTasks(String uri, boolean force) { // sync needed - this can be called also from reporterControl, from other that LSP request thread. The factory function just cretaes a stopped // Task that is executed later. - synchronized (diagnosticTasks) { - diagnosticTasks.computeIfAbsent(uri, u -> { - return BACKGROUND_TASKS.create(() -> { - Document originalDoc = server.getOpenedDocuments().getDocument(uri); - long originalVersion = documentVersion(originalDoc); - AtomicReference docHolder = new AtomicReference<>(originalDoc); - List errorDiags = computeDiags(u, -1, ErrorProvider.Kind.ERRORS, originalVersion, docHolder); - if (documentVersion(originalDoc) == originalVersion) { - publishDiagnostics(uri, errorDiags); - BACKGROUND_TASKS.create(() -> { - List hintDiags = computeDiags(u, -1, ErrorProvider.Kind.HINTS, originalVersion, docHolder); - Document doc = server.getOpenedDocuments().getDocument(uri); - if (documentVersion(doc) == originalVersion) { - publishDiagnostics(uri, hintDiags); - } - }).schedule(DELAY); - } - }); - }).schedule(DELAY); + synchronized (pendingDiagnostics) { + Runnable currentlyRunning = pendingDiagnostics.remove(uri); + if (currentlyRunning != null) { + currentlyRunning.run(); + } + + Document originalDoc = server.getOpenedDocuments().getDocument(uri); + long originalVersion = documentVersion(originalDoc); + AtomicReference docHolder = new AtomicReference<>(originalDoc); + + CompletableFuture errorFuture = + PriorityQueueRun.getInstance() + .runTask(Priority.LOW, + //todo: check document versions: + (__, cancel) -> computeDiags(uri, -1, ErrorProvider.Kind.ERRORS, originalVersion, docHolder, cancel), + null, + DELAY) + .thenAccept(errorDiags -> { + if (documentVersion(originalDoc) == originalVersion) { + publishDiagnostics(uri, errorDiags); + } + }); + CompletableFuture hintsFuture = + PriorityQueueRun.getInstance() + .runTask(Priority.BELOW_LOW, + (__, cancel) -> computeDiags(uri, -1, ErrorProvider.Kind.HINTS, originalVersion, docHolder, cancel), + null, + DELAY) + .thenAccept(errorDiags -> { + if (documentVersion(originalDoc) == originalVersion) { + publishDiagnostics(uri, errorDiags); + } + }); + pendingDiagnostics.put(uri, () -> { + errorFuture.cancel(true); + hintsFuture.cancel(true); + }); } } CompletableFuture> computeDiagnostics(String uri, EnumSet types) { + //TODO: rewrite to PriorityQueueRun CompletableFuture> r = new CompletableFuture<>(); BACKGROUND_TASKS.post(() -> { try { @@ -2141,10 +2243,10 @@ CompletableFuture> computeDiagnostics(String uri, EnumSet docHolder = new AtomicReference<>(originalDoc); List result = Collections.emptyList(); if (types.contains(ErrorProvider.Kind.ERRORS)) { - result = computeDiags(uri, -1, ErrorProvider.Kind.ERRORS, originalVersion, docHolder); + result = computeDiags(uri, -1, ErrorProvider.Kind.ERRORS, originalVersion, docHolder, null); } if (types.contains(ErrorProvider.Kind.HINTS)) { - result = computeDiags(uri, -1, ErrorProvider.Kind.HINTS, originalVersion, docHolder); + result = computeDiags(uri, -1, ErrorProvider.Kind.HINTS, originalVersion, docHolder, null); } r.complete(result); } catch (ThreadDeath td) { @@ -2171,7 +2273,7 @@ CompletableFuture> computeDiagnostics(String uri, EnumSet computeDiags(String uri, int offset, ErrorProvider.Kind errorKind, long orgV, AtomicReference docHolder) { + private List computeDiags(String uri, int offset, ErrorProvider.Kind errorKind, long orgV, AtomicReference docHolder, CancelCheck cancelCheck) { List result = new ArrayList<>(); FileObject file = fromURI(uri); if (file == null) { @@ -2196,6 +2298,9 @@ private List computeDiags(String uri, int offset, ErrorProvider.Kind List errors; if (!errorProviders.isEmpty()) { ErrorProvider.Context context = new ErrorProvider.Context(file, offset, errorKind, hintsPrefsFile); + if (cancelCheck != null) { + cancelCheck.registerCancel(context::cancel); + } class CancelListener implements DocumentListener { @Override public void insertUpdate(DocumentEvent e) { @@ -2218,7 +2323,12 @@ public void changedUpdate(DocumentEvent e) {} doc.addDocumentListener(l); l.checkCancel(); errors = errorProviders.stream().flatMap(provider -> { - List errorsOrNull = provider.computeErrors(context); + List errorsOrNull; + if (!context.isCancelled()) { + errorsOrNull = provider.computeErrors(context); + } else { + errorsOrNull = null; + } if (errorsOrNull == null) { errorsOrNull = Collections.emptyList(); } @@ -2227,6 +2337,9 @@ public void changedUpdate(DocumentEvent e) {} } finally { doc.removeDocumentListener(l); } + if (context.isCancelled()) { + return result; + } } else { errors = null; } @@ -2540,79 +2653,88 @@ private static void reportNotificationDone(String s, Object parameter) { }}; @Override - public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - JavaSource js = getJavaSource(params.getTextDocument().getUri()); - List result = new ArrayList<>(); - if (js != null) { - try { - js.runUserActionTask(cc -> { - cc.toPhase(JavaSource.Phase.RESOLVED); - Document doc = cc.getSnapshot().getSource().getDocument(true); - new SemanticHighlighterBase() { - @Override - protected boolean process(CompilationInfo info, Document doc) { - process(info, doc, new ErrorDescriptionSetter() { - @Override - public void setHighlights(Document doc, Collection> highlights, Map preText) { - //...nothing - } + public CompletableFuture semanticTokensFull(SemanticTokensParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, + (params, cancel) -> { + JavaSource js = getJavaSource(params.getTextDocument().getUri()); + List result = new ArrayList<>(); + if (js != null) { + try { + js.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + Document doc = cc.getSnapshot().getSource().getDocument(true); + class SemanticHighlighterImpl extends SemanticHighlighterBase { + @Override + protected boolean process(CompilationInfo info, Document doc) { + process(info, doc, new ErrorDescriptionSetter() { + @Override + public void setHighlights(Document doc, Collection> highlights, Map preText) { + //...nothing + } - @Override - public void setColorings(Document doc, Map colorings) { - int line = 0; - long column = 0; - int lastLine = 0; - long currentLineStart = 0; - long nextLineStart = getStartPosition(line + 2); - Map ordered = new TreeMap<>((t1, t2) -> t1.offset(null) - t2.offset(null)); - ordered.putAll(colorings); - for (Entry e : ordered.entrySet()) { - int currentOffset = e.getKey().offset(null); - while (nextLineStart < currentOffset) { - line++; - currentLineStart = nextLineStart; - nextLineStart = getStartPosition(line + 2); - column = 0; - } - Optional tokenType = e.getValue().stream().map(c -> coloring2TokenType[c.ordinal()]).collect(Collectors.maxBy((v1, v2) -> v1.intValue() - v2.intValue())); - int modifiers = 0; - for (ColoringAttributes c : e.getValue()) { - int mod = coloring2TokenModifier[c.ordinal()]; - if (mod != (-1)) { - modifiers |= mod; + @Override + public void setColorings(Document doc, Map colorings) { + int line = 0; + long column = 0; + int lastLine = 0; + long currentLineStart = 0; + long nextLineStart = getStartPosition(line + 2); + Map ordered = new TreeMap<>((t1, t2) -> t1.offset(null) - t2.offset(null)); + ordered.putAll(colorings); + for (Entry e : ordered.entrySet()) { + int currentOffset = e.getKey().offset(null); + while (nextLineStart < currentOffset) { + line++; + currentLineStart = nextLineStart; + nextLineStart = getStartPosition(line + 2); + column = 0; + } + Optional tokenType = e.getValue().stream().map(c -> coloring2TokenType[c.ordinal()]).collect(Collectors.maxBy((v1, v2) -> v1.intValue() - v2.intValue())); + int modifiers = 0; + for (ColoringAttributes c : e.getValue()) { + int mod = coloring2TokenModifier[c.ordinal()]; + if (mod != (-1)) { + modifiers |= mod; + } + } + if (tokenType.isPresent() && tokenType.get() >= 0) { + result.add(line - lastLine); + result.add((int) (currentOffset - currentLineStart - column)); + result.add(e.getKey().length()); + result.add(tokenType.get()); + result.add(modifiers); + lastLine = line; + column = currentOffset - currentLineStart; } - } - if (tokenType.isPresent() && tokenType.get() >= 0) { - result.add(line - lastLine); - result.add((int) (currentOffset - currentLineStart - column)); - result.add(e.getKey().length()); - result.add(tokenType.get()); - result.add(modifiers); - lastLine = line; - column = currentOffset - currentLineStart; } } - } - private long getStartPosition(int line) { - try { - return line < 0 ? -1 : info.getCompilationUnit().getLineMap().getStartPosition(line); - } catch (Exception e) { - return info.getText().length(); + private long getStartPosition(int line) { + try { + return line < 0 ? -1 : info.getCompilationUnit().getLineMap().getStartPosition(line); + } catch (Exception e) { + return info.getText().length(); + } } - } - }); - return true; + }); + return true; + } + }; + SemanticHighlighterImpl highlighter = new SemanticHighlighterImpl(); + cancel.registerCancel(highlighter::cancel); + if (cancel.isCancelled()) { + return ; } - }.process(cc, doc); - }, true); - } catch (IOException ex) { - //TODO: include stack trace: - client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); + highlighter.process(cc, doc); + }, true); + } catch (IOException ex) { + //TODO: include stack trace: + client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); + } } - } - SemanticTokens tokens = new SemanticTokens(result); - return CompletableFuture.completedFuture(tokens); + return new SemanticTokens(result); + }, p); } @Override @@ -2816,86 +2938,103 @@ protected CallHierarchyOutgoingCall createResultItem(CallHierarchyItem item, Lis } @Override - public CompletableFuture> inlayHint(InlayHintParams params) { - String uri = params.getTextDocument().getUri(); - - return client.getClientConfigurationManager().getConfiguration(NETBEANS_INLAY_HINT, uri).thenCompose(c -> { - FileObject file; - try { - file = Utils.fromUri(uri); - } catch (MalformedURLException ex) { - return CompletableFuture.failedFuture(ex); - } - Set enabled = null; - if (c != null && c.isJsonArray()) { - enabled = new HashSet<>(); - - JsonArray actualSettings = c.getAsJsonArray(); + public CompletableFuture> inlayHint(InlayHintParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, (params, cancel) -> { + String uri = params.getTextDocument().getUri(); - for (JsonElement el : actualSettings) { - enabled.add(el.getAsJsonPrimitive().getAsString()); + CompletableFuture> outterResult = client.getClientConfigurationManager().getConfiguration(NETBEANS_INLAY_HINT, uri).thenCompose(c -> { + FileObject file; + try { + file = Utils.fromUri(uri); + } catch (MalformedURLException ex) { + return CompletableFuture.failedFuture(ex); } - } - org.netbeans.api.lsp.Range range = new org.netbeans.api.lsp.Range(Utils.getOffset(file, params.getRange().getStart()), - Utils.getOffset(file, params.getRange().getEnd())); - CompletableFuture> result = CompletableFuture.completedFuture(List.of()); - for (InlayHintsProvider p : MimeLookup.getLookup(FileUtil.getMIMEType(file)).lookupAll(InlayHintsProvider.class)) { - Set currentTypes = new HashSet<>(p.supportedHintTypes()); + Set enabled = null; + if (c != null && c.isJsonArray()) { + enabled = new HashSet<>(); + + JsonArray actualSettings = c.getAsJsonArray(); - if (enabled != null) { - currentTypes.retainAll(enabled); + for (JsonElement el : actualSettings) { + enabled.add(el.getAsJsonPrimitive().getAsString()); + } } + org.netbeans.api.lsp.Range range = new org.netbeans.api.lsp.Range(Utils.getOffset(file, params.getRange().getStart()), + Utils.getOffset(file, params.getRange().getEnd())); + CompletableFuture> result = CompletableFuture.completedFuture(List.of()); + for (InlayHintsProvider provider : MimeLookup.getLookup(FileUtil.getMIMEType(file)).lookupAll(InlayHintsProvider.class)) { + if (cancel.isCancelled()) { + break; + } - if (!currentTypes.isEmpty()) { - InlayHintsProvider.Context ctx = new InlayHintsProvider.Context(file, range, currentTypes); - result = result.thenCombine(p.inlayHints(ctx).thenApply(lspHints -> { - List hints = new ArrayList<>(); + Set currentTypes = new HashSet<>(provider.supportedHintTypes()); - for (org.netbeans.api.lsp.InlayHint h : lspHints) { - hints.add(new InlayHint(Utils.createPosition(file, h.getPosition().getOffset()), Either.forRight(List.of(new InlayHintLabelPart(h.getText()))))); - } + if (enabled != null) { + currentTypes.retainAll(enabled); + } - return hints; - }), (l1, l2) -> { - List combined = new ArrayList<>(); + if (!currentTypes.isEmpty()) { + InlayHintsProvider.Context ctx = new InlayHintsProvider.Context(file, range, currentTypes); + result = result.thenCombine(provider.inlayHints(ctx).thenApply(lspHints -> { + List hints = new ArrayList<>(); - combined.addAll(l1); - combined.addAll(l2); + for (org.netbeans.api.lsp.InlayHint h : lspHints) { + hints.add(new InlayHint(Utils.createPosition(file, h.getPosition().getOffset()), Either.forRight(List.of(new InlayHintLabelPart(h.getText()))))); + } - return combined; - }); + return hints; + }), (l1, l2) -> { + List combined = new ArrayList<>(); + + combined.addAll(l1); + combined.addAll(l2); + + return combined; + }); + } } - } - return result; - }); + return result; + }); + cancel.registerCancel(() -> outterResult.cancel(true)); + return outterResult.get(); + }, p); } @Override - public CompletableFuture> inlineValue(InlineValueParams params) { - String uri = params.getTextDocument().getUri(); - FileObject file = fromURI(uri); - if (file == null) { - return CompletableFuture.completedFuture(null); - } - CompletableFuture> result = new CompletableFuture<>(); - result.complete(List.of()); - Document rawDoc = server.getOpenedDocuments().getDocument(uri); - if (!(rawDoc instanceof StyledDocument)) { - return CompletableFuture.completedFuture(Collections.emptyList()); - } - StyledDocument doc = (StyledDocument)rawDoc; - int currentExecutionPosition = Utils.getOffset(doc, params.getContext().getStoppedLocation().getEnd()); - for (InlineValuesProvider provider : MimeLookup.getLookup(file.getMIMEType()).lookupAll(InlineValuesProvider.class)) { - result = result.thenCombine(provider.inlineValues(file, currentExecutionPosition), (l1, l2) -> { - List res = new ArrayList<>(l1.size() + l2.size()); - res.addAll(l1); - for (org.netbeans.api.lsp.InlineValue val : l2) { - res.add(new InlineValue(new InlineValueEvaluatableExpression(new Range(Utils.createPosition(file, val.getRange().getStartOffset()), Utils.createPosition(file, val.getRange().getEndOffset())), val.getExpression()))); - } - return res; - }); - } - return result; + public CompletableFuture> inlineValue(InlineValueParams p) { + return PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, (params, cancel) -> { + String uri = params.getTextDocument().getUri(); + FileObject file = fromURI(uri); + if (file == null) { + return List.of(); + } + CompletableFuture> result = new CompletableFuture<>(); + result.complete(List.of()); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (!(rawDoc instanceof StyledDocument)) { + return List.of(); + } + StyledDocument doc = (StyledDocument)rawDoc; + int currentExecutionPosition = Utils.getOffset(doc, params.getContext().getStoppedLocation().getEnd()); + for (InlineValuesProvider provider : MimeLookup.getLookup(file.getMIMEType()).lookupAll(InlineValuesProvider.class)) { + if (cancel.isCancelled()) { + return List.of(); + } + result = result.thenCombine(provider.inlineValues(file, currentExecutionPosition), (l1, l2) -> { + List res = new ArrayList<>(l1.size() + l2.size()); + res.addAll(l1); + for (org.netbeans.api.lsp.InlineValue val : l2) { + res.add(new InlineValue(new InlineValueEvaluatableExpression(new Range(Utils.createPosition(file, val.getRange().getStartOffset()), Utils.createPosition(file, val.getRange().getEndOffset())), val.getExpression()))); + } + return res; + }); + } + CompletableFuture> resultFin = result; + cancel.registerCancel(() -> resultFin.cancel(true)); + return result.get(); + }, p); } } diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/PriorityQueueRunTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/PriorityQueueRunTest.java new file mode 100644 index 000000000000..0ac50fa8a2d6 --- /dev/null +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/PriorityQueueRunTest.java @@ -0,0 +1,162 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.lsp.server.protocol.PriorityQueueRun.Priority; +import org.openide.util.Exceptions; + +public class PriorityQueueRunTest extends NbTestCase { + + public PriorityQueueRunTest(String name) { + super(name); + } + + public void testSimpleExecute() throws Exception { + String normalInputData = new String("normal"); + int normalResultValue = -1; + String priorityInputData = new String("priority"); + int priorityResultValue = 1; + AtomicBoolean normalWaitCheckingForCancel = new AtomicBoolean(); + normalWaitCheckingForCancel.set(true); + CountDownLatch normalIsWaiting = new CountDownLatch(1); + List resultsInOrder = new ArrayList<>(); + List> pendingFutures = new ArrayList<>(); + pendingFutures.add( + PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, + (data, check) -> { + assertSame(normalInputData, data); + + while (normalWaitCheckingForCancel.get()) { + if (check.isCancelled()) { + return null; + } + normalIsWaiting.countDown(); + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + } + + return normalResultValue; + }, normalInputData) + .thenAccept(resultsInOrder::add)); + //wait until the task is run: + normalIsWaiting.await(); + pendingFutures.add( + PriorityQueueRun.getInstance() + .runTask(Priority.HIGH, + (data, check) -> { + assertSame(priorityInputData, data); + + normalWaitCheckingForCancel.set(false); + + return priorityResultValue; + }, priorityInputData) + .thenAccept(resultsInOrder::add)); + pendingFutures.forEach(cf -> { + try { + cf.get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + }); + + assertEquals(List.of(priorityResultValue, normalResultValue), resultsInOrder); + } + + public void testCancelBeforeRun() throws Exception { + CountDownLatch highPriorityIsWaiting = new CountDownLatch(1); + CountDownLatch highPriorityCanContinue = new CountDownLatch(1); + + PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, + (data, check) -> { + highPriorityIsWaiting.countDown(); + highPriorityCanContinue.await(); + return ""; + }, ""); + + PriorityQueueRun.getInstance() + .runTask(Priority.LOW, + (data, check) -> { + return ""; + }, "") + .cancel(true); + + CompletableFuture realResult = + PriorityQueueRun.getInstance() + .runTask(Priority.BELOW_LOW, + (data, check) -> { + return "real result"; + }, ""); + + highPriorityIsWaiting.await(); + highPriorityCanContinue.countDown(); + assertEquals("real result", realResult.get()); + } + + public void testCancelPatternNestCompletableFuture() throws Exception { + CountDownLatch lowPriorityIsRunning = new CountDownLatch(1); + CountDownLatch lowPriorityCanContinue = new CountDownLatch(1); + AtomicInteger cancelCount = new AtomicInteger(); + + CompletableFuture lowPriorityResult = + PriorityQueueRun.getInstance() + .runTask(Priority.LOW, + (data, check) -> { + lowPriorityIsRunning.countDown(); + lowPriorityCanContinue.await(); + CompletableFuture result = new CompletableFuture<>(); + check.registerCancel(() -> result.cancel(true)); + try { + result.complete(1); + return result.get(); + } catch (CancellationException ex) { + cancelCount.incrementAndGet(); + throw ex; + } + }, ""); + + lowPriorityIsRunning.await(); + + CompletableFuture normalPriorityResult = + PriorityQueueRun.getInstance() + .runTask(Priority.NORMAL, + (data, check) -> { + return "OK"; + }, ""); + + lowPriorityCanContinue.countDown(); + + assertEquals("OK", normalPriorityResult.get()); + assertEquals(1, (int) lowPriorityResult.get()); + } + +} 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 9205dd65fc92..85686ac4c819 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 @@ -56,6 +56,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -68,6 +69,7 @@ import javax.swing.event.ChangeListener; import javax.swing.text.Document; import javax.swing.text.StyledDocument; +import junit.framework.Test; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertNull; @@ -170,6 +172,7 @@ import org.netbeans.api.java.queries.AnnotationProcessingQuery.Result; import org.netbeans.api.java.queries.AnnotationProcessingQuery.Trigger; import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.SourceUtils; import org.netbeans.api.java.source.SourceUtilsTestUtil2; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.project.FileOwnerQuery; @@ -180,6 +183,7 @@ import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.api.sendopts.CommandLine; import org.netbeans.junit.NbTestCase; +import org.netbeans.junit.NbTestSuite; import org.netbeans.modules.java.hints.infrastructure.JavaErrorProvider; import static org.netbeans.modules.java.lsp.server.LspTestUtils.tripleSlashUri; import org.netbeans.modules.java.lsp.server.TestCodeLanguageClient; @@ -289,6 +293,16 @@ protected void setUp() throws Exception { SourceUtilsTestUtil2.disableMultiFileSourceRoots(); super.setUp(); clearWorkDir(); + //the test cases run consequentivelly, but in the same global state. + //Project(s) from the previous test cases may be open, and may cause + //subsequent test failures, if they are closed, trigger scan, and cause + //additional/duplicated diagnostic publish. + //rather close the projects, and wait for scan finished: + Future openProjects = OpenProjects.getDefault().openProjects(); + if (openProjects.isDone()) { + OpenProjects.getDefault().close(openProjects.get()); + SourceUtils.waitScanFinished(); + } ServerSocket srv = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); serverThread = new Thread(() -> { try { @@ -331,13 +345,13 @@ protected void tearDown() throws Exception { OpenProjects.getDefault().close(OpenProjects.getDefault().getOpenProjects()); } - final List[] diags = new List[1]; + final List> diags = new ArrayList<>(); Set diagnosticURIs = Collections.synchronizedSet(new HashSet<>()); void clearDiagnostics() { synchronized (diags) { diagnosticURIs.clear(); - diags[0] = null; + diags.clear(); } } @@ -349,8 +363,17 @@ void cancelDiagnostics(AtomicBoolean cancel) { } class LspClient implements LanguageClient { + private final String uri2CatchDiags; List loggedMessages = new ArrayList<>(); + public LspClient() { + this(null); + } + + public LspClient(String uri2CatchDiags) { + this.uri2CatchDiags = uri2CatchDiags; + } + @Override public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { return CompletableFuture.completedFuture(null); @@ -369,8 +392,11 @@ public void telemetryEvent(Object arg0) { public void publishDiagnostics(PublishDiagnosticsParams params) { synchronized (diags) { diagnosticURIs.add(params.getUri()); - diags[0] = params.getDiagnostics(); - diags.notifyAll(); + + if (uri2CatchDiags == null || uri2CatchDiags.equals(params.getUri())) { + diags.add(params.getDiagnostics()); + diags.notifyAll(); + } } } @@ -481,12 +507,13 @@ public void testDiagnosticsRemovedForDeletedFile() throws Exception { try (Writer w = new FileWriter(src)) { w.write(code); } - Launcher serverLauncher = createClientLauncherWithLogging(new LspClient(), client.getInputStream(), client.getOutputStream()); + Launcher serverLauncher = createClientLauncherWithLogging(new LspClient(src.toURI().toString()), client.getInputStream(), client.getOutputStream()); serverLauncher.startListening(); LanguageServer server = serverLauncher.getRemoteProxy(); InitializeResult result = server.initialize(new InitializeParams()).get(); server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(toURI(src), "java", 0, code))); assertDiags(diags, "Error:0:31-0:35");//errors + assertDiags(diags, "Error:0:31-0:35", "Warning:0:24-0:25");//warnings clearDiagnostics(); Files.move(src.toPath(), src.toPath().resolveSibling("Test2.java")); @@ -635,8 +662,18 @@ public void testDidOpenPreservesLines() throws Exception { assertTrue(hook.didCloseCompleted.tryAcquire(400, TimeUnit.MILLISECONDS)); Reference refDoc = new WeakReference<>(d3); d3 = null; - assertGC("Document should be collected", refDoc); - assertNull(cake.getDocument()); + + //wait until all background tasks are finished: + PriorityQueueRun.getInstance().testsWaitQueueEmpty(); + + StyledDocument doc = cake.getDocument(); + if (doc != null) { + //document might be reopened by background tasks called after close: + refDoc = new WeakReference<>(doc); + doc = null; + assertGC("Document should be collected", refDoc); + assertNull(cake.getDocument()); + } // open again server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(tdi)); @@ -700,7 +737,7 @@ public void testCodeActionWithRemoval() throws Exception { try (Writer w = new FileWriter(src)) { w.write(code); } - List[] diags = new List[1]; + List> diags = new ArrayList<>(); Launcher serverLauncher = createClientLauncherWithLogging(new LspClient() { @Override public void telemetryEvent(Object arg0) { @@ -709,7 +746,7 @@ public void telemetryEvent(Object arg0) { @Override public void publishDiagnostics(PublishDiagnosticsParams params) { synchronized (diags) { - diags[0] = params.getDiagnostics(); + diags.add(params.getDiagnostics()); diags.notifyAll(); } } @@ -755,13 +792,13 @@ public void logMessage(MessageParams arg0) { assertEquals("", edit.getNewText()); } - private List assertDiags(List[] diags, String... expected) { + private List assertDiags(List> diags, String... expected) { return assertDiags(diags, new AtomicBoolean(false), expected); } - private List assertDiags(List[] diags, AtomicBoolean cancel, String... expected) { + private List assertDiags(List> diags, AtomicBoolean cancel, String... expected) { synchronized (diags) { - while (diags[0] == null) { + while (diags.isEmpty()) { try { diags.wait(); } catch (InterruptedException ex) { @@ -771,22 +808,28 @@ private List assertDiags(List[] diags, AtomicBoolean can fail("Diagnostics not received"); } } - Set actualDiags = diags[0].stream() - .map(d -> d.getSeverity() + ":" + - d.getRange().getStart().getLine() + ":" + d.getRange().getStart().getCharacter() + "-" + - d.getRange().getEnd().getLine() + ":" + d.getRange().getEnd().getCharacter()) - .collect(Collectors.toSet()); - String diagsMessage = diags[0].stream() - .map(d -> d.getSeverity() + ":" + - d.getRange().getStart().getLine() + ":" + d.getRange().getStart().getCharacter() + "-" + - d.getRange().getEnd().getLine() + ":" + d.getRange().getEnd().getCharacter() + ": " + - d.getMessage()) - .collect(Collectors.joining("\n")); - assertEquals(diagsMessage, new HashSet<>(Arrays.asList(expected)), actualDiags); - List result = diags[0]; + Function diag2DebugString = + d -> d.getSeverity() + ":" + + d.getRange().getStart().getLine() + ":" + d.getRange().getStart().getCharacter() + "-" + + d.getRange().getEnd().getLine() + ":" + d.getRange().getEnd().getCharacter() + ": " + + d.getMessage(); + + String diagsMessage = diags.stream() + .map(result -> result.stream() + .map(diag2DebugString) + .collect(Collectors.joining(", ", "[", "]"))) + .collect(Collectors.joining("\n")); + + List result = diags.remove(0); + + Set actualDiags = result.stream() + .map(d -> d.getSeverity() + ":" + + d.getRange().getStart().getLine() + ":" + d.getRange().getStart().getCharacter() + "-" + + d.getRange().getEnd().getLine() + ":" + d.getRange().getEnd().getCharacter()) + .collect(Collectors.toSet()); - diags[0] = null; + assertEquals(diagsMessage, new HashSet<>(Arrays.asList(expected)), actualDiags); return result; } @@ -1261,7 +1304,7 @@ public void testOpenProjectOpenJDK() throws Exception { w.write("module java.compiler { }"); } - List[] diags = new List[1]; + List> diags = new ArrayList<>(); boolean[] indexingComplete = new boolean[1]; Launcher serverLauncher = createClientLauncherWithLogging(new LspClient() { @Override @@ -1271,7 +1314,7 @@ public void telemetryEvent(Object arg0) { @Override public void publishDiagnostics(PublishDiagnosticsParams params) { synchronized (diags) { - diags[0] = params.getDiagnostics(); + diags.add(params.getDiagnostics()); diags.notifyAll(); } } @@ -1739,7 +1782,7 @@ public void testFixImports() throws Exception { try (Writer w = new FileWriter(src)) { w.write(code); } - List[] diags = new List[1]; + List> diags = new ArrayList<>(); AtomicBoolean checkForDiags = new AtomicBoolean(false); CountDownLatch indexingComplete = new CountDownLatch(1); Launcher serverLauncher = createClientLauncherWithLogging(new TestCodeLanguageClient() { @@ -1754,7 +1797,7 @@ public void showStatusBarMessage(ShowStatusMessageParams params) { public void publishDiagnostics(PublishDiagnosticsParams params) { if (checkForDiags.get()) { synchronized (diags) { - diags[0] = params.getDiagnostics(); + diags.add(params.getDiagnostics()); diags.notifyAll(); } } @@ -5852,14 +5895,14 @@ public void testFileModificationDiags() throws Exception { synchronized (diags) { long timeout = 1000; long start = System.currentTimeMillis(); - while (diags[0] == null && (System.currentTimeMillis() - start) < timeout) { + while (diags.isEmpty() && (System.currentTimeMillis() - start) < timeout) { try { diags.wait(timeout / 10); } catch (InterruptedException ex) { //ignore } } - assertNull(diags[0]); + assertTrue(diags.isEmpty()); } server.getTextDocumentService().didChange(new DidChangeTextDocumentParams(id, Arrays.asList(new TextDocumentContentChangeEvent(new Range(new Position(2, 1), new Position(2, 1)), 0, " \n ")))); assertDiags(diags, "Warning:4:15-4:16");//errors @@ -5964,7 +6007,7 @@ public void telemetryEvent(Object arg0) { @Override public void publishDiagnostics(PublishDiagnosticsParams params) { synchronized (diags) { - diags[0] = params.getDiagnostics(); + diags.add(params.getDiagnostics()); diags.notifyAll(); } }