extension/src: support stream $/progress message in log style
If the $/progress's begin message start with style: log, the
all message for this progress token will be streamed to a
terminal.
Because executeCommand extends workDoneProgress, if response
from executeCommand contains progress token, the result will
be streamed to the same terminal.
Gopls side change CL 645695.
For golang/vscode-go#3572
Change-Id: I3ad4db2604423a2285a7c0f57b8a4d66d2c1933a
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/645116
kokoro-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Madeline Kalil <mkalil@google.com>
diff --git a/extension/src/goVulncheck.ts b/extension/src/goVulncheck.ts
index d9d0f74..dddb43f 100644
--- a/extension/src/goVulncheck.ts
+++ b/extension/src/goVulncheck.ts
@@ -7,57 +7,7 @@
import { URI } from 'vscode-uri';
import { getGoConfig } from './config';
import { getWorkspaceFolderPath } from './util';
-
-export interface IVulncheckTerminal {
- appendLine: (str: string) => void;
- show: (preserveFocus?: boolean) => void;
- exit: () => void;
-}
-export class VulncheckTerminal implements IVulncheckTerminal {
- private term: vscode.Terminal;
- private writeEmitter = new vscode.EventEmitter<string>();
-
- // Buffer messages emitted before vscode is ready. VSC calls pty.open when it is ready.
- private ptyReady = false;
- private buf: string[] = [];
-
- // Constructor function to stub during test.
- static Open(): IVulncheckTerminal {
- return new VulncheckTerminal();
- }
-
- private constructor() {
- const pty: vscode.Pseudoterminal = {
- onDidWrite: this.writeEmitter.event,
- handleInput: () => this.exit(),
- open: () => {
- this.ptyReady = true;
- this.buf.forEach((l) => this.writeEmitter.fire(l));
- this.buf = [];
- },
- close: () => {}
- };
- this.term = vscode.window.createTerminal({ name: 'govulncheck', pty }); // TODO: iconPath
- }
-
- appendLine(str: string) {
- if (!str.endsWith('\n')) {
- str += '\n';
- }
- str = str.replace(/\n/g, '\n\r'); // replaceAll('\n', '\n\r').
- if (!this.ptyReady) {
- this.buf.push(str); // print when `open` is called.
- } else {
- this.writeEmitter.fire(str);
- }
- }
-
- show(preserveFocus?: boolean) {
- this.term.show(preserveFocus);
- }
-
- exit() {}
-}
+import { IProgressTerminal } from './progressTerminal';
// VulncheckReport is the JSON data type of gopls's vulncheck result.
export interface VulncheckReport {
@@ -72,7 +22,7 @@
export async function writeVulns(
res: VulncheckReport,
- term: IVulncheckTerminal | undefined,
+ term: IProgressTerminal | undefined,
goplsBinPath: string
): Promise<void> {
if (term === undefined) {
diff --git a/extension/src/language/goLanguageServer.ts b/extension/src/language/goLanguageServer.ts
index 1f32c3c..ae70283 100644
--- a/extension/src/language/goLanguageServer.ts
+++ b/extension/src/language/goLanguageServer.ts
@@ -14,6 +14,7 @@
import semver = require('semver');
import util = require('util');
import vscode = require('vscode');
+import { InitializeParams, LSPAny, LSPObject } from 'vscode-languageserver-protocol';
import {
CancellationToken,
CloseAction,
@@ -59,7 +60,8 @@
import { CommandFactory } from '../commands';
import { updateLanguageServerIconGoStatusBar } from '../goStatus';
import { URI } from 'vscode-uri';
-import { IVulncheckTerminal, VulncheckReport, VulncheckTerminal, writeVulns } from '../goVulncheck';
+import { VulncheckReport, writeVulns } from '../goVulncheck';
+import { ActiveProgressTerminals, IProgressTerminal, ProgressTerminal } from '../progressTerminal';
import { createHash } from 'crypto';
import { GoExtensionContext } from '../context';
import { GoDocumentSelector } from '../goMode';
@@ -379,9 +381,23 @@
this.onDidChangeVulncheckResultEmitter.dispose();
return super.dispose(timeout);
}
+
public get onDidChangeVulncheckResult(): vscode.Event<VulncheckEvent> {
return this.onDidChangeVulncheckResultEmitter.event;
}
+
+ protected fillInitializeParams(params: InitializeParams): void {
+ super.fillInitializeParams(params);
+
+ // VSCode-Go honors most client capabilities from the vscode-languageserver-node
+ // library. Experimental capabilities not used by vscode-languageserver-node
+ // can be used for custom communication between vscode-go and gopls.
+ // See https://github.com/microsoft/vscode-languageserver-node/issues/1607
+ const experimental: LSPObject = {
+ progressMessageStyles: ['log']
+ };
+ params.capabilities.experimental = experimental;
+ }
}
type VulncheckEvent = {
@@ -402,10 +418,12 @@
// we want to handle the connection close error case specially. Capture the error
// in initializationFailedHandler and handle it in the connectionCloseHandler.
let initializationError: ResponseError<InitializeError> | undefined = undefined;
- let govulncheckTerminal: IVulncheckTerminal | undefined;
+ // TODO(hxjiang): deprecate special handling for async call gopls.run_govulncheck.
+ let govulncheckTerminal: IProgressTerminal | undefined;
const pendingVulncheckProgressToken = new Map<ProgressToken, any>();
const onDidChangeVulncheckResultEmitter = new vscode.EventEmitter<VulncheckEvent>();
+
// cfg is captured by closures for later use during error report.
const c = new GoLanguageClient(
'go', // id
@@ -489,13 +507,34 @@
handleWorkDoneProgress: async (token, params, next) => {
switch (params.kind) {
case 'begin':
+ if (typeof params.message === 'string') {
+ const paragraphs = params.message.split('\n\n', 2);
+ const metadata = paragraphs[0].trim();
+ if (!metadata.startsWith('style: ')) {
+ break;
+ }
+ const style = metadata.substring('style: '.length);
+ if (style === 'log') {
+ const term = ProgressTerminal.Open(params.title, token);
+ if (paragraphs.length > 1) {
+ term.appendLine(paragraphs[1]);
+ }
+ term.show();
+ }
+ }
break;
case 'report':
+ if (params.message) {
+ ActiveProgressTerminals.get(token)?.appendLine(params.message);
+ }
if (pendingVulncheckProgressToken.has(token) && params.message) {
govulncheckTerminal?.appendLine(params.message);
}
break;
case 'end':
+ if (params.message) {
+ ActiveProgressTerminals.get(token)?.appendLine(params.message);
+ }
if (pendingVulncheckProgressToken.has(token)) {
const out = pendingVulncheckProgressToken.get(token);
pendingVulncheckProgressToken.delete(token);
@@ -507,7 +546,7 @@
},
executeCommand: async (command: string, args: any[], next: ExecuteCommandSignature) => {
try {
- if (command === 'gopls.tidy') {
+ if (command === 'gopls.tidy' || command === 'gopls.vulncheck') {
await vscode.workspace.saveAll(false);
}
if (command === 'gopls.run_govulncheck' && args.length && args[0].URI) {
@@ -520,17 +559,35 @@
await vscode.workspace.saveAll(false);
const uri = args[0].URI ? URI.parse(args[0].URI) : undefined;
const dir = uri?.fsPath?.endsWith('.mod') ? path.dirname(uri.fsPath) : uri?.fsPath;
- govulncheckTerminal = VulncheckTerminal.Open();
+ govulncheckTerminal = ProgressTerminal.Open('govulncheck');
govulncheckTerminal.appendLine(`⚡ govulncheck -C ${dir} ./...\n\n`);
govulncheckTerminal.show();
}
const res = await next(command, args);
- if (command === 'gopls.run_govulncheck') {
- const progressToken = res.Token;
- if (progressToken) {
- pendingVulncheckProgressToken.set(progressToken, args[0]);
+
+ const progressToken = <ProgressToken>res.Token;
+ // The progressToken from executeCommand indicates that
+ // gopls may trigger a related workDoneProgress
+ // notification, either before or after the command
+ // completes.
+ // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverInitiatedProgress
+ if (progressToken !== undefined) {
+ switch (command) {
+ case 'gopls.run_govulncheck':
+ pendingVulncheckProgressToken.set(progressToken, args[0]);
+ break;
+ case 'gopls.vulncheck':
+ // Write the vulncheck report to the terminal.
+ if (ActiveProgressTerminals.has(progressToken)) {
+ writeVulns(res.Result, ActiveProgressTerminals.get(progressToken), cfg.path);
+ }
+ break;
+ default:
+ // By default, dump the result to the terminal.
+ ActiveProgressTerminals.get(progressToken)?.appendLine(res.Result);
}
}
+
return res;
} catch (e) {
// TODO: how to print ${e} reliably???
diff --git a/extension/src/progressTerminal.ts b/extension/src/progressTerminal.ts
new file mode 100644
index 0000000..aaf482a
--- /dev/null
+++ b/extension/src/progressTerminal.ts
@@ -0,0 +1,80 @@
+/*---------------------------------------------------------
+ * Copyright 2025 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+import vscode = require('vscode');
+
+import { ProgressToken } from 'vscode-languageclient';
+
+// ActiveProgressTerminals maps progress tokens to their corresponding terminals.
+// Entries are added when terminals are created for workdone progress and
+// deleted when closed by the user, which is interpreted as the user discarding
+// any further information.
+// There is no guarantee a terminal will remain available for the entire
+// duration of a workdone progress notification.
+// Logs can be appended to the terminal even after the workdone progress
+// notification finishes, allowing responses from requests extending
+// WorkDoneProgressOptions to be displayed in the same terminal.
+export const ActiveProgressTerminals = new Map<ProgressToken, IProgressTerminal>();
+
+export interface IProgressTerminal {
+ appendLine: (str: string) => void;
+ show: (preserveFocus?: boolean) => void;
+ exit: () => void;
+}
+export class ProgressTerminal implements IProgressTerminal {
+ private progressToken?: ProgressToken;
+ private term: vscode.Terminal;
+ private writeEmitter = new vscode.EventEmitter<string>();
+
+ // Buffer messages emitted before vscode is ready. VSC calls pty.open when it is ready.
+ private ptyReady = false;
+ private buf: string[] = [];
+
+ // Constructor function to stub during test.
+ static Open(name = 'progress', token?: ProgressToken): IProgressTerminal {
+ return new ProgressTerminal(name, token);
+ }
+
+ // ProgressTerminal created with token will be managed by map
+ // ActiveProgressTerminals.
+ private constructor(name: string, token?: ProgressToken) {
+ const pty: vscode.Pseudoterminal = {
+ onDidWrite: this.writeEmitter.event,
+ handleInput: () => this.exit(),
+ open: () => {
+ this.ptyReady = true;
+ this.buf.forEach((l) => this.writeEmitter.fire(l));
+ this.buf = [];
+ },
+ close: () => {
+ if (this.progressToken !== undefined) {
+ ActiveProgressTerminals.delete(this.progressToken);
+ }
+ }
+ };
+ this.term = vscode.window.createTerminal({ name: name, pty }); // TODO: iconPath
+ if (token !== undefined) {
+ this.progressToken = token;
+ ActiveProgressTerminals.set(this.progressToken, this);
+ }
+ }
+
+ appendLine(str: string) {
+ if (!str.endsWith('\n')) {
+ str += '\n';
+ }
+ str = str.replace(/\n/g, '\n\r'); // replaceAll('\n', '\n\r').
+ if (!this.ptyReady) {
+ this.buf.push(str); // print when `open` is called.
+ } else {
+ this.writeEmitter.fire(str);
+ }
+ }
+
+ show(preserveFocus?: boolean) {
+ this.term.show(preserveFocus);
+ }
+
+ exit() {}
+}
diff --git a/extension/test/gopls/vulncheck.test.ts b/extension/test/gopls/vulncheck.test.ts
index 7c92ea6..7048b20 100644
--- a/extension/test/gopls/vulncheck.test.ts
+++ b/extension/test/gopls/vulncheck.test.ts
@@ -6,7 +6,7 @@
import path = require('path');
import sinon = require('sinon');
import vscode = require('vscode');
-import { VulncheckTerminal } from '../../src/goVulncheck';
+import { ProgressTerminal } from '../../src/progressTerminal';
import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageserver-protocol';
import { Env, FakeOutputChannel } from './goplsTestEnv.utils';
import { URI } from 'vscode-uri';
@@ -34,7 +34,7 @@
sandbox.stub(config, 'getGoConfig').returns(goConfig);
await env.startGopls(undefined, goConfig, fixtureDir);
- sandbox.stub(VulncheckTerminal, 'Open').returns({
+ sandbox.stub(ProgressTerminal, 'Open').returns({
appendLine: fakeTerminal.appendLine,
show: () => {},
exit: () => {}