in src/language/goLanguageServer.ts [413:750]
export async function buildLanguageClient(
goCtx: GoExtensionContext,
cfg: BuildLanguageClientOption
): Promise<GoLanguageClient> {
const goplsWorkspaceConfig = await adjustGoplsWorkspaceConfiguration(cfg, getGoplsConfig(), 'gopls', undefined);
const documentSelector = [
// gopls handles only file URIs.
{ language: 'go', scheme: 'file' },
{ language: 'go.mod', scheme: 'file' },
{ language: 'go.sum', scheme: 'file' },
{ language: 'go.work', scheme: 'file' },
{ language: 'gotmpl', scheme: 'file' }
];
// when initialization is failed after the connection is established,
// we want to handle the connection close error case specially. Capture the error
// in initializationFailedHandler and handle it in the connectionCloseHandler.
let initializationError: WebRequest.ResponseError<InitializeError> | undefined = undefined;
const govulncheckOutputChannel = goCtx.govulncheckOutputChannel;
const pendingVulncheckProgressToken = new Map<ProgressToken, any>();
const onDidChangeVulncheckResultEmitter = new vscode.EventEmitter<VulncheckEvent>();
const c = new GoLanguageClient(
'go', // id
cfg.serverName, // name e.g. gopls
{
command: cfg.path,
args: ['-mode=stdio', ...cfg.flags],
options: { env: cfg.env }
} as ServerOptions,
{
initializationOptions: goplsWorkspaceConfig,
documentSelector,
uriConverters: {
// Apply file:/// scheme to all file paths.
code2Protocol: (uri: vscode.Uri): string =>
(uri.scheme ? uri : uri.with({ scheme: 'file' })).toString(),
protocol2Code: (uri: string) => vscode.Uri.parse(uri)
},
outputChannel: cfg.outputChannel,
traceOutputChannel: cfg.traceOutputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never,
initializationFailedHandler: (error: WebRequest.ResponseError<InitializeError>): boolean => {
initializationError = error;
return false;
},
errorHandler: {
error: (error: Error, message: Message, count: number) => {
// Allow 5 crashes before shutdown.
if (count < 5) {
return {
message: '', // suppresses error popups
action: ErrorAction.Continue
};
}
return {
action: ErrorAction.Shutdown
};
},
closed: () => {
if (initializationError !== undefined) {
suggestGoplsIssueReport(
goCtx,
'The gopls server failed to initialize.',
errorKind.initializationFailure,
initializationError
);
initializationError = undefined;
// In case of initialization failure, do not try to restart.
return {
message: 'The gopls server failed to initialize.',
action: CloseAction.DoNotRestart
};
}
// Allow 5 crashes before shutdown.
const { crashCount = 0 } = goCtx;
goCtx.crashCount = crashCount + 1;
if (goCtx.crashCount < 5) {
return {
message: '', // suppresses error popups
action: CloseAction.Restart
};
}
suggestGoplsIssueReport(
goCtx,
'The connection to gopls has been closed. The gopls server may have crashed.',
errorKind.crash
);
updateLanguageServerIconGoStatusBar(false, true);
return {
action: CloseAction.DoNotRestart
};
}
},
middleware: {
handleWorkDoneProgress: async (token, params, next) => {
switch (params.kind) {
case 'begin':
break;
case 'report':
if (pendingVulncheckProgressToken.has(token) && params.message) {
govulncheckOutputChannel?.appendLine(params.message);
}
break;
case 'end':
if (pendingVulncheckProgressToken.has(token)) {
const out = pendingVulncheckProgressToken.get(token);
pendingVulncheckProgressToken.delete(token);
if (params.message === 'completed') {
// success. In case of failure, it will be 'failed'
onDidChangeVulncheckResultEmitter.fire({ URI: out.URI });
}
}
}
next(token, params);
},
executeCommand: async (command: string, args: any[], next: ExecuteCommandSignature) => {
try {
if (command === 'gopls.run_govulncheck' && args.length) {
await vscode.workspace.saveAll(false);
// TODO: move this output printing to goVulncheck.ts.
govulncheckOutputChannel?.replace(`govulncheck ./... for ${args[0].URI}\n`);
govulncheckOutputChannel?.appendLine('govulncheck is an experimental tool.');
govulncheckOutputChannel?.appendLine(
'Share feedback at https://go.dev/s/vsc-vulncheck-feedback.\n'
);
govulncheckOutputChannel?.show();
}
if (command === 'gopls.tidy') {
await vscode.workspace.saveAll(false);
}
const res = await next(command, args);
if (command === 'gopls.run_govulncheck') {
const progressToken = res.Token;
if (progressToken) {
pendingVulncheckProgressToken.set(progressToken, args[0]);
}
}
return res;
} catch (e) {
// TODO: how to print ${e} reliably???
const answer = await vscode.window.showErrorMessage(
`Command '${command}' failed: ${e}.`,
'Show Trace'
);
if (answer === 'Show Trace') {
goCtx.serverOutputChannel?.show();
}
return null;
}
},
provideFoldingRanges: async (
doc: vscode.TextDocument,
context: FoldingContext,
token: CancellationToken,
next: ProvideFoldingRangeSignature
) => {
const ranges = await next(doc, context, token);
if ((!ranges || ranges.length === 0) && doc.lineCount > 0) {
return undefined;
}
return ranges;
},
provideCodeLenses: async (
doc: vscode.TextDocument,
token: vscode.CancellationToken,
next: ProvideCodeLensesSignature
): Promise<vscode.CodeLens[]> => {
const codeLens = await next(doc, token);
if (!codeLens || codeLens.length === 0) {
return codeLens ?? [];
}
return codeLens.reduce((lenses: vscode.CodeLens[], lens: vscode.CodeLens) => {
switch (lens.command?.title) {
case 'run test': {
return [...lenses, ...createTestCodeLens(lens)];
}
case 'run benchmark': {
return [...lenses, ...createBenchmarkCodeLens(lens)];
}
default: {
return [...lenses, lens];
}
}
}, []);
},
provideDocumentFormattingEdits: async (
document: vscode.TextDocument,
options: vscode.FormattingOptions,
token: vscode.CancellationToken,
next: ProvideDocumentFormattingEditsSignature
) => {
if (cfg.features.formatter) {
return cfg.features.formatter.provideDocumentFormattingEdits(document, options, token);
}
return next(document, options, token);
},
handleDiagnostics: (
uri: vscode.Uri,
diagnostics: vscode.Diagnostic[],
next: HandleDiagnosticsSignature
) => {
const { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection } = goCtx;
// Deduplicate diagnostics with those found by the other tools.
removeDuplicateDiagnostics(vetDiagnosticCollection, uri, diagnostics);
removeDuplicateDiagnostics(buildDiagnosticCollection, uri, diagnostics);
removeDuplicateDiagnostics(lintDiagnosticCollection, uri, diagnostics);
return next(uri, diagnostics);
},
provideCompletionItem: async (
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.CompletionContext,
token: vscode.CancellationToken,
next: ProvideCompletionItemsSignature
) => {
const list = await next(document, position, context, token);
if (!list) {
return list;
}
const items = Array.isArray(list) ? list : list.items;
// Give all the candidates the same filterText to trick VSCode
// into not reordering our candidates. All the candidates will
// appear to be equally good matches, so VSCode's fuzzy
// matching/ranking just maintains the natural "sortText"
// ordering. We can only do this in tandem with
// "incompleteResults" since otherwise client side filtering is
// important.
if (!Array.isArray(list) && list.isIncomplete && list.items.length > 1) {
let hardcodedFilterText = items[0].filterText;
if (!hardcodedFilterText) {
// tslint:disable:max-line-length
// According to LSP spec,
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
// if filterText is falsy, the `label` should be used.
// But we observed that's not the case.
// Even if vscode picked the label value, that would
// cause to reorder candiates, which is not ideal.
// Force to use non-empty `label`.
// https://github.com/golang/vscode-go/issues/441
let { label } = items[0];
if (typeof label !== 'string') label = label.label;
hardcodedFilterText = label;
}
for (const item of items) {
item.filterText = hardcodedFilterText;
}
}
const paramHintsEnabled = vscode.workspace.getConfiguration('editor.parameterHints', {
languageId: 'go',
uri: document.uri
});
// If the user has parameterHints (signature help) enabled,
// trigger it for function or method completion items.
if (paramHintsEnabled) {
for (const item of items) {
if (item.kind === CompletionItemKind.Method || item.kind === CompletionItemKind.Function) {
item.command = {
title: 'triggerParameterHints',
command: 'editor.action.triggerParameterHints'
};
}
}
}
return list;
},
// Keep track of the last file change in order to not prompt
// user if they are actively working.
didOpen: async (e, next) => {
goCtx.lastUserAction = new Date();
next(e);
},
didChange: async (e, next) => {
goCtx.lastUserAction = new Date();
next(e);
},
didClose: async (e, next) => {
goCtx.lastUserAction = new Date();
next(e);
},
didSave: async (e, next) => {
goCtx.lastUserAction = new Date();
next(e);
},
workspace: {
configuration: async (
params: ConfigurationParams,
token: CancellationToken,
next: ConfigurationRequest.HandlerSignature
): Promise<any[] | ResponseError<void>> => {
const configs = await next(params, token);
if (!configs || !Array.isArray(configs)) {
return configs;
}
const ret = [] as any[];
for (let i = 0; i < configs.length; i++) {
let workspaceConfig = configs[i];
if (!!workspaceConfig && typeof workspaceConfig === 'object') {
const scopeUri = params.items[i].scopeUri;
const resource = scopeUri ? vscode.Uri.parse(scopeUri) : undefined;
const section = params.items[i].section;
workspaceConfig = await adjustGoplsWorkspaceConfiguration(
cfg,
workspaceConfig,
section,
resource
);
}
ret.push(workspaceConfig);
}
return ret;
}
}
}
} as LanguageClientOptions,
onDidChangeVulncheckResultEmitter
);
onDidChangeVulncheckResultEmitter.event(async (e: VulncheckEvent) => {
if (!govulncheckOutputChannel) return;
if (!e || !e.URI) {
govulncheckOutputChannel.appendLine(`unexpected vulncheck event: ${JSON.stringify(e)}`);
return;
}
try {
const res = await goplsFetchVulncheckResult(goCtx, e.URI.toString());
writeVulns(res, govulncheckOutputChannel);
} catch (e) {
govulncheckOutputChannel.appendLine(`Fetching govulncheck output from gopls failed ${e}`);
}
});
return c;
}