about summary refs log tree commit diff
path: root/src/tools/rust-analyzer/editors/code
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/rust-analyzer/editors/code')
-rw-r--r--src/tools/rust-analyzer/editors/code/package.json19
-rw-r--r--src/tools/rust-analyzer/editors/code/src/client.ts8
-rw-r--r--src/tools/rust-analyzer/editors/code/src/config.ts4
-rw-r--r--src/tools/rust-analyzer/editors/code/src/ctx.ts16
-rw-r--r--src/tools/rust-analyzer/editors/code/src/debug.ts60
-rw-r--r--src/tools/rust-analyzer/editors/code/src/lsp_ext.ts36
-rw-r--r--src/tools/rust-analyzer/editors/code/src/test_explorer.ts173
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);
+    };
+};