Skip to content

Commit

Permalink
extension/src: support stream $/progress message in log style
Browse files Browse the repository at this point in the history
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 <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>
  • Loading branch information
h9jiang committed Feb 7, 2025
1 parent 0575f50 commit 84fc37a
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 62 deletions.
54 changes: 2 additions & 52 deletions extension/src/goVulncheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,7 @@ import cp = require('child_process');
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 {
Expand All @@ -72,7 +22,7 @@ export interface VulncheckReport {

export async function writeVulns(
res: VulncheckReport,
term: IVulncheckTerminal | undefined,
term: IProgressTerminal | undefined,
goplsBinPath: string
): Promise<void> {
if (term === undefined) {
Expand Down
73 changes: 65 additions & 8 deletions extension/src/language/goLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import path = require('path');
import semver = require('semver');
import util = require('util');
import vscode = require('vscode');
import { InitializeParams, LSPAny, LSPObject } from 'vscode-languageserver-protocol';

Check warning on line 17 in extension/src/language/goLanguageServer.ts

View workflow job for this annotation

GitHub Actions / ubuntu-latest stable 1.21

'LSPAny' is defined but never used

Check warning on line 17 in extension/src/language/goLanguageServer.ts

View workflow job for this annotation

GitHub Actions / ubuntu-latest stable 1.22

'LSPAny' is defined but never used

Check warning on line 17 in extension/src/language/goLanguageServer.ts

View workflow job for this annotation

GitHub Actions / ubuntu-latest stable 1.23

'LSPAny' is defined but never used
import {
CancellationToken,
CloseAction,
Expand Down Expand Up @@ -59,7 +60,8 @@ import { maybePromptForDeveloperSurvey } from '../goDeveloperSurvey';
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';
Expand Down Expand Up @@ -379,9 +381,23 @@ export class GoLanguageClient extends LanguageClient implements vscode.Disposabl
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 = {
Expand All @@ -402,10 +418,12 @@ export async function buildLanguageClient(
// 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
Expand Down Expand Up @@ -489,13 +507,34 @@ export async function buildLanguageClient(
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);
Expand All @@ -507,7 +546,7 @@ export async function buildLanguageClient(
},
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) {
Expand All @@ -520,17 +559,35 @@ export async function buildLanguageClient(
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???
Expand Down
80 changes: 80 additions & 0 deletions extension/src/progressTerminal.ts
Original file line number Diff line number Diff line change
@@ -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() {}
}
4 changes: 2 additions & 2 deletions extension/test/gopls/vulncheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import assert from 'assert';
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';
Expand Down Expand Up @@ -34,7 +34,7 @@ suite('writeVulns', function () {
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: () => {}
Expand Down

0 comments on commit 84fc37a

Please sign in to comment.