From c7e83f71a4d8322709b5cd8ef216fc77cd6b5f80 Mon Sep 17 00:00:00 2001 From: buravc Date: Wed, 9 Oct 2024 21:34:43 +0200 Subject: [PATCH] src/goCodeLens: show codelens for implementations of symbols This change adds implementations as a codelens to abstract and concrete symbols. Fixes #56695 --- docs/settings.md | 2 + extension/package.json | 5 + extension/src/goCodeLens.ts | 155 +++++++++++++++++++++ extension/src/goMain.ts | 2 + extension/src/language/goLanguageServer.ts | 39 +++++- 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 extension/src/goCodeLens.ts diff --git a/docs/settings.md b/docs/settings.md index aa55b8298e..bded92b5b4 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -211,11 +211,13 @@ Feature level setting to enable/disable code lens for references and run/debug t | Properties | Description | | --- | --- | | `runtest` | If true, enables code lens for running and debugging tests
Default: `true` | +| `implementation` | If true, enables code lens for showing implementations
Default: `true` | Default: ``` { "runtest" : true, + "implementation" : true, } ``` ### `go.formatFlags` diff --git a/extension/package.json b/extension/package.json index 6306554f46..f466ae8d28 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1551,6 +1551,11 @@ "type": "boolean", "default": true, "description": "If true, enables code lens for running and debugging tests" + }, + "implementation": { + "type": "boolean", + "default": true, + "description": "If true, enables code lens for showing implementations" } }, "additionalProperties": false, diff --git a/extension/src/goCodeLens.ts b/extension/src/goCodeLens.ts new file mode 100644 index 0000000000..b2533ad491 --- /dev/null +++ b/extension/src/goCodeLens.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------*/ + +'use strict'; + +import vscode = require('vscode'); +import { CancellationToken, CodeLens, TextDocument } from 'vscode'; +import { getGoConfig } from './config'; +import { GoBaseCodeLensProvider } from './goBaseCodelens'; +import { GoDocumentSymbolProvider } from './goDocumentSymbols'; +import { GoExtensionContext } from './context'; +import { GO_MODE } from './goMode'; +import { getSymbolImplementations } from './language/goLanguageServer'; + +export class GoCodeLensProvider extends GoBaseCodeLensProvider { + static activate(ctx: vscode.ExtensionContext, goCtx: GoExtensionContext) { + const codeLensProvider = new this(goCtx); + ctx.subscriptions.push(vscode.languages.registerCodeLensProvider(GO_MODE, codeLensProvider)); + ctx.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { + if (!e.affectsConfiguration('go')) { + return; + } + const updatedGoConfig = getGoConfig(); + if (updatedGoConfig['enableCodeLens']) { + codeLensProvider.setEnabled(updatedGoConfig['enableCodeLens']['implementation']); + } + }) + ); + } + + constructor(private readonly goCtx: GoExtensionContext) { + super(); + } + + public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + if (!this.enabled) { + return []; + } + const config = getGoConfig(document.uri); + const codeLensConfig = config.get<{ [key: string]: any }>('enableCodeLens'); + const codelensEnabled = codeLensConfig ? codeLensConfig['implementation'] : false; + if (!codelensEnabled || !document.fileName.endsWith('.go')) { + return []; + } + + const abstractCodelenses = this.getCodeLensForAbstractSymbols(document, token); + const concreteCodelenses = this.getCodeLensForConcreteSymbols(document, token); + + const codeLenses = await Promise.all([abstractCodelenses, concreteCodelenses]); + + return codeLenses.flat(); + } + + private async getCodeLensForConcreteSymbols(document: TextDocument, token: CancellationToken): Promise { + const concreteTypes = await this.getConcreteTypes(document); + if (concreteTypes && concreteTypes.length) { + const concreteTypesCodeLens = await this.mapSymbolsToCodeLenses(document, concreteTypes); + return concreteTypesCodeLens; + } + + return []; + } + + private async getCodeLensForAbstractSymbols(document: TextDocument, token: CancellationToken): Promise { + const interfaces = await this.getInterfaces(document); + if (interfaces && interfaces.length) { + const interfacesCodeLens = this.mapSymbolsToCodeLenses(document, interfaces); + + const methodsCodeLens = this.mapSymbolsToCodeLenses( + document, + interfaces.flatMap((i) => i.children) + ); + + const codeLenses = await Promise.all([interfacesCodeLens, methodsCodeLens]); + + return codeLenses.flat(); + } + return []; + } + + private async getInterfaces(document: TextDocument): Promise { + const documentSymbolProvider = GoDocumentSymbolProvider(this.goCtx); + const symbols = await documentSymbolProvider.provideDocumentSymbols(document); + if (!symbols || symbols.length === 0) { + return []; + } + const pkg = symbols[0]; + if (!pkg) { + return []; + } + const children = pkg.children; + const interfaces = children.filter((s) => s.kind === vscode.SymbolKind.Interface); + if (!interfaces) { + return []; + } + + return interfaces; + } + + private async getConcreteTypes(document: TextDocument): Promise { + const documentSymbolProvider = GoDocumentSymbolProvider(this.goCtx); + const symbols = await documentSymbolProvider.provideDocumentSymbols(document); + if (!symbols || symbols.length === 0) { + return []; + } + const pkg = symbols[0]; + if (!pkg) { + return []; + } + const children = pkg.children; + const concreteTypes = children.filter((s) => + [vscode.SymbolKind.Struct, vscode.SymbolKind.Method].includes(s.kind) + ); + if (!concreteTypes) { + return []; + } + + return concreteTypes; + } + + private async mapSymbolsToCodeLenses( + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[] + ): Promise { + return Promise.all( + symbols.map(async (s) => { + const implementations = await this.getImplementations(document, s); + if (implementations.length) { + return new CodeLens(s.range, { + title: `${implementations.length} implementation${implementations.length > 1 ? 's' : ''}`, + command: 'editor.action.goToLocations', + arguments: [document.uri, s.range.start, implementations, 'peek'] + }); + } + + return new CodeLens(s.range, { + title: 'no implementation found', + command: '' + }); + }) + ); + } + + private async getImplementations( + document: vscode.TextDocument, + symbol: vscode.DocumentSymbol + ): Promise { + return getSymbolImplementations(this.goCtx, document, symbol); + } +} diff --git a/extension/src/goMain.ts b/extension/src/goMain.ts index 015b1eb13a..58a1d7cc79 100644 --- a/extension/src/goMain.ts +++ b/extension/src/goMain.ts @@ -73,6 +73,7 @@ import * as commands from './commands'; import { toggleVulncheckCommandFactory } from './goVulncheck'; import { GoTaskProvider } from './goTaskProvider'; import { setTelemetryEnvVars, telemetryReporter } from './goTelemetry'; +import { GoCodeLensProvider } from './goCodeLens'; const goCtx: GoExtensionContext = {}; @@ -144,6 +145,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { + const languageClient = goCtx.languageClient; + if (languageClient) { + const params = { + textDocument: { uri: document.uri.toString() }, + position: { + line: symbol.selectionRange.start.line, + character: symbol.selectionRange.start.character + } + }; + try { + const implementations = await languageClient.sendRequest('textDocument/implementation', params); + return ( + implementations.map( + (i) => + new vscode.Location( + vscode.Uri.parse(i.uri), + new vscode.Range( + new vscode.Position(i.range.start.line, i.range.start.character), + new vscode.Position(i.range.end.line, i.range.end.character) + ) + ) + ) || [] + ); + } catch (error) { + console.error(`unable to get implementations for ${symbol.name}:`, error); + return []; + } + } + return []; +}