diff options
| author | Laurențiu Nicola <lnicola@dend.ro> | 2024-03-10 08:47:38 +0200 |
|---|---|---|
| committer | Laurențiu Nicola <lnicola@dend.ro> | 2024-03-10 08:47:38 +0200 |
| commit | 56493e4cbd6262adae03e73aefb8a9e618a9fc2d (patch) | |
| tree | 404d4a38aff53e1c880c2708478fdcaf0b2f6e61 /src/tools/rust-analyzer/editors/code | |
| parent | 5bc7b9ac8ace5312e1d2cdc2722715cf58d4f926 (diff) | |
| parent | 574e23ec508064613783cba3d1833a95fd9a5080 (diff) | |
| download | rust-56493e4cbd6262adae03e73aefb8a9e618a9fc2d.tar.gz rust-56493e4cbd6262adae03e73aefb8a9e618a9fc2d.zip | |
Merge commit '574e23ec508064613783cba3d1833a95fd9a5080' into sync-from-ra
Diffstat (limited to 'src/tools/rust-analyzer/editors/code')
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/package.json | 19 | ||||
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/src/client.ts | 8 | ||||
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/src/config.ts | 4 | ||||
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/src/ctx.ts | 16 | ||||
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/src/debug.ts | 60 | ||||
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/src/lsp_ext.ts | 36 | ||||
| -rw-r--r-- | src/tools/rust-analyzer/editors/code/src/test_explorer.ts | 173 |
7 files changed, 302 insertions, 14 deletions
diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json index d86365591a6..c34b8e25de0 100644 --- a/src/tools/rust-analyzer/editors/code/package.json +++ b/src/tools/rust-analyzer/editors/code/package.json @@ -510,6 +510,11 @@ "default": true, "type": "boolean" }, + "rust-analyzer.testExplorer": { + "markdownDescription": "Whether to show the test explorer.", + "default": false, + "type": "boolean" + }, "$generated-start": {}, "rust-analyzer.assist.emitMustUse": { "markdownDescription": "Whether to insert #[must_use] when generating `as_` methods\nfor enum variants.", @@ -948,6 +953,11 @@ "default": {}, "type": "object" }, + "rust-analyzer.diagnostics.styleLints.enable": { + "markdownDescription": "Whether to run additional style lints.", + "default": false, + "type": "boolean" + }, "rust-analyzer.diagnostics.warningsAsHint": { "markdownDescription": "List of warnings that should be displayed with hint severity.\n\nThe warnings will be indicated by faded text or three dots in code\nand will not show up in the `Problems Panel`.", "default": [], @@ -1134,6 +1144,15 @@ } ] }, + "rust-analyzer.hover.show.traitAssocItems": { + "markdownDescription": "How many associated items of a trait to display when hovering a trait.", + "default": null, + "type": [ + "null", + "integer" + ], + "minimum": 0 + }, "rust-analyzer.imports.granularity.enforce": { "markdownDescription": "Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.", "default": false, diff --git a/src/tools/rust-analyzer/editors/code/src/client.ts b/src/tools/rust-analyzer/editors/code/src/client.ts index c27a446b380..1cbf247297f 100644 --- a/src/tools/rust-analyzer/editors/code/src/client.ts +++ b/src/tools/rust-analyzer/editors/code/src/client.ts @@ -372,13 +372,18 @@ export async function createClient( ); // To turn on all proposed features use: client.registerProposedFeatures(); - client.registerFeature(new ExperimentalFeatures()); + client.registerFeature(new ExperimentalFeatures(config)); client.registerFeature(new OverrideFeatures()); return client; } class ExperimentalFeatures implements lc.StaticFeature { + private readonly testExplorer: boolean; + + constructor(config: Config) { + this.testExplorer = config.testExplorer || false; + } getState(): lc.FeatureState { return { kind: "static" }; } @@ -391,6 +396,7 @@ class ExperimentalFeatures implements lc.StaticFeature { colorDiagnosticOutput: true, openServerLogs: true, localDocs: true, + testExplorer: this.testExplorer, commands: { commands: [ "rust-analyzer.runSingle", diff --git a/src/tools/rust-analyzer/editors/code/src/config.ts b/src/tools/rust-analyzer/editors/code/src/config.ts index 51a0aece820..92a816bfbcb 100644 --- a/src/tools/rust-analyzer/editors/code/src/config.ts +++ b/src/tools/rust-analyzer/editors/code/src/config.ts @@ -266,6 +266,10 @@ export class Config { return this.get<string | undefined>("cargoRunner"); } + get testExplorer() { + return this.get<boolean | undefined>("testExplorer"); + } + get runnablesExtraEnv() { const item = this.get<any>("runnables.extraEnv") ?? this.get<any>("runnableEnv"); if (!item) return item; diff --git a/src/tools/rust-analyzer/editors/code/src/ctx.ts b/src/tools/rust-analyzer/editors/code/src/ctx.ts index 01a3aca1323..f76dec2629a 100644 --- a/src/tools/rust-analyzer/editors/code/src/ctx.ts +++ b/src/tools/rust-analyzer/editors/code/src/ctx.ts @@ -24,6 +24,7 @@ import { PersistentState } from "./persistent_state"; import { bootstrap } from "./bootstrap"; import type { RustAnalyzerExtensionApi } from "./main"; import type { JsonProject } from "./rust_project"; +import { prepareTestExplorer } from "./test_explorer"; // We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if // only those are in use. We use "Empty" to represent these scenarios @@ -74,6 +75,7 @@ export class Ctx implements RustAnalyzerExtensionApi { private _client: lc.LanguageClient | undefined; private _serverPath: string | undefined; private traceOutputChannel: vscode.OutputChannel | undefined; + private testController: vscode.TestController | undefined; private outputChannel: vscode.OutputChannel | undefined; private clientSubscriptions: Disposable[]; private state: PersistentState; @@ -102,14 +104,20 @@ export class Ctx implements RustAnalyzerExtensionApi { workspace: Workspace, ) { extCtx.subscriptions.push(this); + this.config = new Config(extCtx); this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + if (this.config.testExplorer) { + this.testController = vscode.tests.createTestController( + "rustAnalyzerTestController", + "Rust Analyzer test controller", + ); + } this.workspace = workspace; this.clientSubscriptions = []; this.commandDisposables = []; this.commandFactories = commandFactories; this.unlinkedFiles = []; this.state = new PersistentState(extCtx.globalState); - this.config = new Config(extCtx); this.updateCommands("disable"); this.setServerStatus({ @@ -120,6 +128,7 @@ export class Ctx implements RustAnalyzerExtensionApi { dispose() { this.config.dispose(); this.statusBar.dispose(); + this.testController?.dispose(); void this.disposeClient(); this.commandDisposables.forEach((disposable) => disposable.dispose()); } @@ -264,6 +273,9 @@ export class Ctx implements RustAnalyzerExtensionApi { await client.start(); this.updateCommands(); + if (this.testController) { + prepareTestExplorer(this, this.testController, client); + } if (this.config.showDependenciesExplorer) { this.prepareTreeDependenciesView(client); } @@ -491,7 +503,7 @@ export class Ctx implements RustAnalyzerExtensionApi { this.extCtx.subscriptions.push(d); } - private pushClientCleanup(d: Disposable) { + pushClientCleanup(d: Disposable) { this.clientSubscriptions.push(d); } } diff --git a/src/tools/rust-analyzer/editors/code/src/debug.ts b/src/tools/rust-analyzer/editors/code/src/debug.ts index d9c6b6ac456..3fc63fc7d81 100644 --- a/src/tools/rust-analyzer/editors/code/src/debug.ts +++ b/src/tools/rust-analyzer/editors/code/src/debug.ts @@ -81,8 +81,9 @@ async function getDebugConfiguration( if (!editor) return; const knownEngines: Record<string, DebugConfigProvider> = { - "vadimcn.vscode-lldb": getLldbDebugConfig, - "ms-vscode.cpptools": getCppvsDebugConfig, + "ms-vscode.cpptools": getCCppDebugConfig, + "vadimcn.vscode-lldb": getCodeLldbDebugConfig, + "webfreak.debug": getNativeDebugConfig, }; const debugOptions = ctx.config.debug; @@ -97,12 +98,14 @@ async function getDebugConfiguration( } if (!debugEngine) { + const commandCCpp: string = createCommandLink("ms-vscode.cpptools"); const commandCodeLLDB: string = createCommandLink("vadimcn.vscode-lldb"); - const commandCpp: string = createCommandLink("ms-vscode.cpptools"); + const commandNativeDebug: string = createCommandLink("webfreak.debug"); await vscode.window.showErrorMessage( `Install [CodeLLDB](command:${commandCodeLLDB} "Open CodeLLDB")` + - ` or [C/C++](command:${commandCpp} "Open C/C++") extension for debugging.`, + `, [C/C++](command:${commandCCpp} "Open C/C++") ` + + `or [Native Debug](command:${commandNativeDebug} "Open Native Debug") for debugging.`, ); return; } @@ -184,7 +187,7 @@ async function getDebugExecutableInfo( return executableInfo; } -function getLldbDebugConfig( +function getCCppDebugConfig( runnable: ra.Runnable, executable: string, cargoWorkspace: string, @@ -192,19 +195,18 @@ function getLldbDebugConfig( sourceFileMap?: Record<string, string>, ): vscode.DebugConfiguration { return { - type: "lldb", + type: os.platform() === "win32" ? "cppvsdbg" : "cppdbg", request: "launch", name: runnable.label, program: executable, args: runnable.args.executableArgs, cwd: cargoWorkspace || runnable.args.workspaceRoot, - sourceMap: sourceFileMap, - sourceLanguages: ["rust"], + sourceFileMap, env, }; } -function getCppvsDebugConfig( +function getCodeLldbDebugConfig( runnable: ra.Runnable, executable: string, cargoWorkspace: string, @@ -212,13 +214,49 @@ function getCppvsDebugConfig( sourceFileMap?: Record<string, string>, ): vscode.DebugConfiguration { return { - type: os.platform() === "win32" ? "cppvsdbg" : "cppdbg", + type: "lldb", request: "launch", name: runnable.label, program: executable, args: runnable.args.executableArgs, cwd: cargoWorkspace || runnable.args.workspaceRoot, - sourceFileMap, + sourceMap: sourceFileMap, + sourceLanguages: ["rust"], env, }; } + +function getNativeDebugConfig( + runnable: ra.Runnable, + executable: string, + cargoWorkspace: string, + env: Record<string, string>, + _sourceFileMap?: Record<string, string>, +): vscode.DebugConfiguration { + return { + type: "gdb", + request: "launch", + name: runnable.label, + target: executable, + // See https://github.com/WebFreak001/code-debug/issues/359 + arguments: quote(runnable.args.executableArgs), + cwd: cargoWorkspace || runnable.args.workspaceRoot, + env, + valuesFormatting: "prettyPrinters", + }; +} + +// Based on https://github.com/ljharb/shell-quote/blob/main/quote.js +function quote(xs: string[]) { + return xs + .map(function (s) { + if (/["\s]/.test(s) && !/'/.test(s)) { + return "'" + s.replace(/(['\\])/g, "\\$1") + "'"; + } + if (/["'\s]/.test(s)) { + return '"' + s.replace(/(["\\$`!])/g, "\\$1") + '"'; + } + return s.replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2"); + }) + .join(" "); +} diff --git a/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts b/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts index 6c961f53e7e..31ac3d9413e 100644 --- a/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts +++ b/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts @@ -68,6 +68,42 @@ export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void> "rust-analyzer/viewItemTree", ); +export type DiscoverTestParams = { testId?: string | undefined }; +export type RunTestParams = { + include?: string[] | undefined; + exclude?: string[] | undefined; +}; +export type TestItem = { + id: string; + label: string; + kind: "package" | "module" | "test"; + canResolveChildren: boolean; + parent?: string | undefined; + textDocument?: lc.TextDocumentIdentifier | undefined; + range?: lc.Range | undefined; + runnable?: Runnable | undefined; +}; +export type DiscoverTestResults = { tests: TestItem[]; scope: string[] }; +export type TestState = + | { tag: "failed"; message: string } + | { tag: "passed" } + | { tag: "started" } + | { tag: "enqueued" } + | { tag: "skipped" }; +export type ChangeTestStateParams = { testId: string; state: TestState }; +export const discoverTest = new lc.RequestType<DiscoverTestParams, DiscoverTestResults, void>( + "experimental/discoverTest", +); +export const discoveredTests = new lc.NotificationType<DiscoverTestResults>( + "experimental/discoveredTests", +); +export const runTest = new lc.RequestType<RunTestParams, void, void>("experimental/runTest"); +export const abortRunTest = new lc.NotificationType0("experimental/abortRunTest"); +export const endRunTest = new lc.NotificationType0("experimental/endRunTest"); +export const changeTestState = new lc.NotificationType<ChangeTestStateParams>( + "experimental/changeTestState", +); + export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier }; export interface FetchDependencyListParams {} diff --git a/src/tools/rust-analyzer/editors/code/src/test_explorer.ts b/src/tools/rust-analyzer/editors/code/src/test_explorer.ts new file mode 100644 index 00000000000..2f0b4d5b5cf --- /dev/null +++ b/src/tools/rust-analyzer/editors/code/src/test_explorer.ts @@ -0,0 +1,173 @@ +import * as vscode from "vscode"; +import type * as lc from "vscode-languageclient/node"; +import * as ra from "./lsp_ext"; + +import type { Ctx } from "./ctx"; +import { startDebugSession } from "./debug"; + +export const prepareTestExplorer = ( + ctx: Ctx, + testController: vscode.TestController, + client: lc.LanguageClient, +) => { + let currentTestRun: vscode.TestRun | undefined; + let idToTestMap: Map<string, vscode.TestItem> = new Map(); + const idToRunnableMap: Map<string, ra.Runnable> = new Map(); + + testController.createRunProfile( + "Run Tests", + vscode.TestRunProfileKind.Run, + async (request: vscode.TestRunRequest, cancelToken: vscode.CancellationToken) => { + if (currentTestRun) { + await client.sendNotification(ra.abortRunTest); + while (currentTestRun) { + await new Promise((resolve) => setTimeout(resolve, 1)); + } + } + + currentTestRun = testController.createTestRun(request); + cancelToken.onCancellationRequested(async () => { + await client.sendNotification(ra.abortRunTest); + }); + const include = request.include?.map((x) => x.id); + const exclude = request.exclude?.map((x) => x.id); + await client.sendRequest(ra.runTest, { include, exclude }); + }, + true, + undefined, + false, + ); + + testController.createRunProfile( + "Debug Tests", + vscode.TestRunProfileKind.Debug, + async (request: vscode.TestRunRequest) => { + if (request.include?.length !== 1 || request.exclude?.length !== 0) { + await vscode.window.showErrorMessage("You can debug only one test at a time"); + return; + } + const id = request.include[0]!.id; + const runnable = idToRunnableMap.get(id); + if (!runnable) { + await vscode.window.showErrorMessage("You can debug only one test at a time"); + return; + } + await startDebugSession(ctx, runnable); + }, + true, + undefined, + false, + ); + + const addTest = (item: ra.TestItem) => { + const parentList = item.parent + ? idToTestMap.get(item.parent)!.children + : testController.items; + const oldTest = parentList.get(item.id); + const uri = item.textDocument?.uri ? vscode.Uri.parse(item.textDocument?.uri) : undefined; + const range = + item.range && + new vscode.Range( + new vscode.Position(item.range.start.line, item.range.start.character), + new vscode.Position(item.range.end.line, item.range.end.character), + ); + if (oldTest) { + if (oldTest.uri?.toString() === uri?.toString()) { + oldTest.range = range; + return; + } + parentList.delete(item.id); + } + const iconToVscodeMap = { + package: "package", + module: "symbol-module", + test: "beaker", + }; + const test = testController.createTestItem( + item.id, + `$(${iconToVscodeMap[item.kind]}) ${item.label}`, + uri, + ); + test.range = range; + test.canResolveChildren = item.canResolveChildren; + idToTestMap.set(item.id, test); + if (item.runnable) { + idToRunnableMap.set(item.id, item.runnable); + } + parentList.add(test); + }; + + const addTestGroup = (testsAndScope: ra.DiscoverTestResults) => { + const { tests, scope } = testsAndScope; + const testSet: Set<string> = new Set(); + for (const test of tests) { + addTest(test); + testSet.add(test.id); + } + // FIXME(hack_recover_crate_name): We eagerly resolve every test if we got a lazy top level response (detected + // by `!scope`). ctx is not a good thing and wastes cpu and memory unnecessarily, so we should remove it. + if (!scope) { + for (const test of tests) { + void testController.resolveHandler!(idToTestMap.get(test.id)); + } + } + if (!scope) { + return; + } + const recursivelyRemove = (tests: vscode.TestItemCollection) => { + for (const [testId, _] of tests) { + if (!testSet.has(testId)) { + tests.delete(testId); + } else { + recursivelyRemove(tests.get(testId)!.children); + } + } + }; + for (const root of scope) { + recursivelyRemove(idToTestMap.get(root)!.children); + } + }; + + ctx.pushClientCleanup( + client.onNotification(ra.discoveredTests, (results) => { + addTestGroup(results); + }), + ); + + ctx.pushClientCleanup( + client.onNotification(ra.endRunTest, () => { + currentTestRun!.end(); + currentTestRun = undefined; + }), + ); + + ctx.pushClientCleanup( + client.onNotification(ra.changeTestState, (results) => { + const test = idToTestMap.get(results.testId)!; + if (results.state.tag === "failed") { + currentTestRun!.failed(test, new vscode.TestMessage(results.state.message)); + } else if (results.state.tag === "passed") { + currentTestRun!.passed(test); + } else if (results.state.tag === "started") { + currentTestRun!.started(test); + } else if (results.state.tag === "skipped") { + currentTestRun!.skipped(test); + } else if (results.state.tag === "enqueued") { + currentTestRun!.enqueued(test); + } + }), + ); + + testController.resolveHandler = async (item) => { + const results = await client.sendRequest(ra.discoverTest, { testId: item?.id }); + addTestGroup(results); + }; + + testController.refreshHandler = async () => { + testController.items.forEach((t) => { + testController.items.delete(t.id); + }); + idToTestMap = new Map(); + await testController.resolveHandler!(undefined); + }; +}; |
