From 73902becacbbc0d02966173496a71fca39b80d2b Mon Sep 17 00:00:00 2001 From: Jan Horvath Date: Thu, 28 Nov 2024 08:32:08 +0100 Subject: [PATCH] Create new output channel for every LspIO --- .../server/protocol/NbCodeClientWrapper.java | 21 ++++ .../server/protocol/NbCodeLanguageClient.java | 12 ++ .../lsp/server/protocol/OutputMessage.java | 37 ++++++ .../java/lsp/server/protocol/Server.java | 21 ++++ .../ui/AbstractLspInputOutputProvider.java | 30 ++++- .../lsp/server/TestCodeLanguageClient.java | 22 ++++ .../lsp/server/explorer/ProjectViewTest.java | 21 ++++ java/java.lsp.server/vscode/src/extension.ts | 119 +++++++++++++++++- java/java.lsp.server/vscode/src/protocol.ts | 22 ++++ 9 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OutputMessage.java diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java index 7f633df6758e..a0c4f672ca32 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java @@ -223,4 +223,25 @@ public CompletableFuture configurationUpdate(UpdateConfigParams params) { public CompletableFuture requestDocumentSave(SaveDocumentRequestParams documentUris) { return remote.requestDocumentSave(documentUris); } + + @Override + public CompletableFuture writeOutput(OutputMessage lm) { + return remote.writeOutput(lm); + } + + @Override + public CompletableFuture showOutput(String outputName) { + return remote.showOutput(outputName); + } + + @Override + public CompletableFuture closeOutput(String outputName) { + return remote.closeOutput(outputName); + } + + @Override + public CompletableFuture resetOutput(String outputName) { + return remote.resetOutput(outputName); + } + } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java index 1c44d56fa5fe..767edb696c28 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java @@ -154,4 +154,16 @@ public default boolean isRequestDispatcherThread() { @JsonRequest("window/documentSave") public CompletableFuture requestDocumentSave(@NonNull SaveDocumentRequestParams documentUri); + @JsonRequest("output/write") + public CompletableFuture writeOutput(OutputMessage message); + + @JsonRequest("output/show") + public CompletableFuture showOutput(String outputName); + + @JsonRequest("output/close") + public CompletableFuture closeOutput(String outputName); + + @JsonRequest("output/reset") + public CompletableFuture resetOutput(String outputName); + } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OutputMessage.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OutputMessage.java new file mode 100644 index 000000000000..63e953f0b88b --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OutputMessage.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * + * @author Jan Horvath + */ +public final class OutputMessage { + + String outputName; + String message; + boolean stdIO; + + public OutputMessage(String outputName, String message, boolean stdIO) { + this.outputName = outputName; + this.message = message; + this.stdIO = stdIO; + } + +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java index 120c8590877b..6060385d63c7 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java @@ -1358,6 +1358,27 @@ public CompletableFuture requestDocumentSave(SaveDocumentRequestParams logWarning(Arrays.asList(documentUris)); return CompletableFuture.completedFuture(false); } + + @Override + public CompletableFuture writeOutput(OutputMessage lm) { + logWarning(lm.message); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture showOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture closeOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture resetOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } }; diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspInputOutputProvider.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspInputOutputProvider.java index 34c3407af843..aa778d9e3926 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspInputOutputProvider.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspInputOutputProvider.java @@ -18,6 +18,7 @@ */ package org.netbeans.modules.java.lsp.server.ui; +import org.netbeans.modules.java.lsp.server.protocol.OutputMessage; import java.io.CharArrayReader; import java.io.IOException; import java.io.InputStream; @@ -29,6 +30,8 @@ import org.netbeans.api.io.Hyperlink; import org.netbeans.api.io.OutputColor; import org.netbeans.api.io.ShowOperation; +import org.netbeans.modules.java.lsp.server.LspServerUtils; +import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient; import org.netbeans.modules.java.lsp.server.ui.AbstractLspInputOutputProvider.LspIO; import org.netbeans.spi.io.InputOutputProvider; import org.openide.util.Lookup; @@ -82,14 +85,26 @@ public final Lookup getIOLookup(LspIO io) { @Override public final void resetIO(LspIO io) { + NbCodeLanguageClient client = LspServerUtils.findLspClient(Lookup.getDefault()); + if (client != null) { + client.resetOutput(io.name); + } } @Override public final void showIO(LspIO io, Set operations) { + NbCodeLanguageClient client = LspServerUtils.findLspClient(Lookup.getDefault()); + if (client != null) { + client.showOutput(io.name); + } } @Override public final void closeIO(LspIO io) { + NbCodeLanguageClient client = LspServerUtils.findLspClient(Lookup.getDefault()); + if (client != null) { + client.closeOutput(io.name); + } } @Override @@ -131,6 +146,7 @@ public final void setIODescription(LspIO io, String description) { public static final class LspIO { private final String name; private final IOContext ctx; + private final NbCodeLanguageClient client; final Lookup lookup; final Reader in; final LspWriter out; @@ -164,12 +180,16 @@ public void close() { }; } this.in = in; + client = LspServerUtils.findLspClient(Lookup.getDefault()); + if (client != null) { + client.resetOutput(name); + } } boolean isClosed() { return out.closed && err.closed; } - + private final class LspWriter extends Writer { private final boolean stdIO; volatile boolean closed; @@ -181,10 +201,8 @@ private final class LspWriter extends Writer { @Override public void write(char[] cbuf, int off, int len) throws IOException { String chunk = new String(cbuf, off, len); - if (stdIO) { - ctx.stdOut(chunk); - } else { - ctx.stdErr(chunk); + if (len > 0) { + client.writeOutput(new OutputMessage(name, chunk, stdIO)); } } @@ -198,5 +216,5 @@ public void close() throws IOException { } } } - + } diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/TestCodeLanguageClient.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/TestCodeLanguageClient.java index 489670f57254..31f9a128ece6 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/TestCodeLanguageClient.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/TestCodeLanguageClient.java @@ -43,6 +43,7 @@ import org.netbeans.modules.java.lsp.server.input.ShowInputBoxParams; import org.netbeans.modules.java.lsp.server.input.ShowMutliStepInputParams; import org.netbeans.modules.java.lsp.server.input.ShowQuickPickParams; +import org.netbeans.modules.java.lsp.server.protocol.OutputMessage; import org.netbeans.modules.java.lsp.server.protocol.SaveDocumentRequestParams; import org.netbeans.modules.java.lsp.server.protocol.ShowStatusMessageParams; import org.netbeans.modules.java.lsp.server.protocol.TestProgressParams; @@ -160,4 +161,25 @@ public CompletableFuture configurationUpdate(UpdateConfigParams params) { public CompletableFuture requestDocumentSave(SaveDocumentRequestParams documentUris) { return CompletableFuture.completedFuture(false); } + + @Override + public CompletableFuture writeOutput(OutputMessage message) { + System.out.println(message); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture showOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture closeOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture resetOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } } diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/explorer/ProjectViewTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/explorer/ProjectViewTest.java index 592ad8a726b2..a8edbcb45bb9 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/explorer/ProjectViewTest.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/explorer/ProjectViewTest.java @@ -77,6 +77,7 @@ import org.netbeans.modules.java.lsp.server.input.ShowInputBoxParams; import org.netbeans.modules.java.lsp.server.input.ShowMutliStepInputParams; import org.netbeans.modules.java.lsp.server.input.ShowQuickPickParams; +import org.netbeans.modules.java.lsp.server.protocol.OutputMessage; import org.netbeans.modules.java.lsp.server.protocol.SaveDocumentRequestParams; import org.netbeans.modules.java.lsp.server.protocol.SetTextEditorDecorationParams; import org.netbeans.modules.java.lsp.server.protocol.ShowStatusMessageParams; @@ -285,6 +286,26 @@ public CompletableFuture configurationUpdate(UpdateConfigParams params) { public CompletableFuture requestDocumentSave(SaveDocumentRequestParams documentUris) { return CompletableFuture.completedFuture(false); } + + @Override + public CompletableFuture writeOutput(OutputMessage message) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture showOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture closeOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture resetOutput(String outputName) { + return CompletableFuture.completedFuture(null); + } } private static Launcher createLauncher(NbCodeLanguageClient client, InputStream in, OutputStream out, diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts index 8db683de5091..7fa9750b5c73 100644 --- a/java/java.lsp.server/vscode/src/extension.ts +++ b/java/java.lsp.server/vscode/src/extension.ts @@ -52,7 +52,7 @@ import * as launcher from './nbcode'; import {NbTestAdapter} from './testAdapter'; import { asRanges, StatusMessageRequest, ShowStatusMessageParams, QuickPickRequest, InputBoxRequest, MutliStepInputRequest, TestProgressNotification, DebugConnector, TextEditorDecorationCreateRequest, TextEditorDecorationSetNotification, TextEditorDecorationDisposeNotification, HtmlPageRequest, HtmlPageParams, - ExecInHtmlPageRequest, SetTextEditorDecorationParams, ProjectActionParams, UpdateConfigurationRequest, QuickPickStep, InputBoxStep, SaveDocumentsRequest, SaveDocumentRequestParams + ExecInHtmlPageRequest, SetTextEditorDecorationParams, ProjectActionParams, UpdateConfigurationRequest, QuickPickStep, InputBoxStep, SaveDocumentsRequest, SaveDocumentRequestParams, OutputMessage, WriteOutputRequest, ShowOutputRequest, CloseOutputRequest, ResetOutputRequest } from './protocol'; import * as launchConfigurations from './launchConfigurations'; import { createTreeViewService, TreeViewService, TreeItemDecorator, Visualizer, CustomizableTreeDataProvider } from './explorer'; @@ -377,6 +377,107 @@ function getValueAfterPrefix(input: string | undefined, prefix: string): string return ''; } +class LineBufferingPseudoterminal implements vscode.Pseudoterminal { + private static instances = new Map(); + + private writeEmitter = new vscode.EventEmitter(); + onDidWrite: vscode.Event = this.writeEmitter.event; + + private closeEmitter = new vscode.EventEmitter(); + onDidClose?: vscode.Event = this.closeEmitter.event; + + private buffer: string = ''; + private isOpen = false; + private readonly name: string; + private terminal: vscode.Terminal | undefined; + + private constructor(name: string) { + this.name = name; + } + + open(): void { + this.isOpen = true; + } + + close(): void { + this.isOpen = false; + this.closeEmitter.fire(); + } + + /** + * Accepts partial input strings and logs complete lines when they are formed. + * Also processes carriage returns (\r) to overwrite the current line. + * @param input The string input to the pseudoterminal. + */ + public acceptInput(input: string): void { + if (!this.isOpen) { + return; + } + + for (const char of input) { + if (char === '\n') { + // Process a newline: log the current buffer and reset it + this.logLine(this.buffer.trim()); + this.buffer = ''; + } else if (char === '\r') { + // Process a carriage return: log the current buffer on the same line + this.logInline(this.buffer.trim()); + this.buffer = ''; + } else { + // Append characters to the buffer + this.buffer += char; + } + } + } + + private logLine(line: string): void { + console.log('[Gradle Debug]', line.toString()); + this.writeEmitter.fire(`${line}\r\n`); + } + + private logInline(line: string): void { + // Clear the current line and move the cursor to the start + this.writeEmitter.fire(`\x1b[2K\x1b[1G${line}`); + } + + public flushBuffer(): void { + if (this.buffer.trim().length > 0) { + this.logLine(this.buffer.trim()); + this.buffer = ''; + } + } + + public clear(): void { + this.writeEmitter.fire('\x1b[2J\x1b[3J\x1b[H'); // Clear screen and move cursor to top-left + } + + public show(): void { + if (!this.terminal) { + this.terminal = vscode.window.createTerminal({ + name: this.name, + pty: this, + }); + } + this.terminal.show(); + } + + /** + * Gets an existing instance or creates a new one by the terminal name. + * The terminal is also created and managed internally. + * @param name The name of the pseudoterminal. + * @returns The instance of the pseudoterminal. + */ + public static getInstance(name: string): LineBufferingPseudoterminal { + if (!this.instances.has(name)) { + const instance = new LineBufferingPseudoterminal(name); + this.instances.set(name, instance); + } + const instance = this.instances.get(name)!; + instance.show(); + return instance; + } +} + export function activate(context: ExtensionContext): VSNetBeansAPI { const provider = new StringContentProvider(); const scheme = 'in-memory'; @@ -1508,6 +1609,22 @@ function doActivateWithJDK(specifiedJDK: string | null, context: ExtensionContex decorations.set(decorationType.key, decorationType); return decorationType.key; }); + c.onRequest(WriteOutputRequest.type, param => { + const outputTerminal = LineBufferingPseudoterminal.getInstance(param.outputName) + outputTerminal.acceptInput(param.message); + }); + c.onRequest(ShowOutputRequest.type, param => { + const outputTerminal = LineBufferingPseudoterminal.getInstance(param) + outputTerminal.show(); + }); + c.onRequest(CloseOutputRequest.type, param => { + const outputTerminal = LineBufferingPseudoterminal.getInstance(param) + outputTerminal.close(); + }); + c.onRequest(ResetOutputRequest.type, param => { + const outputTerminal = LineBufferingPseudoterminal.getInstance(param) + outputTerminal.clear(); + }); c.onNotification(TextEditorDecorationSetNotification.type, param => { let decorationType = decorations.get(param.key); if (decorationType) { diff --git a/java/java.lsp.server/vscode/src/protocol.ts b/java/java.lsp.server/vscode/src/protocol.ts index fbe70181121c..0a1856f0ed40 100644 --- a/java/java.lsp.server/vscode/src/protocol.ts +++ b/java/java.lsp.server/vscode/src/protocol.ts @@ -323,3 +323,25 @@ export function asRange(value: Range | undefined | null): vscode.Range | undefin export function asRanges(value: Range[]): vscode.Range[] { return value.map(value => asRange(value)); } + +export interface OutputMessage { + outputName: string; + message: string; + stdIO: boolean; +} + +export namespace WriteOutputRequest { + export const type = new ProtocolRequestType('output/write'); +} + +export namespace ShowOutputRequest { + export const type = new ProtocolRequestType('output/show'); +} + +export namespace CloseOutputRequest { + export const type = new ProtocolRequestType('output/close'); +} + +export namespace ResetOutputRequest { + export const type = new ProtocolRequestType('output/reset'); +}