Skip to content

Commit 84fc37a

Browse files
committed
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 #3572 Change-Id: I3ad4db2604423a2285a7c0f57b8a4d66d2c1933a Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/645116 kokoro-CI: kokoro <[email protected]> Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Madeline Kalil <[email protected]>
1 parent 0575f50 commit 84fc37a

File tree

4 files changed

+149
-62
lines changed

4 files changed

+149
-62
lines changed

extension/src/goVulncheck.ts

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,7 @@ import cp = require('child_process');
77
import { URI } from 'vscode-uri';
88
import { getGoConfig } from './config';
99
import { getWorkspaceFolderPath } from './util';
10-
11-
export interface IVulncheckTerminal {
12-
appendLine: (str: string) => void;
13-
show: (preserveFocus?: boolean) => void;
14-
exit: () => void;
15-
}
16-
export class VulncheckTerminal implements IVulncheckTerminal {
17-
private term: vscode.Terminal;
18-
private writeEmitter = new vscode.EventEmitter<string>();
19-
20-
// Buffer messages emitted before vscode is ready. VSC calls pty.open when it is ready.
21-
private ptyReady = false;
22-
private buf: string[] = [];
23-
24-
// Constructor function to stub during test.
25-
static Open(): IVulncheckTerminal {
26-
return new VulncheckTerminal();
27-
}
28-
29-
private constructor() {
30-
const pty: vscode.Pseudoterminal = {
31-
onDidWrite: this.writeEmitter.event,
32-
handleInput: () => this.exit(),
33-
open: () => {
34-
this.ptyReady = true;
35-
this.buf.forEach((l) => this.writeEmitter.fire(l));
36-
this.buf = [];
37-
},
38-
close: () => {}
39-
};
40-
this.term = vscode.window.createTerminal({ name: 'govulncheck', pty }); // TODO: iconPath
41-
}
42-
43-
appendLine(str: string) {
44-
if (!str.endsWith('\n')) {
45-
str += '\n';
46-
}
47-
str = str.replace(/\n/g, '\n\r'); // replaceAll('\n', '\n\r').
48-
if (!this.ptyReady) {
49-
this.buf.push(str); // print when `open` is called.
50-
} else {
51-
this.writeEmitter.fire(str);
52-
}
53-
}
54-
55-
show(preserveFocus?: boolean) {
56-
this.term.show(preserveFocus);
57-
}
58-
59-
exit() {}
60-
}
10+
import { IProgressTerminal } from './progressTerminal';
6111

6212
// VulncheckReport is the JSON data type of gopls's vulncheck result.
6313
export interface VulncheckReport {
@@ -72,7 +22,7 @@ export interface VulncheckReport {
7222

7323
export async function writeVulns(
7424
res: VulncheckReport,
75-
term: IVulncheckTerminal | undefined,
25+
term: IProgressTerminal | undefined,
7626
goplsBinPath: string
7727
): Promise<void> {
7828
if (term === undefined) {

extension/src/language/goLanguageServer.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import path = require('path');
1414
import semver = require('semver');
1515
import util = require('util');
1616
import vscode = require('vscode');
17+
import { InitializeParams, LSPAny, LSPObject } from 'vscode-languageserver-protocol';
1718
import {
1819
CancellationToken,
1920
CloseAction,
@@ -59,7 +60,8 @@ import { maybePromptForDeveloperSurvey } from '../goDeveloperSurvey';
5960
import { CommandFactory } from '../commands';
6061
import { updateLanguageServerIconGoStatusBar } from '../goStatus';
6162
import { URI } from 'vscode-uri';
62-
import { IVulncheckTerminal, VulncheckReport, VulncheckTerminal, writeVulns } from '../goVulncheck';
63+
import { VulncheckReport, writeVulns } from '../goVulncheck';
64+
import { ActiveProgressTerminals, IProgressTerminal, ProgressTerminal } from '../progressTerminal';
6365
import { createHash } from 'crypto';
6466
import { GoExtensionContext } from '../context';
6567
import { GoDocumentSelector } from '../goMode';
@@ -379,9 +381,23 @@ export class GoLanguageClient extends LanguageClient implements vscode.Disposabl
379381
this.onDidChangeVulncheckResultEmitter.dispose();
380382
return super.dispose(timeout);
381383
}
384+
382385
public get onDidChangeVulncheckResult(): vscode.Event<VulncheckEvent> {
383386
return this.onDidChangeVulncheckResultEmitter.event;
384387
}
388+
389+
protected fillInitializeParams(params: InitializeParams): void {
390+
super.fillInitializeParams(params);
391+
392+
// VSCode-Go honors most client capabilities from the vscode-languageserver-node
393+
// library. Experimental capabilities not used by vscode-languageserver-node
394+
// can be used for custom communication between vscode-go and gopls.
395+
// See https://github.com/microsoft/vscode-languageserver-node/issues/1607
396+
const experimental: LSPObject = {
397+
progressMessageStyles: ['log']
398+
};
399+
params.capabilities.experimental = experimental;
400+
}
385401
}
386402

387403
type VulncheckEvent = {
@@ -402,10 +418,12 @@ export async function buildLanguageClient(
402418
// we want to handle the connection close error case specially. Capture the error
403419
// in initializationFailedHandler and handle it in the connectionCloseHandler.
404420
let initializationError: ResponseError<InitializeError> | undefined = undefined;
405-
let govulncheckTerminal: IVulncheckTerminal | undefined;
406421

422+
// TODO(hxjiang): deprecate special handling for async call gopls.run_govulncheck.
423+
let govulncheckTerminal: IProgressTerminal | undefined;
407424
const pendingVulncheckProgressToken = new Map<ProgressToken, any>();
408425
const onDidChangeVulncheckResultEmitter = new vscode.EventEmitter<VulncheckEvent>();
426+
409427
// cfg is captured by closures for later use during error report.
410428
const c = new GoLanguageClient(
411429
'go', // id
@@ -489,13 +507,34 @@ export async function buildLanguageClient(
489507
handleWorkDoneProgress: async (token, params, next) => {
490508
switch (params.kind) {
491509
case 'begin':
510+
if (typeof params.message === 'string') {
511+
const paragraphs = params.message.split('\n\n', 2);
512+
const metadata = paragraphs[0].trim();
513+
if (!metadata.startsWith('style: ')) {
514+
break;
515+
}
516+
const style = metadata.substring('style: '.length);
517+
if (style === 'log') {
518+
const term = ProgressTerminal.Open(params.title, token);
519+
if (paragraphs.length > 1) {
520+
term.appendLine(paragraphs[1]);
521+
}
522+
term.show();
523+
}
524+
}
492525
break;
493526
case 'report':
527+
if (params.message) {
528+
ActiveProgressTerminals.get(token)?.appendLine(params.message);
529+
}
494530
if (pendingVulncheckProgressToken.has(token) && params.message) {
495531
govulncheckTerminal?.appendLine(params.message);
496532
}
497533
break;
498534
case 'end':
535+
if (params.message) {
536+
ActiveProgressTerminals.get(token)?.appendLine(params.message);
537+
}
499538
if (pendingVulncheckProgressToken.has(token)) {
500539
const out = pendingVulncheckProgressToken.get(token);
501540
pendingVulncheckProgressToken.delete(token);
@@ -507,7 +546,7 @@ export async function buildLanguageClient(
507546
},
508547
executeCommand: async (command: string, args: any[], next: ExecuteCommandSignature) => {
509548
try {
510-
if (command === 'gopls.tidy') {
549+
if (command === 'gopls.tidy' || command === 'gopls.vulncheck') {
511550
await vscode.workspace.saveAll(false);
512551
}
513552
if (command === 'gopls.run_govulncheck' && args.length && args[0].URI) {
@@ -520,17 +559,35 @@ export async function buildLanguageClient(
520559
await vscode.workspace.saveAll(false);
521560
const uri = args[0].URI ? URI.parse(args[0].URI) : undefined;
522561
const dir = uri?.fsPath?.endsWith('.mod') ? path.dirname(uri.fsPath) : uri?.fsPath;
523-
govulncheckTerminal = VulncheckTerminal.Open();
562+
govulncheckTerminal = ProgressTerminal.Open('govulncheck');
524563
govulncheckTerminal.appendLine(`⚡ govulncheck -C ${dir} ./...\n\n`);
525564
govulncheckTerminal.show();
526565
}
527566
const res = await next(command, args);
528-
if (command === 'gopls.run_govulncheck') {
529-
const progressToken = res.Token;
530-
if (progressToken) {
531-
pendingVulncheckProgressToken.set(progressToken, args[0]);
567+
568+
const progressToken = <ProgressToken>res.Token;
569+
// The progressToken from executeCommand indicates that
570+
// gopls may trigger a related workDoneProgress
571+
// notification, either before or after the command
572+
// completes.
573+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverInitiatedProgress
574+
if (progressToken !== undefined) {
575+
switch (command) {
576+
case 'gopls.run_govulncheck':
577+
pendingVulncheckProgressToken.set(progressToken, args[0]);
578+
break;
579+
case 'gopls.vulncheck':
580+
// Write the vulncheck report to the terminal.
581+
if (ActiveProgressTerminals.has(progressToken)) {
582+
writeVulns(res.Result, ActiveProgressTerminals.get(progressToken), cfg.path);
583+
}
584+
break;
585+
default:
586+
// By default, dump the result to the terminal.
587+
ActiveProgressTerminals.get(progressToken)?.appendLine(res.Result);
532588
}
533589
}
590+
534591
return res;
535592
} catch (e) {
536593
// TODO: how to print ${e} reliably???

extension/src/progressTerminal.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2025 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
import vscode = require('vscode');
6+
7+
import { ProgressToken } from 'vscode-languageclient';
8+
9+
// ActiveProgressTerminals maps progress tokens to their corresponding terminals.
10+
// Entries are added when terminals are created for workdone progress and
11+
// deleted when closed by the user, which is interpreted as the user discarding
12+
// any further information.
13+
// There is no guarantee a terminal will remain available for the entire
14+
// duration of a workdone progress notification.
15+
// Logs can be appended to the terminal even after the workdone progress
16+
// notification finishes, allowing responses from requests extending
17+
// WorkDoneProgressOptions to be displayed in the same terminal.
18+
export const ActiveProgressTerminals = new Map<ProgressToken, IProgressTerminal>();
19+
20+
export interface IProgressTerminal {
21+
appendLine: (str: string) => void;
22+
show: (preserveFocus?: boolean) => void;
23+
exit: () => void;
24+
}
25+
export class ProgressTerminal implements IProgressTerminal {
26+
private progressToken?: ProgressToken;
27+
private term: vscode.Terminal;
28+
private writeEmitter = new vscode.EventEmitter<string>();
29+
30+
// Buffer messages emitted before vscode is ready. VSC calls pty.open when it is ready.
31+
private ptyReady = false;
32+
private buf: string[] = [];
33+
34+
// Constructor function to stub during test.
35+
static Open(name = 'progress', token?: ProgressToken): IProgressTerminal {
36+
return new ProgressTerminal(name, token);
37+
}
38+
39+
// ProgressTerminal created with token will be managed by map
40+
// ActiveProgressTerminals.
41+
private constructor(name: string, token?: ProgressToken) {
42+
const pty: vscode.Pseudoterminal = {
43+
onDidWrite: this.writeEmitter.event,
44+
handleInput: () => this.exit(),
45+
open: () => {
46+
this.ptyReady = true;
47+
this.buf.forEach((l) => this.writeEmitter.fire(l));
48+
this.buf = [];
49+
},
50+
close: () => {
51+
if (this.progressToken !== undefined) {
52+
ActiveProgressTerminals.delete(this.progressToken);
53+
}
54+
}
55+
};
56+
this.term = vscode.window.createTerminal({ name: name, pty }); // TODO: iconPath
57+
if (token !== undefined) {
58+
this.progressToken = token;
59+
ActiveProgressTerminals.set(this.progressToken, this);
60+
}
61+
}
62+
63+
appendLine(str: string) {
64+
if (!str.endsWith('\n')) {
65+
str += '\n';
66+
}
67+
str = str.replace(/\n/g, '\n\r'); // replaceAll('\n', '\n\r').
68+
if (!this.ptyReady) {
69+
this.buf.push(str); // print when `open` is called.
70+
} else {
71+
this.writeEmitter.fire(str);
72+
}
73+
}
74+
75+
show(preserveFocus?: boolean) {
76+
this.term.show(preserveFocus);
77+
}
78+
79+
exit() {}
80+
}

extension/test/gopls/vulncheck.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import assert from 'assert';
66
import path = require('path');
77
import sinon = require('sinon');
88
import vscode = require('vscode');
9-
import { VulncheckTerminal } from '../../src/goVulncheck';
9+
import { ProgressTerminal } from '../../src/progressTerminal';
1010
import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageserver-protocol';
1111
import { Env, FakeOutputChannel } from './goplsTestEnv.utils';
1212
import { URI } from 'vscode-uri';
@@ -34,7 +34,7 @@ suite('writeVulns', function () {
3434
sandbox.stub(config, 'getGoConfig').returns(goConfig);
3535
await env.startGopls(undefined, goConfig, fixtureDir);
3636

37-
sandbox.stub(VulncheckTerminal, 'Open').returns({
37+
sandbox.stub(ProgressTerminal, 'Open').returns({
3838
appendLine: fakeTerminal.appendLine,
3939
show: () => {},
4040
exit: () => {}

0 commit comments

Comments
 (0)