From 37d5a0343ce4f0044ebeef7b75ed47e58eea75e7 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Tue, 5 May 2026 23:53:24 -0700 Subject: [PATCH 01/57] Add VS Code new project command Implement a rust-analyzer VS Code command for creating new Cargo projects from the editor. Add command registration, UI entry points, configuration for post-create behavior, focused validation, and tests for the command flow. AI tools were used to research comparable editor behaviors and help refine the implementation plan; the resulting changes were reviewed before submission. --- .../rust-analyzer/editors/code/package.json | 40 +++ .../editors/code/src/commands.ts | 1 + .../rust-analyzer/editors/code/src/config.ts | 4 + .../rust-analyzer/editors/code/src/main.ts | 6 + .../editors/code/src/new_project.ts | 311 ++++++++++++++++++ .../editors/code/src/toolchain.ts | 45 ++- .../editors/code/tests/unit/commands.test.ts | 80 +++++ .../code/walkthrough-create-project.md | 9 + 8 files changed, 492 insertions(+), 4 deletions(-) create mode 100644 src/tools/rust-analyzer/editors/code/src/new_project.ts create mode 100644 src/tools/rust-analyzer/editors/code/tests/unit/commands.test.ts create mode 100644 src/tools/rust-analyzer/editors/code/walkthrough-create-project.md diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json index 14369e6f33e43..319b888e37b39 100644 --- a/src/tools/rust-analyzer/editors/code/package.json +++ b/src/tools/rust-analyzer/editors/code/package.json @@ -214,6 +214,11 @@ "title": "Reload workspace", "category": "rust-analyzer" }, + { + "command": "rust-analyzer.newProject", + "title": "Create New Project...", + "category": "rust-analyzer" + }, { "command": "rust-analyzer.rebuildProcMacros", "title": "Rebuild proc macros and build scripts", @@ -486,6 +491,23 @@ "markdownDescription": "Do not start rust-analyzer server when the extension is activated.", "default": false, "type": "boolean" + }, + "rust-analyzer.projectCreation.openAfterCreate": { + "markdownDescription": "Control what happens after `rust-analyzer: Create New Project...` finishes creating a Cargo project.", + "default": "ask", + "enum": [ + "ask", + "open", + "openNewWindow", + "addToWorkspace" + ], + "enumDescriptions": [ + "Prompt for how to open the new project.", + "Open the new project in the current window.", + "Open the new project in a new window.", + "Add the new project to the current workspace, or open it if no workspace is open." + ], + "type": "string" } } }, @@ -3804,6 +3826,9 @@ "command": "rust-analyzer.memoryUsage", "when": "inRustProject" }, + { + "command": "rust-analyzer.newProject" + }, { "command": "rust-analyzer.reloadWorkspace", "when": "inRustProject" @@ -3919,6 +3944,13 @@ } ] }, + "viewsWelcome": [ + { + "view": "explorer", + "contents": "Create a new Rust project.\n[Create Rust Project](command:rust-analyzer.newProject)", + "when": "workspaceFolderCount == 0" + } + ], "viewsContainers": { "activitybar": [ { @@ -3944,6 +3976,14 @@ "title": "Learn about rust-analyzer", "description": "A brief introduction to get started with rust-analyzer. Learn about key features and resources to help you get the most out of the extension.", "steps": [ + { + "id": "create-project", + "title": "Create a Rust project", + "description": "Start a new Cargo binary or library project from VS Code.\n\n[Create a Rust Project](command:rust-analyzer.newProject)", + "media": { + "markdown": "./walkthrough-create-project.md" + } + }, { "id": "setup", "title": "Useful Setup Tips", diff --git a/src/tools/rust-analyzer/editors/code/src/commands.ts b/src/tools/rust-analyzer/editors/code/src/commands.ts index 302f51dee44d2..66c981428a4ea 100644 --- a/src/tools/rust-analyzer/editors/code/src/commands.ts +++ b/src/tools/rust-analyzer/editors/code/src/commands.ts @@ -33,6 +33,7 @@ import { log } from "./util"; import type { SyntaxElement } from "./syntax_tree_provider"; export * from "./run"; +export { newProject } from "./new_project"; export function analyzerStatus(ctx: CtxInit): Cmd { const tdcp = new (class implements vscode.TextDocumentContentProvider { diff --git a/src/tools/rust-analyzer/editors/code/src/config.ts b/src/tools/rust-analyzer/editors/code/src/config.ts index d65f011c754be..0f783c71b9541 100644 --- a/src/tools/rust-analyzer/editors/code/src/config.ts +++ b/src/tools/rust-analyzer/editors/code/src/config.ts @@ -447,6 +447,10 @@ export class Config { return this.get("initializeStopped"); } + get projectCreationOpenAfterCreate() { + return this.get("projectCreation.openAfterCreate"); + } + get askBeforeUpdateTest() { return this.get("runnables.askBeforeUpdateTest"); } diff --git a/src/tools/rust-analyzer/editors/code/src/main.ts b/src/tools/rust-analyzer/editors/code/src/main.ts index 7d91286552aaa..d32d49b485e72 100644 --- a/src/tools/rust-analyzer/editors/code/src/main.ts +++ b/src/tools/rust-analyzer/editors/code/src/main.ts @@ -161,6 +161,12 @@ function createCommands(): Record { memoryUsage: { enabled: commands.memoryUsage }, reloadWorkspace: { enabled: commands.reloadWorkspace }, rebuildProcMacros: { enabled: commands.rebuildProcMacros }, + newProject: { + // Project creation is a pure VS Code-side workflow and should stay available even in + // empty windows before rust-analyzer has started or a Rust workspace exists. + enabled: commands.newProject, + disabled: commands.newProject, + }, matchingBrace: { enabled: commands.matchingBrace }, joinLines: { enabled: commands.joinLines }, parentModule: { enabled: commands.parentModule }, diff --git a/src/tools/rust-analyzer/editors/code/src/new_project.ts b/src/tools/rust-analyzer/editors/code/src/new_project.ts new file mode 100644 index 0000000000000..da3181ea3ec98 --- /dev/null +++ b/src/tools/rust-analyzer/editors/code/src/new_project.ts @@ -0,0 +1,311 @@ +import * as vscode from "vscode"; + +import type { Ctx, Cmd } from "./ctx"; +import * as ra from "./lsp_ext"; +import { cargoPath } from "./toolchain"; +import { log, spawnAsync } from "./util"; + +type NewProjectKind = "bin" | "lib"; +type NewProjectOpenAction = "open" | "openNewWindow" | "addToWorkspace"; + +type NewProjectTemplate = { + detail: string; + id: NewProjectKind; + label: string; +}; + +type NewProjectCargo = { + cargo: string; + cargoEnv: NodeJS.ProcessEnv; +}; + +const NEW_PROJECT_TEMPLATES: readonly NewProjectTemplate[] = [ + { + id: "bin", + label: "Binary Application", + detail: "Create a Cargo binary package (`cargo new --bin`)", + }, + { + id: "lib", + label: "Library", + detail: "Create a Cargo library package (`cargo new --lib`)", + }, +] as const; + +export function newProject(ctx: Ctx): Cmd { + return async () => { + const cargo = await resolveNewProjectCargo(ctx); + + const selectedKind = await promptForNewProjectTemplate(); + if (!selectedKind) { + return; + } + + const parentFolder = await promptForNewProjectParentFolder(); + if (!parentFolder) { + return; + } + + const projectName = await promptForNewProjectName(parentFolder); + if (!projectName) { + return; + } + + if (!(await createNewProject(cargo, parentFolder, selectedKind, projectName))) { + return; + } + + const projectUri = vscode.Uri.joinPath(parentFolder, projectName); + const defaultAction = determineNewProjectOpenAction( + ctx.config.projectCreationOpenAfterCreate, + Boolean(vscode.workspace.workspaceFolders?.length), + ); + const action = + defaultAction === "ask" + ? await promptForNewProjectOpenAction( + projectName, + Boolean(vscode.workspace.workspaceFolders?.length), + ) + : defaultAction; + + if (action) { + await executeNewProjectOpenAction(ctx, action, projectUri); + } + }; +} + +async function resolveNewProjectCargo(ctx: Ctx): Promise { + // Use the same effective environment rust-analyzer uses elsewhere so project creation sees + // toolchain wrappers, PATH overrides, and CARGO_HOME changes from configuration. + const cargoEnv = { ...process.env, ...ctx.config.serverExtraEnv }; + return { cargo: await cargoPath(cargoEnv), cargoEnv }; +} + +async function promptForNewProjectTemplate(): Promise { + const selected = await vscode.window.showQuickPick(NEW_PROJECT_TEMPLATES, { + placeHolder: "Select a Rust project kind", + }); + return selected?.id; +} + +async function promptForNewProjectParentFolder(): Promise { + const selectedFolder = await vscode.window.showOpenDialog({ + title: "Select the parent folder for the new Rust project", + openLabel: "Select parent folder", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + }); + if (!selectedFolder?.length) { + return undefined; + } + return selectedFolder[0]; +} + +const CARGO_MANIFEST_NAME_PATTERN = /^[\p{Alphabetic}\p{Number}_-]+$/u; + +// Keep local validation focused on stable checks that can be reported in the input box, then let +// `cargo new` remain the source of truth for package-name-specific identifier and keyword rules. +export function validateNewProjectName( + value: string, + existingNames: readonly string[], +): string | undefined { + const trimmedValue = value.trim(); + if (trimmedValue.length === 0) { + return "Project name cannot be empty."; + } + if (trimmedValue.includes("/") || trimmedValue.includes("\\")) { + return "Project name cannot contain '/' or '\\' characters."; + } + if (trimmedValue === "." || trimmedValue === "..") { + return "Project name cannot be '.' or '..'."; + } + if (!CARGO_MANIFEST_NAME_PATTERN.test(trimmedValue)) { + return "Project name can contain only alphanumeric characters, '-' or '_'."; + } + if (existingNames.includes(trimmedValue)) { + return "A file or folder with this name already exists."; + } + return undefined; +} + +async function promptForNewProjectName(parentFolder: vscode.Uri): Promise { + let existingNames: string[] = []; + try { + const entries = await vscode.workspace.fs.readDirectory(parentFolder); + existingNames = entries.map(([name]) => name); + } catch (error) { + log.error("Failed to read project parent folder", error); + void vscode.window.showErrorMessage("Failed to read the selected parent folder."); + return undefined; + } + + const projectName = await vscode.window.showInputBox({ + prompt: `Enter the new project name to create inside ${parentFolder.fsPath}`, + validateInput: async (value) => validateNewProjectName(value, existingNames), + }); + return projectName?.trim(); +} + +export function cargoNewArgs(kind: NewProjectKind, name: string): string[] { + return ["new", kind === "bin" ? "--bin" : "--lib", name]; +} + +async function createNewProject( + cargo: NewProjectCargo, + parentFolder: vscode.Uri, + kind: NewProjectKind, + projectName: string, +): Promise { + const args = cargoNewArgs(kind, projectName); + const createResult = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Creating Rust project ${projectName}`, + }, + async () => + spawnAsync(cargo.cargo, args, { + cwd: parentFolder.fsPath, + env: cargo.cargoEnv, + }), + ); + + if (createResult.error || createResult.status !== 0) { + const details = formatProcessDetails(createResult); + await showNewProjectError("Failed to create Rust project.", details || undefined, { + cargo: cargo.cargo, + args, + cwd: parentFolder.fsPath, + error: createResult.error?.message, + status: createResult.status, + stderr: createResult.stderr || undefined, + stdout: createResult.stdout || undefined, + }); + return false; + } + + return true; +} + +function formatProcessDetails(result: { error?: Error; stderr: string; stdout: string }): string { + return [result.stderr, result.stdout, result.error?.message] + .filter((value): value is string => Boolean(value && value.trim().length > 0)) + .join("\n") + .trim(); +} + +async function showNewProjectError( + message: string, + details: string | undefined, + logContext: { + cargo: string; + args: string[]; + cwd?: string; + error?: string; + status: number | null; + stderr?: string; + stdout?: string; + }, +): Promise { + // Keep command-failure logging focused on the invocation and process output. Environment + // variables may contain secrets such as API keys, tokens, and credentials, so failure logs + // must not dump the merged env here. + const commandLine = [logContext.cargo, ...logContext.args].join(" "); + log.error(message); + log.error(`command: ${commandLine}`); + if (logContext.cwd) { + log.error(`cwd: ${logContext.cwd}`); + } + log.error(`exit status: ${String(logContext.status)}`); + if (logContext.error) { + log.error(`error: ${logContext.error}`); + } + if (logContext.stderr) { + log.error(`stderr:\n${logContext.stderr}`); + } + if (logContext.stdout) { + log.error(`stdout:\n${logContext.stdout}`); + } + const selection = await vscode.window.showErrorMessage( + details ? `${message}\n${details}` : message, + "Open Logs", + ); + if (selection === "Open Logs") { + await vscode.commands.executeCommand("rust-analyzer.openLogs"); + } +} + +export function determineNewProjectOpenAction( + configuredAction: string | undefined, + hasWorkspaceFolders: boolean, +): "ask" | NewProjectOpenAction { + switch (configuredAction) { + case "open": + case "openNewWindow": + return configuredAction; + case "addToWorkspace": + // Adding to a workspace only makes sense when one is already open. Falling back to + // "open" keeps the setting usable in empty windows without adding another prompt path. + return hasWorkspaceFolders ? configuredAction : "open"; + default: + return "ask"; + } +} + +async function promptForNewProjectOpenAction( + projectName: string, + hasWorkspaceFolders: boolean, +): Promise { + let message = `Would you like to open ${projectName}?`; + const open = "Open"; + const openNewWindow = "Open in New Window"; + const choices = [open, openNewWindow]; + + const addToWorkspace = "Add to VS Code Workspace"; + if (hasWorkspaceFolders) { + message = `Would you like to open ${projectName}, or add it to the current VS Code workspace?`; + choices.push(addToWorkspace); + } + + const result = await vscode.window.showInformationMessage( + message, + { modal: true, detail: "The default action can be configured in settings." }, + ...choices, + ); + + const actionMap: Record = { + [open]: "open", + [openNewWindow]: "openNewWindow", + [addToWorkspace]: "addToWorkspace", + }; + return result ? actionMap[result] : undefined; +} + +async function executeNewProjectOpenAction( + ctx: Ctx, + action: NewProjectOpenAction, + projectUri: vscode.Uri, +): Promise { + if (action === "open") { + await vscode.commands.executeCommand("vscode.openFolder", projectUri, { + forceReuseWindow: true, + }); + return; + } + + if (action === "openNewWindow") { + await vscode.commands.executeCommand("vscode.openFolder", projectUri, { + forceNewWindow: true, + }); + return; + } + + const index = vscode.workspace.workspaceFolders?.length ?? 0; + vscode.workspace.updateWorkspaceFolders(index, 0, { uri: projectUri }); + // Reuse the existing workspace window when requested, but nudge rust-analyzer afterwards so + // the newly added Cargo project is discovered immediately instead of waiting for a later + // background refresh. + if (ctx.client?.isRunning()) { + await ctx.client.sendRequest(ra.reloadWorkspace); + } +} diff --git a/src/tools/rust-analyzer/editors/code/src/toolchain.ts b/src/tools/rust-analyzer/editors/code/src/toolchain.ts index 76946d1510162..784ee260d088e 100644 --- a/src/tools/rust-analyzer/editors/code/src/toolchain.ts +++ b/src/tools/rust-analyzer/editors/code/src/toolchain.ts @@ -160,9 +160,44 @@ export function cargoPath(env?: Env): Promise { if (env?.["RUSTC_TOOLCHAIN"]) { return Promise.resolve("cargo"); } + if (env) { + return getPathForExecutableWithEnv("cargo", env); + } return getPathForExecutable("cargo"); } +/** + * Resolves an executable using an explicitly supplied environment instead of the VS Code host + * process environment. + * + * Some extension call sites already construct the exact env they will use for spawning, so path + * resolution needs to honor that same `PATH`/`CARGO_HOME` view to avoid launching a different + * toolchain than the one that was resolved. + */ +async function getPathForExecutableWithEnv( + executableName: "cargo" | "rustc" | "rustup", + env: Env, +): Promise { + const envVar = env[executableName.toUpperCase()]; + if (envVar) { + return envVar; + } + + if (await lookupInPath(executableName, env["PATH"] ?? "")) { + return executableName; + } + + const cargoHome = getCargoHomeFromPath(env["CARGO_HOME"]); + if (cargoHome) { + const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName); + if (await isFileAtUri(standardPath)) { + return standardPath.fsPath; + } + } + + return executableName; +} + /** Mirrors `toolchain::get_path_for_executable()` implementation */ const getPathForExecutable = memoizeAsync( // We apply caching to decrease file-system interactions @@ -172,7 +207,7 @@ const getPathForExecutable = memoizeAsync( if (envVar) return envVar; } - if (await lookupInPath(executableName)) return executableName; + if (await lookupInPath(executableName, process.env["PATH"] ?? "")) return executableName; const cargoHome = getCargoHome(); if (cargoHome) { @@ -183,9 +218,7 @@ const getPathForExecutable = memoizeAsync( }, ); -async function lookupInPath(exec: string): Promise { - const paths = process.env["PATH"] ?? ""; - +async function lookupInPath(exec: string, paths: string): Promise { const candidates = paths.split(path.delimiter).flatMap((dirInPath) => { const candidate = path.join(dirInPath, exec); return os.type() === "Windows_NT" ? [candidate, `${candidate}.exe`] : [candidate]; @@ -201,6 +234,10 @@ async function lookupInPath(exec: string): Promise { function getCargoHome(): vscode.Uri | null { const envVar = process.env["CARGO_HOME"]; + return getCargoHomeFromPath(envVar); +} + +function getCargoHomeFromPath(envVar: string | undefined): vscode.Uri | null { if (envVar) return vscode.Uri.file(envVar); try { diff --git a/src/tools/rust-analyzer/editors/code/tests/unit/commands.test.ts b/src/tools/rust-analyzer/editors/code/tests/unit/commands.test.ts new file mode 100644 index 0000000000000..900bb2779d565 --- /dev/null +++ b/src/tools/rust-analyzer/editors/code/tests/unit/commands.test.ts @@ -0,0 +1,80 @@ +import * as assert from "node:assert/strict"; + +import { + cargoNewArgs, + determineNewProjectOpenAction, + validateNewProjectName, +} from "../../src/new_project"; +import type { Context } from "."; + +export async function getTests(ctx: Context) { + await ctx.suite("New project command", (suite) => { + suite.addTest("rejects empty project name", async () => { + assert.equal(validateNewProjectName("", []), "Project name cannot be empty."); + assert.equal(validateNewProjectName(" ", []), "Project name cannot be empty."); + }); + + suite.addTest("rejects dot project names", async () => { + assert.equal(validateNewProjectName(".", []), "Project name cannot be '.' or '..'."); + assert.equal(validateNewProjectName("..", []), "Project name cannot be '.' or '..'."); + }); + + suite.addTest("rejects path separators", async () => { + assert.equal( + validateNewProjectName("foo/bar", []), + "Project name cannot contain '/' or '\\' characters.", + ); + assert.equal( + validateNewProjectName("foo\\bar", []), + "Project name cannot contain '/' or '\\' characters.", + ); + }); + + suite.addTest("rejects invalid Cargo package name characters", async () => { + assert.equal( + validateNewProjectName("foo.bar", []), + "Project name can contain only alphanumeric characters, '-' or '_'.", + ); + assert.equal( + validateNewProjectName("foo bar", []), + "Project name can contain only alphanumeric characters, '-' or '_'.", + ); + assert.equal( + validateNewProjectName("foo+bar", []), + "Project name can contain only alphanumeric characters, '-' or '_'.", + ); + }); + + suite.addTest("rejects existing child folder collisions", async () => { + assert.equal( + validateNewProjectName("demo", ["demo"]), + "A file or folder with this name already exists.", + ); + }); + + suite.addTest("accepts a normal project name", async () => { + assert.equal(validateNewProjectName("demo-project", []), undefined); + }); + + suite.addTest("resolves addToWorkspace fallback without workspace", async () => { + assert.equal(determineNewProjectOpenAction("addToWorkspace", false), "open"); + }); + + suite.addTest("keeps addToWorkspace when workspace exists", async () => { + assert.equal(determineNewProjectOpenAction("addToWorkspace", true), "addToWorkspace"); + }); + + suite.addTest("defaults to ask for unknown values", async () => { + assert.equal(determineNewProjectOpenAction(undefined, true), "ask"); + assert.equal(determineNewProjectOpenAction("ask", true), "ask"); + }); + + suite.addTest("builds binary cargo args", async () => { + assert.deepEqual(cargoNewArgs("bin", "demo"), ["new", "--bin", "demo"]); + }); + + suite.addTest("builds library cargo args", async () => { + assert.deepEqual(cargoNewArgs("lib", "demo"), ["new", "--lib", "demo"]); + }); + }); +} diff --git a/src/tools/rust-analyzer/editors/code/walkthrough-create-project.md b/src/tools/rust-analyzer/editors/code/walkthrough-create-project.md new file mode 100644 index 0000000000000..34c04cdd1db4a --- /dev/null +++ b/src/tools/rust-analyzer/editors/code/walkthrough-create-project.md @@ -0,0 +1,9 @@ +# Create a New Cargo Project + +Create a new Cargo project without leaving VS Code. + +The command lets you choose the package kind, parent folder, project name, and +how the new project should be opened afterward. + +The project is created with Cargo, so the generated files and defaults match +the normal `cargo new` experience. From f018c4eb6413b8e578406a0c1c2b0d0cd02572d6 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Sun, 24 May 2026 17:50:15 -0700 Subject: [PATCH 02/57] Fix new-project log handling Point the new project failure flow at the extension logs and make the log-opening command available when the language server is not running. Also add tests for the env-aware cargo path resolution path introduced for project creation. --- .../editors/code/src/new_project.ts | 6 +-- .../rust-analyzer/editors/code/src/util.ts | 6 ++- .../code/tests/unit/launch_config.test.ts | 44 ++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/tools/rust-analyzer/editors/code/src/new_project.ts b/src/tools/rust-analyzer/editors/code/src/new_project.ts index da3181ea3ec98..f5f6d86da04df 100644 --- a/src/tools/rust-analyzer/editors/code/src/new_project.ts +++ b/src/tools/rust-analyzer/editors/code/src/new_project.ts @@ -228,10 +228,10 @@ async function showNewProjectError( } const selection = await vscode.window.showErrorMessage( details ? `${message}\n${details}` : message, - "Open Logs", + "Open Extension Logs", ); - if (selection === "Open Logs") { - await vscode.commands.executeCommand("rust-analyzer.openLogs"); + if (selection === "Open Extension Logs") { + log.show(); } } diff --git a/src/tools/rust-analyzer/editors/code/src/util.ts b/src/tools/rust-analyzer/editors/code/src/util.ts index 05b475080cbd2..ddeec814b4158 100644 --- a/src/tools/rust-analyzer/editors/code/src/util.ts +++ b/src/tools/rust-analyzer/editors/code/src/util.ts @@ -22,6 +22,10 @@ class Log { log: true, }); + show(): void { + this.output.show(true); + } + trace(...messages: [unknown, ...unknown[]]): void { this.output.trace(this.stringify(messages)); } @@ -40,7 +44,7 @@ class Log { error(...messages: [unknown, ...unknown[]]): void { this.output.error(this.stringify(messages)); - this.output.show(true); + this.show(); } private stringify(messages: unknown[]): string { diff --git a/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts b/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts index eeb8608a42db5..69be84dcea6df 100644 --- a/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts +++ b/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts @@ -1,5 +1,8 @@ import * as assert from "assert"; -import { Cargo } from "../../src/toolchain"; +import * as os from "os"; +import * as path from "path"; +import { mkdtemp, mkdir, rm, writeFile } from "fs/promises"; +import { Cargo, cargoPath } from "../../src/toolchain"; import type { Context } from "."; export async function getTests(ctx: Context) { @@ -96,4 +99,43 @@ export async function getTests(ctx: Context) { assert.notDeepStrictEqual(args.filter, undefined); }); }); + + await ctx.suite("Toolchain resolution", (suite) => { + suite.addTest("prefers explicit CARGO from provided env", async () => { + const explicitCargo = path.join(os.tmpdir(), "custom-cargo"); + assert.strictEqual(await cargoPath({ CARGO: explicitCargo }), explicitCargo); + }); + + suite.addTest("resolves cargo from provided PATH", async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "ra-cargo-path-")); + try { + const cargoBinary = path.join(tempDir, process.platform === "win32" ? "cargo.exe" : "cargo"); + await writeFile(cargoBinary, ""); + + assert.strictEqual(await cargoPath({ PATH: tempDir }), "cargo"); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); + + suite.addTest("resolves cargo from provided CARGO_HOME", async () => { + const cargoHome = await mkdtemp(path.join(os.tmpdir(), "ra-cargo-home-")); + try { + const binDir = path.join(cargoHome, "bin"); + await mkdir(binDir); + const cargoBinary = path.join( + binDir, + process.platform === "win32" ? "cargo.exe" : "cargo", + ); + await writeFile(cargoBinary, ""); + + assert.strictEqual( + await cargoPath({ PATH: "", CARGO_HOME: cargoHome }), + cargoBinary, + ); + } finally { + await rm(cargoHome, { recursive: true, force: true }); + } + }); + }); } From abba25eb718b26dbef19c2ed6716abfe093d47b6 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Mon, 25 May 2026 16:06:14 +0800 Subject: [PATCH 03/57] fix: rename schema subItems with sub_items --- src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs | 4 ++-- .../rust-analyzer/docs/book/src/configuration_generated.md | 2 +- src/tools/rust-analyzer/editors/code/package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs index 6f532e4224885..74ba183f97ddb 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs @@ -662,7 +662,7 @@ config_data! { /// For traits the type "methods" can be used to only exclude the methods but not the trait /// itself. /// - /// For modules the type "subItems" can be used to only exclude the all items in it but not the module + /// For modules the type "sub_items" can be used to only exclude the all items in it but not the module /// itself. This does not include items defined in nested modules. /// /// This setting also inherits `#rust-analyzer.completion.excludeTraits#`. @@ -4135,7 +4135,7 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json }, "type": { "type": "string", - "enum": ["always", "methods", "subItems"], + "enum": ["always", "methods", "sub_items"], "enumDescriptions": [ "Do not show this item or its methods (if it is a trait) in auto-import completions.", "Do not show this trait's methods in auto-import completions.", diff --git a/src/tools/rust-analyzer/docs/book/src/configuration_generated.md b/src/tools/rust-analyzer/docs/book/src/configuration_generated.md index 7bbb9e0258169..bc5ef9e96c02a 100644 --- a/src/tools/rust-analyzer/docs/book/src/configuration_generated.md +++ b/src/tools/rust-analyzer/docs/book/src/configuration_generated.md @@ -446,7 +446,7 @@ verbose form `{ "path": "path::to::item", type: "always" }`. For traits the type "methods" can be used to only exclude the methods but not the trait itself. -For modules the type "subItems" can be used to only exclude the all items in it but not the module +For modules the type "sub_items" can be used to only exclude the all items in it but not the module itself. This does not include items defined in nested modules. This setting also inherits `#rust-analyzer.completion.excludeTraits#`. diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json index 8df606d4c4ca8..9ca1e80c6ac7b 100644 --- a/src/tools/rust-analyzer/editors/code/package.json +++ b/src/tools/rust-analyzer/editors/code/package.json @@ -1328,7 +1328,7 @@ "title": "Completion", "properties": { "rust-analyzer.completion.autoimport.exclude": { - "markdownDescription": "A list of full paths to items to exclude from auto-importing completions.\n\nTraits in this list won't have their methods suggested in completions unless the trait\nis in scope.\n\nYou can either specify a string path which defaults to type \"always\" or use the more\nverbose form `{ \"path\": \"path::to::item\", type: \"always\" }`.\n\nFor traits the type \"methods\" can be used to only exclude the methods but not the trait\nitself.\n\nFor modules the type \"subItems\" can be used to only exclude the all items in it but not the module\nitself. This does not include items defined in nested modules.\n\nThis setting also inherits `#rust-analyzer.completion.excludeTraits#`.", + "markdownDescription": "A list of full paths to items to exclude from auto-importing completions.\n\nTraits in this list won't have their methods suggested in completions unless the trait\nis in scope.\n\nYou can either specify a string path which defaults to type \"always\" or use the more\nverbose form `{ \"path\": \"path::to::item\", type: \"always\" }`.\n\nFor traits the type \"methods\" can be used to only exclude the methods but not the trait\nitself.\n\nFor modules the type \"sub_items\" can be used to only exclude the all items in it but not the module\nitself. This does not include items defined in nested modules.\n\nThis setting also inherits `#rust-analyzer.completion.excludeTraits#`.", "default": [ { "path": "core::borrow::Borrow", @@ -1356,7 +1356,7 @@ "enum": [ "always", "methods", - "subItems" + "sub_items" ], "enumDescriptions": [ "Do not show this item or its methods (if it is a trait) in auto-import completions.", From 460c582f863e57825436d4541a2620c8b56f5566 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Mon, 25 May 2026 14:05:59 -0700 Subject: [PATCH 04/57] Format launch config test Run the VS Code formatter to fix the remaining Prettier issue in the launch config test before updating rust-lang/rust-analyzer#22103. AI-assisted: OpenAI Codex was used to identify and apply this change. --- .../editors/code/tests/unit/launch_config.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts b/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts index 69be84dcea6df..9bba33f8e9d20 100644 --- a/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts +++ b/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts @@ -109,7 +109,10 @@ export async function getTests(ctx: Context) { suite.addTest("resolves cargo from provided PATH", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "ra-cargo-path-")); try { - const cargoBinary = path.join(tempDir, process.platform === "win32" ? "cargo.exe" : "cargo"); + const cargoBinary = path.join( + tempDir, + process.platform === "win32" ? "cargo.exe" : "cargo", + ); await writeFile(cargoBinary, ""); assert.strictEqual(await cargoPath({ PATH: tempDir }), "cargo"); From b8155a639ddfafaf00a15ba1c3e5cab3e544354b Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Mon, 25 May 2026 14:05:59 -0700 Subject: [PATCH 05/57] Fix VS Code cargo executable probing Handle Windows executable suffixes when the VS Code extension probes for cargo through PATH or CARGO_HOME, and preserve case-insensitive environment variable lookup for supplied env objects so copied Windows `Path` entries still resolve cargo. Normalize and document the Windows CARGO_HOME test comparison as well, because VS Code path resolution can lowercase the drive letter in `fsPath` without changing which executable was found. This keeps the extension-side lookup aligned with the Rust-side toolchain helper behavior and avoids breaking create-project on common Windows setups. AI-assisted: OpenAI Codex was used to identify and apply this change. --- .../editors/code/src/toolchain.ts | 41 ++++++++++++++----- .../code/tests/unit/launch_config.test.ts | 24 ++++++----- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/tools/rust-analyzer/editors/code/src/toolchain.ts b/src/tools/rust-analyzer/editors/code/src/toolchain.ts index 784ee260d088e..6ae365c71c7ac 100644 --- a/src/tools/rust-analyzer/editors/code/src/toolchain.ts +++ b/src/tools/rust-analyzer/editors/code/src/toolchain.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import * as readline from "readline"; import * as vscode from "vscode"; -import { Env, log, memoizeAsync, unwrapUndefinable } from "./util"; +import { Env, isWindows, log, memoizeAsync, unwrapUndefinable } from "./util"; import type { CargoRunnableArgs } from "./lsp_ext"; interface CompilationArtifact { @@ -178,20 +178,22 @@ async function getPathForExecutableWithEnv( executableName: "cargo" | "rustc" | "rustup", env: Env, ): Promise { - const envVar = env[executableName.toUpperCase()]; + const envVar = getEnvVar(env, executableName.toUpperCase()); if (envVar) { return envVar; } - if (await lookupInPath(executableName, env["PATH"] ?? "")) { + if (await lookupInPath(executableName, getEnvVar(env, "PATH") ?? "")) { return executableName; } - const cargoHome = getCargoHomeFromPath(env["CARGO_HOME"]); + const cargoHome = getCargoHomeFromPath(getEnvVar(env, "CARGO_HOME")); if (cargoHome) { - const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName); - if (await isFileAtUri(standardPath)) { - return standardPath.fsPath; + for (const candidate of executableCandidates(executableName)) { + const standardPath = vscode.Uri.joinPath(cargoHome, "bin", candidate); + if (await isFileAtUri(standardPath)) { + return standardPath.fsPath; + } } } @@ -211,8 +213,10 @@ const getPathForExecutable = memoizeAsync( const cargoHome = getCargoHome(); if (cargoHome) { - const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName); - if (await isFileAtUri(standardPath)) return standardPath.fsPath; + for (const candidate of executableCandidates(executableName)) { + const standardPath = vscode.Uri.joinPath(cargoHome, "bin", candidate); + if (await isFileAtUri(standardPath)) return standardPath.fsPath; + } } return executableName; }, @@ -220,8 +224,7 @@ const getPathForExecutable = memoizeAsync( async function lookupInPath(exec: string, paths: string): Promise { const candidates = paths.split(path.delimiter).flatMap((dirInPath) => { - const candidate = path.join(dirInPath, exec); - return os.type() === "Windows_NT" ? [candidate, `${candidate}.exe`] : [candidate]; + return executableCandidates(exec).map((candidate) => path.join(dirInPath, candidate)); }); for await (const isFile of candidates.map(isFileAtPath)) { @@ -232,6 +235,22 @@ async function lookupInPath(exec: string, paths: string): Promise { return false; } +function executableCandidates(executableName: string): string[] { + // Keep the extension-side probe aligned with `crates/toolchain::probe_for_binary()`, which + // checks both the bare executable name and the platform suffix such as `.exe` on Windows. + // That matters for both PATH lookups and `$CARGO_HOME/bin/` fallbacks. + return isWindows ? [executableName, `${executableName}.exe`] : [executableName]; +} + +function getEnvVar(env: Env, name: string): string | undefined { + if (!isWindows) { + return env[name]; + } + + const foldedName = name.toLowerCase(); + return Object.entries(env).find(([key]) => key.toLowerCase() === foldedName)?.[1]; +} + function getCargoHome(): vscode.Uri | null { const envVar = process.env["CARGO_HOME"]; return getCargoHomeFromPath(envVar); diff --git a/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts b/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts index 9bba33f8e9d20..8da62cd241655 100644 --- a/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts +++ b/src/tools/rust-analyzer/editors/code/tests/unit/launch_config.test.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as path from "path"; import { mkdtemp, mkdir, rm, writeFile } from "fs/promises"; import { Cargo, cargoPath } from "../../src/toolchain"; +import { isWindows, normalizeDriveLetter } from "../../src/util"; import type { Context } from "."; export async function getTests(ctx: Context) { @@ -109,13 +110,11 @@ export async function getTests(ctx: Context) { suite.addTest("resolves cargo from provided PATH", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "ra-cargo-path-")); try { - const cargoBinary = path.join( - tempDir, - process.platform === "win32" ? "cargo.exe" : "cargo", - ); + const cargoBinary = path.join(tempDir, isWindows ? "cargo.exe" : "cargo"); await writeFile(cargoBinary, ""); - assert.strictEqual(await cargoPath({ PATH: tempDir }), "cargo"); + const pathKey = isWindows ? "Path" : "PATH"; + assert.strictEqual(await cargoPath({ [pathKey]: tempDir }), "cargo"); } finally { await rm(tempDir, { recursive: true, force: true }); } @@ -126,15 +125,12 @@ export async function getTests(ctx: Context) { try { const binDir = path.join(cargoHome, "bin"); await mkdir(binDir); - const cargoBinary = path.join( - binDir, - process.platform === "win32" ? "cargo.exe" : "cargo", - ); + const cargoBinary = path.join(binDir, isWindows ? "cargo.exe" : "cargo"); await writeFile(cargoBinary, ""); assert.strictEqual( - await cargoPath({ PATH: "", CARGO_HOME: cargoHome }), - cargoBinary, + normalizeComparablePath(await cargoPath({ PATH: "", CARGO_HOME: cargoHome })), + normalizeComparablePath(cargoBinary), ); } finally { await rm(cargoHome, { recursive: true, force: true }); @@ -142,3 +138,9 @@ export async function getTests(ctx: Context) { }); }); } + +function normalizeComparablePath(filePath: string): string { + // Windows path comparisons should ignore drive-letter casing because `fsPath` + // may normalize it differently while still pointing to the same file. + return normalizeDriveLetter(filePath, isWindows); +} From 1d9b66032c690ab3c47bec1f5341a62ef35b7d2e Mon Sep 17 00:00:00 2001 From: itang06 Date: Wed, 13 May 2026 00:59:07 -0700 Subject: [PATCH 06/57] internal: add item_list and mod_ constructors to SyntaxFactory Part of #18285 --- .../src/handlers/extract_module.rs | 5 +-- .../src/ast/syntax_factory/constructors.rs | 33 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs index 40eaed0080a0a..4e30b3efb280e 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs @@ -17,9 +17,10 @@ use syntax::{ ast::{ self, HasVisibility, edit::{AstNodeEdit, IndentLevel}, - make, + syntax_factory::SyntaxFactory, }, - match_ast, ted, + match_ast, + syntax_editor::{Position, SyntaxEditor}, }; use crate::{AssistContext, Assists}; diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/syntax_factory/constructors.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/syntax_factory/constructors.rs index 2f7eab2423ce5..7f9cd1fce94eb 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/syntax_factory/constructors.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/syntax_factory/constructors.rs @@ -4,8 +4,8 @@ use either::Either; use crate::{ AstNode, Edition, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, ast::{ - self, HasArgList, HasAttrs, HasGenericArgs, HasGenericParams, HasLoopBody, HasName, - HasTypeBounds, HasVisibility, Lifetime, Param, RangeItem, make, + self, HasArgList, HasAttrs, HasGenericArgs, HasGenericParams, HasLoopBody, HasModuleItem, + HasName, HasTypeBounds, HasVisibility, Lifetime, Param, RangeItem, make, }, syntax_editor::SyntaxMappingBuilder, }; @@ -2015,6 +2015,35 @@ impl SyntaxFactory { make::assoc_item_list(None).clone_for_update() } + pub fn item_list(&self, items: impl IntoIterator) -> ast::ItemList { + let (items, input) = iterator_input(items); + let items_vec: Vec<_> = items.into_iter().collect(); + let ast = make::item_list(Some(items_vec)).clone_for_update(); + + if let Some(mut mapping) = self.mappings() { + let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone()); + builder.map_children(input, ast.items().map(|item: ast::Item| item.syntax().clone())); + builder.finish(&mut mapping); + } + + ast + } + + pub fn mod_(&self, name: ast::Name, body: Option) -> ast::Module { + let ast = make::mod_(name.clone(), body.clone()).clone_for_update(); + + if let Some(mut mapping) = self.mappings() { + let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone()); + builder.map_node(name.syntax().clone(), ast.name().unwrap().syntax().clone()); + if let Some(body) = body { + builder.map_node(body.syntax().clone(), ast.item_list().unwrap().syntax().clone()); + } + builder.finish(&mut mapping); + } + + ast + } + pub fn attr_outer(&self, meta: ast::Meta) -> ast::Attr { let ast = make::attr_outer(meta.clone()).clone_for_update(); From 37188cca9a41483f9dcd3e7d116dadb965aed672 Mon Sep 17 00:00:00 2001 From: itang06 Date: Wed, 13 May 2026 00:05:22 -0700 Subject: [PATCH 07/57] internal: migrate extract_module assist to SyntaxEditor --- .../src/handlers/extract_module.rs | 172 +++++++++++------- .../crates/syntax/src/ast/edit_in_place.rs | 10 - 2 files changed, 104 insertions(+), 78 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs index 4e30b3efb280e..40c54e98820a8 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_module.rs @@ -122,15 +122,18 @@ pub(crate) fn extract_module(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> let (usages_to_be_processed, record_fields, use_stmts_to_be_inserted) = module.get_usages_and_record_fields(ctx, module_text_range); + let make = SyntaxFactory::without_mappings(); + builder.edit_file(ctx.vfs_file_id()); use_stmts_to_be_inserted.into_iter().for_each(|(_, use_stmt)| { builder.insert(ctx.selection_trimmed().end(), format!("\n{use_stmt}")); }); - let import_items = module.resolve_imports(curr_parent_module, ctx); - module.change_visibility(record_fields); + let import_items = module.resolve_imports(curr_parent_module, ctx, &make); + module.change_visibility(record_fields, &make); - let module_def = generate_module_def(&impl_parent, &module).indent(old_item_indent); + let module_def = + generate_module_def(&impl_parent, &module, &make).indent(old_item_indent); let mut usages_to_be_processed_for_cur_file = vec![]; for (file_id, usages) in usages_to_be_processed { @@ -186,6 +189,7 @@ pub(crate) fn extract_module(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> fn generate_module_def( parent_impl: &Option, Module { name, body_items, use_items }: &Module, + make: &SyntaxFactory, ) -> ast::Module { let items: Vec<_> = if let Some(impl_) = parent_impl.as_ref() && let Some(self_ty) = impl_.self_ty() @@ -196,11 +200,15 @@ fn generate_module_def( .filter_map(ast::AssocItem::cast) .map(|it| it.indent(IndentLevel(1))) .collect_vec(); - let assoc_item_list = make::assoc_item_list(Some(assoc_items)).clone_for_update(); - let impl_ = impl_.reset_indent(); - ted::replace(impl_.get_or_create_assoc_item_list().syntax(), assoc_item_list.syntax()); + let impl_reset = impl_.reset_indent(); + let (editor, impl_root) = SyntaxEditor::with_ast_node(&impl_reset); + let assoc_item_list = editor.make().assoc_item_list(assoc_items); + if let Some(existing_list) = impl_root.assoc_item_list() { + editor.replace(existing_list.syntax(), assoc_item_list.syntax()); + } + let impl_ = ast::Impl::cast(editor.finish().new_root().clone()).unwrap(); // Add the import for enum/struct corresponding to given impl block - let use_impl = make_use_stmt_of_node_with_super(self_ty.syntax()); + let use_impl = make_use_stmt_of_node_with_super(self_ty.syntax(), make); once(use_impl) .chain(use_items.iter().cloned()) .chain(once(ast::Item::Impl(impl_))) @@ -210,19 +218,18 @@ fn generate_module_def( }; let items = items.into_iter().map(|it| it.reset_indent().indent(IndentLevel(1))).collect_vec(); - let module_body = make::item_list(Some(items)); - - let module_name = make::name(name); - make::mod_(module_name, Some(module_body)) + let module_body = make.item_list(items); + let module_name = make.name(name); + make.mod_(module_name, Some(module_body)) } -fn make_use_stmt_of_node_with_super(node_syntax: &SyntaxNode) -> ast::Item { - let super_path = make::ext::ident_path("super"); - let node_path = make::ext::ident_path(&node_syntax.to_string()); - let use_ = make::use_( +fn make_use_stmt_of_node_with_super(node_syntax: &SyntaxNode, make: &SyntaxFactory) -> ast::Item { + let super_path = make.ident_path("super"); + let node_path = make.path_from_text(&node_syntax.to_string()); + let use_ = make.use_( + [], None, - None, - make::use_tree(make::join_paths(vec![super_path, node_path]), None, None, false), + make.use_tree(make.path_concat(super_path, node_path), None, None, false), ); ast::Item::from(use_) @@ -386,18 +393,30 @@ impl Module { if use_.syntax().parent().is_some_and(|parent| parent == covering_node) && use_stmts_set.insert(use_.syntax().text_range().start()) { - let use_ = use_stmts_to_be_inserted - .entry(use_.syntax().text_range().start()) - .or_insert_with(|| use_.clone_subtree().clone_for_update()); - for seg in use_ - .syntax() - .descendants() - .filter_map(ast::NameRef::cast) - .filter(|seg| seg.syntax().to_string() == name_ref.to_string()) - { - let new_ref = make::path_from_text(&format!("{mod_name}::{seg}")) - .clone_for_update(); - ted::replace(seg.syntax().parent()?, new_ref.syntax()); + let key = use_.syntax().text_range().start(); + let entry = + use_stmts_to_be_inserted.entry(key).or_insert_with(|| use_.clone()); + let (editor, edit_root) = SyntaxEditor::with_ast_node(&*entry); + let replacements: Vec<_> = { + let make = editor.make(); + edit_root + .syntax() + .descendants() + .filter_map(ast::NameRef::cast) + .filter(|seg| seg.syntax().to_string() == name_ref.to_string()) + .filter_map(|seg| { + Some(( + seg.syntax().parent()?, + make.path_from_text(&format!("{mod_name}::{seg}")), + )) + }) + .collect() + }; + if !replacements.is_empty() { + for (parent, new_ref) in &replacements { + editor.replace(parent, new_ref.syntax()); + } + *entry = ast::Use::cast(editor.finish().new_root().clone()).unwrap(); } } } @@ -408,18 +427,23 @@ impl Module { } } - fn change_visibility(&mut self, record_fields: Vec) { + fn change_visibility(&mut self, record_fields: Vec, make: &SyntaxFactory) { + for item in &mut self.body_items { + let (_, root) = SyntaxEditor::with_ast_node(&item.reset_indent()); + *item = root; + } + let (mut replacements, record_field_parents, impls) = - get_replacements_for_visibility_change(&mut self.body_items, false); + get_replacements_for_visibility_change(&self.body_items); - let mut impl_items = impls + let impl_items = impls .into_iter() .flat_map(|impl_| impl_.syntax().descendants()) .filter_map(ast::Item::cast) .collect_vec(); let (mut impl_item_replacements, _, _) = - get_replacements_for_visibility_change(&mut impl_items, true); + get_replacements_for_visibility_change(&impl_items); replacements.append(&mut impl_item_replacements); @@ -433,17 +457,39 @@ impl Module { } } - for (vis, syntax) in replacements { - let item = syntax.children_with_tokens().find(|node_or_token| { - match node_or_token.kind() { - // We're skipping comments, doc comments, and attribute macros that may precede the keyword - // that the visibility should be placed before. - SyntaxKind::COMMENT | SyntaxKind::ATTR | SyntaxKind::WHITESPACE => false, - _ => true, - } - }); + for body_item in &mut self.body_items { + let insert_targets: Vec<_> = replacements + .iter() + .filter(|(vis, syntax)| { + vis.is_none() + && (syntax == body_item.syntax() + || syntax.ancestors().any(|a| &a == body_item.syntax())) + }) + .filter_map(|(_, syntax)| { + syntax.children_with_tokens().find(|nt| { + !matches!( + nt.kind(), + SyntaxKind::COMMENT | SyntaxKind::ATTR | SyntaxKind::WHITESPACE + ) + }) + }) + .collect(); - add_change_vis(vis, item); + if insert_targets.is_empty() { + continue; + } + + let (editor, _) = SyntaxEditor::new(body_item.syntax().clone()); + for target in insert_targets { + editor.insert_all( + Position::before(target), + vec![ + make.visibility_pub_crate().syntax().clone().into(), + make.whitespace(" ").into(), + ], + ); + } + *body_item = ast::Item::cast(editor.finish().new_root().clone()).unwrap(); } } @@ -451,6 +497,7 @@ impl Module { &mut self, module: Option, ctx: &AssistContext<'_, '_>, + make: &SyntaxFactory, ) -> Vec { let mut imports_to_remove = vec![]; let mut node_set = FxHashSet::default(); @@ -477,7 +524,8 @@ impl Module { }) .for_each(|(node, def)| { if node_set.insert(node.to_string()) - && let Some(import) = self.process_def_in_sel(def, &node, &module, ctx) + && let Some(import) = + self.process_def_in_sel(def, &node, &module, ctx, make) { check_intersection_and_push(&mut imports_to_remove, import); } @@ -493,6 +541,7 @@ impl Module { use_node: &SyntaxNode, curr_parent_module: &Option, ctx: &AssistContext<'_, '_>, + make: &SyntaxFactory, ) -> Option { //We only need to find in the current file let selection_range = ctx.selection_trimmed(); @@ -568,7 +617,7 @@ impl Module { // mod -> ust_stmt transversal // true | false -> super import insertion // true | true -> super import insertion - let super_use_node = make_use_stmt_of_node_with_super(use_node); + let super_use_node = make_use_stmt_of_node_with_super(use_node, make); self.use_items.insert(0, super_use_node); } None => {} @@ -591,14 +640,14 @@ impl Module { if !first_path_in_use_tree_str.contains("super") && !first_path_in_use_tree_str.contains("crate") { - let super_path = make::ext::ident_path("super"); + let super_path = make.ident_path("super"); use_tree_str.push(super_path); } } use_tree_paths = Some(use_tree_str); } else if def_in_mod && def_out_sel { - let super_use_node = make_use_stmt_of_node_with_super(use_node); + let super_use_node = make_use_stmt_of_node_with_super(use_node, make); self.use_items.insert(0, super_use_node); } } @@ -610,7 +659,7 @@ impl Module { && let Some(first_path_in_use_tree) = use_tree_paths.first() && first_path_in_use_tree.to_string().contains("super") { - use_tree_paths.insert(0, make::ext::ident_path("super")); + use_tree_paths.insert(0, make.ident_path("super")); } let is_item = matches!( @@ -625,12 +674,12 @@ impl Module { | Definition::TypeAlias(_) ); - if (def_out_sel || !is_item) && use_stmt_not_in_sel { - let use_ = make::use_( - None, - None, - make::use_tree(make::join_paths(use_tree_paths), None, None, false), - ); + if (def_out_sel || !is_item) + && use_stmt_not_in_sel + && let Some(joined) = + use_tree_paths.into_iter().reduce(|acc, p| make.path_concat(acc, p)) + { + let use_ = make.use_([], None, make.use_tree(joined, None, None, false)); self.use_items.insert(0, ast::Item::from(use_)); } } @@ -742,8 +791,7 @@ fn check_def_in_mod_and_out_sel( } fn get_replacements_for_visibility_change( - items: &mut [ast::Item], - is_clone_for_updated: bool, + items: &[ast::Item], ) -> ( Vec<(Option, SyntaxNode)>, Vec<(Option, SyntaxNode)>, @@ -754,9 +802,6 @@ fn get_replacements_for_visibility_change( let mut impls = Vec::new(); for item in items { - if !is_clone_for_updated { - *item = item.clone_for_update(); - } //Use stmts are ignored macro_rules! push_to_replacement { ($it:ident) => { @@ -813,15 +858,6 @@ fn get_use_tree_paths_from_path( Some(use_tree_str) } -fn add_change_vis(vis: Option, node_or_token_opt: Option) { - if vis.is_none() - && let Some(node_or_token) = node_or_token_opt - { - let pub_crate_vis = make::visibility_pub_crate().clone_for_update(); - ted::insert(ted::Position::before(node_or_token), pub_crate_vis.syntax()); - } -} - fn indent_range_before_given_node(node: &SyntaxNode) -> Option { node.siblings_with_tokens(syntax::Direction::Prev) .find(|x| x.kind() == WHITESPACE) diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs index 4a8c9d450c45c..d7f1844b132cf 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs @@ -286,16 +286,6 @@ impl ast::Use { } } -impl ast::Impl { - pub fn get_or_create_assoc_item_list(&self) -> ast::AssocItemList { - if self.assoc_item_list().is_none() { - let assoc_item_list = make::assoc_item_list(None).clone_for_update(); - ted::append_child(self.syntax(), assoc_item_list.syntax()); - } - self.assoc_item_list().unwrap() - } -} - impl ast::RecordExprField { /// This will either replace the initializer, or in the case that this is a shorthand convert /// the initializer into the name ref and insert the expr as the new initializer. From de062d934dafa4563b49670473ce06032021d01c Mon Sep 17 00:00:00 2001 From: Kao-Wei Yeh Date: Mon, 8 Jun 2026 14:41:06 +0800 Subject: [PATCH 08/57] Make assist `inline_type_alias` work on ADT definitions --- .../src/handlers/inline_type_alias.rs | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_type_alias.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_type_alias.rs index e4a1314f5bb29..bb76e2743c377 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_type_alias.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_type_alias.rs @@ -135,7 +135,20 @@ pub(crate) fn inline_type_alias(acc: &mut Assists, ctx: &AssistContext<'_, '_>) PathResolution::SelfType(imp) => { concrete_type = imp.source(ctx.db())?.value.self_ty()?; } - // FIXME: should also work in ADT definitions + PathResolution::Def(hir::ModuleDef::Adt(adt)) => { + let make = SyntaxFactory::without_mappings(); + let src = adt.source(ctx.db())?.value; + let name = src.name()?; + let generic_params = src.generic_param_list(); + let name_ref = make.name_ref(&name.text()); + let segment = match generic_params { + Some(params) => { + make.path_segment_generics(name_ref, params.to_generic_args(&make)) + } + None => make.path_segment(name_ref), + }; + concrete_type = make.ty_path_from_segments([segment], false); + } _ => return None, } @@ -996,6 +1009,68 @@ trait Tr { ); } + #[test] + fn inline_self_type_in_adt_definition() { + check_assist( + inline_type_alias, + r#" +enum Foo { + A(i32), + B(Box), +} +"#, + r#" +enum Foo { + A(i32), + B(Box), +} +"#, + ); + check_assist( + inline_type_alias, + r#" +struct Foo { + a: Box, +} +"#, + r#" +struct Foo { + a: Box, +} +"#, + ); + check_assist( + inline_type_alias, + r#" +struct Foo { + a: T, + b: Box, +} +"#, + r#" +struct Foo { + a: T, + b: Box>, +} +"#, + ); + check_assist( + inline_type_alias, + r#" +union Foo { + a: u32, + b: std::mem::ManuallyDrop>, +} +"#, + r#" +union Foo { + a: u32, + b: std::mem::ManuallyDrop>, +} +"#, + ); + } + #[test] fn inline_types_with_lifetime() { check_assist( From f42e0022f447ba1d3fe684f9a41fd03a028e09a1 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Tue, 9 Jun 2026 17:24:05 +0800 Subject: [PATCH 09/57] feat: support flyimport exclude variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example --- ```json { "rust-analyzer.completion.autoimport.exclude": [ {"path": "xxx_crate::Foo", "type": "variants"}, ] } ``` ```rust enum Foo { Variant1, Variant2, } fn main() { V$0 } ``` **Before this PR** ``` ev TupleV(…) TupleV(u32) ev Variant1 (use Foo::Variant1) Variant1 ev Variant2 (use Foo::Variant2) Variant2 bt u32 u32 ``` **After this PR** ``` ev TupleV(…) TupleV(u32) bt u32 u32 ``` --- .../crates/ide-completion/src/config.rs | 1 + .../crates/ide-completion/src/context.rs | 12 ++++ .../ide-completion/src/tests/expression.rs | 70 +++++++++++++++++++ .../crates/rust-analyzer/src/config.rs | 9 ++- .../docs/book/src/configuration_generated.md | 3 + .../rust-analyzer/editors/code/package.json | 5 +- 6 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/config.rs b/src/tools/rust-analyzer/crates/ide-completion/src/config.rs index ac52c816e52a9..0739084381b3b 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/config.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/config.rs @@ -45,6 +45,7 @@ pub enum AutoImportExclusionType { Always, Methods, SubItems, + Variants, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs index f7bb14391b7de..507ecaff0dca0 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs @@ -868,10 +868,22 @@ impl<'a, 'db> CompletionContext<'a, 'db> { _ => None, }) .collect::>(); + let exclude_variants = exclude_flyimport + .iter() + .flat_map(|it| match it { + (ModuleDef::Adt(hir::Adt::Enum(enum_)), AutoImportExclusionType::Variants) => { + enum_.variants(db) + } + _ => vec![], + }) + .collect::>(); exclude_flyimport .extend(exclude_traits.iter().map(|&t| (t.into(), AutoImportExclusionType::Always))); exclude_flyimport .extend(exclude_subitems.into_iter().map(|it| (it, AutoImportExclusionType::Always))); + exclude_flyimport.extend( + exclude_variants.into_iter().map(|it| (it.into(), AutoImportExclusionType::Always)), + ); // FIXME: This should be part of `CompletionAnalysis` / `expand_and_analyze` let complete_semicolon = if !config.add_semicolon_to_unit { diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs index e49afa66ad738..57f067dc94768 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs @@ -3041,6 +3041,76 @@ fn foo() { ); } +#[test] +fn flyimport_excluded_enum_variants_from_flyimport() { + check_with_config( + CompletionConfig { + exclude_flyimport: vec![( + "ra_test_fixture::Foo".to_owned(), + AutoImportExclusionType::Variants, + )], + ..TEST_CONFIG + }, + r#" +enum Foo { + Variant1, + Variant2, +} +fn foo() { + V$0 +} + "#, + expect![[r#" + ct CONST Unit + en Enum Enum + en Foo Foo + fn foo() fn() + fn function() fn() + ma makro!(…) macro_rules! makro + md module:: + sc STATIC Unit + st Record Record + st Tuple Tuple + st Unit Unit + un Union Union + ev TupleV(…) TupleV(u32) + bt u32 u32 + kw async + kw const + kw crate:: + kw enum + kw extern + kw false + kw fn + kw for + kw if + kw if let + kw impl + kw impl for + kw let + kw letm + kw loop + kw match + kw mod + kw return + kw self:: + kw static + kw struct + kw trait + kw true + kw type + kw union + kw unsafe + kw use + kw while + kw while let + sn macro_rules + sn pd + sn ppd + "#]], + ); +} + #[test] fn excluded_trait_method_is_excluded_from_path_completion() { check_with_config( diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs index 74ba183f97ddb..4ea447333903d 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs @@ -665,6 +665,9 @@ config_data! { /// For modules the type "sub_items" can be used to only exclude the all items in it but not the module /// itself. This does not include items defined in nested modules. /// + /// For enums the type "variants" can be used to only exclude the all variants in it but not the enum + /// itself. + /// /// This setting also inherits `#rust-analyzer.completion.excludeTraits#`. completion_autoimport_exclude: Vec = vec![ AutoImportExclusion::Verbose { path: "core::borrow::Borrow".to_owned(), r#type: AutoImportExclusionType::Methods }, @@ -1944,6 +1947,9 @@ impl Config { AutoImportExclusionType::SubItems => { ide_completion::AutoImportExclusionType::SubItems } + AutoImportExclusionType::Variants => { + ide_completion::AutoImportExclusionType::Variants + } }, ), }) @@ -3004,6 +3010,7 @@ pub enum AutoImportExclusionType { Always, Methods, SubItems, + Variants, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -4135,7 +4142,7 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json }, "type": { "type": "string", - "enum": ["always", "methods", "sub_items"], + "enum": ["always", "methods", "sub_items", "variants"], "enumDescriptions": [ "Do not show this item or its methods (if it is a trait) in auto-import completions.", "Do not show this trait's methods in auto-import completions.", diff --git a/src/tools/rust-analyzer/docs/book/src/configuration_generated.md b/src/tools/rust-analyzer/docs/book/src/configuration_generated.md index bc5ef9e96c02a..76b626b09fb5b 100644 --- a/src/tools/rust-analyzer/docs/book/src/configuration_generated.md +++ b/src/tools/rust-analyzer/docs/book/src/configuration_generated.md @@ -449,6 +449,9 @@ itself. For modules the type "sub_items" can be used to only exclude the all items in it but not the module itself. This does not include items defined in nested modules. +For enums the type "variants" can be used to only exclude the all variants in it but not the enum +itself. + This setting also inherits `#rust-analyzer.completion.excludeTraits#`. diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json index cb328e8866e26..7bc8fda65985e 100644 --- a/src/tools/rust-analyzer/editors/code/package.json +++ b/src/tools/rust-analyzer/editors/code/package.json @@ -1333,7 +1333,7 @@ "title": "Completion", "properties": { "rust-analyzer.completion.autoimport.exclude": { - "markdownDescription": "A list of full paths to items to exclude from auto-importing completions.\n\nTraits in this list won't have their methods suggested in completions unless the trait\nis in scope.\n\nYou can either specify a string path which defaults to type \"always\" or use the more\nverbose form `{ \"path\": \"path::to::item\", type: \"always\" }`.\n\nFor traits the type \"methods\" can be used to only exclude the methods but not the trait\nitself.\n\nFor modules the type \"sub_items\" can be used to only exclude the all items in it but not the module\nitself. This does not include items defined in nested modules.\n\nThis setting also inherits `#rust-analyzer.completion.excludeTraits#`.", + "markdownDescription": "A list of full paths to items to exclude from auto-importing completions.\n\nTraits in this list won't have their methods suggested in completions unless the trait\nis in scope.\n\nYou can either specify a string path which defaults to type \"always\" or use the more\nverbose form `{ \"path\": \"path::to::item\", type: \"always\" }`.\n\nFor traits the type \"methods\" can be used to only exclude the methods but not the trait\nitself.\n\nFor modules the type \"sub_items\" can be used to only exclude the all items in it but not the module\nitself. This does not include items defined in nested modules.\n\nFor enums the type \"variants\" can be used to only exclude the all variants in it but not the enum\nitself.\n\nThis setting also inherits `#rust-analyzer.completion.excludeTraits#`.", "default": [ { "path": "core::borrow::Borrow", @@ -1361,7 +1361,8 @@ "enum": [ "always", "methods", - "sub_items" + "sub_items", + "variants" ], "enumDescriptions": [ "Do not show this item or its methods (if it is a trait) in auto-import completions.", From c965932b218a62670664001da68450a4200b802f Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Tue, 9 Jun 2026 18:44:46 +0800 Subject: [PATCH 10/57] fix: supports inline variable in macro Example --- ```rust macro_rules! m { ($i:expr) => { $i } } fn f() { let xyz = 0; m!(xyz$0); } ``` **Before this PR** Assist not applicable **After this PR** ```rust macro_rules! m { ($i:expr) => { $i } } fn f() { m!(0); } ``` --- .../src/handlers/inline_local_variable.rs | 137 +++++++++--------- 1 file changed, 71 insertions(+), 66 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs index 531adf62ba3f5..d94632b56c958 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs @@ -1,19 +1,20 @@ use either::{Either, for_both}; use hir::{PathResolution, Semantics}; use ide_db::{ - EditionedFileId, RootDatabase, + RootDatabase, defs::Definition, - search::{FileReference, FileReferenceNode, UsageSearchResult}, + search::{FileReference, UsageSearchResult}, }; use syntax::{ - Direction, TextRange, + Direction, T, TextRange, ast::{self, AstNode, AstToken, HasName}, - syntax_editor::{Element, SyntaxEditor}, + syntax_editor::{Element, Position, SyntaxEditor}, }; use crate::{ AssistId, assist_context::{AssistContext, Assists}, + utils::cover_edit_range, }; // Assist: inline_local_variable @@ -33,13 +34,13 @@ use crate::{ // } // ``` pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> { - let file_id = ctx.file_id(); + let source = ctx.source_file().syntax(); let range = ctx.selection_trimmed(); let InlineData { let_stmt, delete_let, references, target } = - if let Some(path_expr) = ctx.find_node_at_offset::() { - inline_usage(&ctx.sema, path_expr, range, file_id) + if let Some(path_expr) = ctx.find_node_at_offset_with_descend::() { + inline_usage(&ctx.sema, path_expr, range) } else if let Some(let_stmt) = ctx.find_node_at_offset() { - inline_let(&ctx.sema, let_stmt, range, file_id) + inline_let(&ctx.sema, let_stmt, range) } else { None }?; @@ -47,47 +48,29 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' either::Either::Left(it) => it.initializer()?, either::Either::Right(it) => it.expr()?, }; - - let wrap_in_parens = references - .into_iter() - .filter_map(|FileReference { range, name, .. }| match name { - FileReferenceNode::NameRef(name) => Some((range, name)), - _ => None, - }) - .map(|(range, name_ref)| { - if range != name_ref.syntax().text_range() { - // Do not rename inside macros - // FIXME: This feels like a bad heuristic for macros - return None; + let needs_parens = |name_ref: &ast::NameRef| { + let usage_node = + name_ref.syntax().ancestors().find(|it| ast::PathExpr::can_cast(it.kind())); + let usage_parent = usage_node.as_ref().and_then(|it| it.parent()); + match (usage_node, usage_parent) { + (Some(usage), Some(parent)) => { + initializer_expr.needs_parens_in_place_of(&parent, &usage) } - let usage_node = - name_ref.syntax().ancestors().find(|it| ast::PathExpr::can_cast(it.kind())); - let usage_parent_option = usage_node.as_ref().and_then(|it| it.parent()); - let usage_parent = match usage_parent_option { - Some(u) => u, - None => return Some((name_ref, false)), - }; - let should_wrap = initializer_expr - .needs_parens_in_place_of(&usage_parent, usage_node.as_ref().unwrap()); - Some((name_ref, should_wrap)) - }) - .collect::>>()?; - - let target = match target { - ast::NameOrNameRef::Name(it) => it.syntax().clone(), - ast::NameOrNameRef::NameRef(it) => it.syntax().clone(), + _ => false, + } }; acc.add( AssistId::refactor_inline("inline_local_variable"), "Inline variable", - target.text_range(), - move |builder| { - let editor = builder.make_editor(&target); + target.range, + |builder| { + let editor = builder.make_editor(source); let make = editor.make(); if delete_let { editor.delete(let_stmt.syntax()); + // Processing let-expr in let-chain if let Some(bin_expr) = let_stmt.syntax().parent().and_then(ast::BinExpr::cast) && let Some(op_token) = bin_expr.op_token() { @@ -99,19 +82,27 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' } } - for (name, should_wrap) in wrap_in_parens { - let replacement = if should_wrap { + for FileReference { range, name, .. } in references { + let Some(name) = name.as_name_ref().cloned() else { continue }; + let replacement = if needs_parens(&name) { make.expr_paren(initializer_expr.clone()).into() } else { initializer_expr.clone() }; - if let Some(record_field) = ast::RecordExprField::for_field_name(&name) { + let place = cover_edit_range(source, range); + if ast::RecordExprField::for_field_name(&name).is_some() { cov_mark::hit!(inline_field_shorthand); - let replacement = make.record_expr_field(name, Some(replacement)); - editor.replace(record_field.syntax(), replacement.syntax()); + editor.insert_all( + Position::after(place.end()), + vec![ + make.token(T![:]).into(), + make.whitespace(" ").into(), + replacement.syntax().clone().into(), + ], + ); } else { - editor.replace(name.syntax(), replacement.syntax()); + editor.replace_all(place, vec![replacement.syntax().clone().into()]); } } builder.add_file_edits(ctx.vfs_file_id(), editor); @@ -122,7 +113,7 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' struct InlineData { let_stmt: Either, delete_let: bool, - target: ast::NameOrNameRef, + target: hir::FileRange, references: Vec, } @@ -130,7 +121,6 @@ fn inline_let( sema: &Semantics<'_, RootDatabase>, let_stmt: Either, range: TextRange, - file_id: EditionedFileId, ) -> Option { let bind_pat = match for_both!(&let_stmt, it => it.pat())? { ast::Pat::IdentPat(pat) => pat, @@ -140,20 +130,18 @@ fn inline_let( cov_mark::hit!(test_not_inline_mut_variable); return None; } - if !bind_pat.syntax().text_range().contains_range(range) { + let target = sema.original_range_opt(bind_pat.name()?.syntax())?; + if !target.range.contains_range(range) { cov_mark::hit!(not_applicable_outside_of_bind_pat); return None; } let local = sema.to_def(&bind_pat)?; - let UsageSearchResult { mut references } = Definition::Local(local).usages(sema).all(); - match references.remove(&file_id) { - Some(references) => Some(InlineData { - let_stmt, - delete_let: true, - target: ast::NameOrNameRef::Name(bind_pat.name()?), - references, - }), + let UsageSearchResult { references } = Definition::Local(local).usages(sema).all(); + let references = references.into_iter().flat_map(|it| it.1).collect::>(); + + match references.first() { + Some(_) => Some(InlineData { let_stmt, delete_let: true, target, references }), None => { cov_mark::hit!(test_not_applicable_if_variable_unused); None @@ -165,11 +153,11 @@ fn inline_usage( sema: &Semantics<'_, RootDatabase>, path_expr: ast::PathExpr, range: TextRange, - file_id: EditionedFileId, ) -> Option { let path = path_expr.path()?; let name = path.as_single_name_ref()?; - if !name.syntax().text_range().contains_range(range) { + let target = sema.original_range_opt(name.syntax())?; + if !target.range.contains_range(range) { cov_mark::hit!(test_not_inline_selection_too_broad); return None; } @@ -193,12 +181,12 @@ fn inline_usage( let let_stmt = AstNode::cast(bind_pat.syntax().parent()?)?; - let UsageSearchResult { mut references } = Definition::Local(local).usages(sema).all(); - let mut references = references.remove(&file_id)?; + let UsageSearchResult { references } = Definition::Local(local).usages(sema).all(); + let mut references = references.into_iter().flat_map(|it| it.1).collect::>(); let delete_let = references.len() == 1; references.retain(|fref| fref.name.as_name_ref() == Some(&name)); - Some(InlineData { let_stmt, delete_let, target: ast::NameOrNameRef::NameRef(name), references }) + Some(InlineData { let_stmt, delete_let, target, references }) } fn remove_whitespace(elem: impl Element, dir: Direction, editor: &SyntaxEditor) { @@ -969,8 +957,8 @@ fn main() { } #[test] - fn not_applicable_on_local_usage_in_macro() { - check_assist_not_applicable( + fn local_usage_in_macro() { + check_assist( inline_local_variable, r#" macro_rules! m { @@ -978,11 +966,19 @@ macro_rules! m { } fn f() { let xyz = 0; - m!(xyz$0); // replacing it would break the macro + m!(xyz$0); // some macros may break, but it's best to support them +} +"#, + r#" +macro_rules! m { + ($i:ident) => { $i } +} +fn f() { + m!(0); // some macros may break, but it's best to support them } "#, ); - check_assist_not_applicable( + check_assist( inline_local_variable, r#" macro_rules! m { @@ -990,10 +986,19 @@ macro_rules! m { } fn f() { let xyz$0 = 0; - m!(xyz); // replacing it would break the macro + m!(xyz); // some macros may break, but it's best to support them +} +"#, + r#" +macro_rules! m { + ($i:ident) => { $i } +} +fn f() { + m!(0); // some macros may break, but it's best to support them } "#, ); + // FIXME: supports let-stmt inside macro case } #[test] From 54577e7f0bcf04dbd17f996d115b104a5554a0fe Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Tue, 9 Jun 2026 19:56:55 +0800 Subject: [PATCH 11/57] fix: do not panic variable def in macro Example --- ```rust macro_rules! i { ($($t:tt)*) => { $($t)* } } fn f() { i!(let xyz = 0;); _ = xyz$0; _ = xyz; } ``` **Before this PR** ``` request handler panicked: can't resolve SyntaxNodePtr { kind: LET_STMT, range: 0..9 } with SOURCE_FILE@0..108 ``` **After this PR** ```rust macro_rules! i { ($($t:tt)*) => { $($t)* } } fn f() { i!(let xyz = 0;); _ = 0; _ = xyz; } ``` --- .../src/handlers/inline_local_variable.rs | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs index d94632b56c958..7fba1ef057bdf 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs @@ -39,7 +39,7 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' let InlineData { let_stmt, delete_let, references, target } = if let Some(path_expr) = ctx.find_node_at_offset_with_descend::() { inline_usage(&ctx.sema, path_expr, range) - } else if let Some(let_stmt) = ctx.find_node_at_offset() { + } else if let Some(let_stmt) = ctx.find_node_at_offset_with_descend() { inline_let(&ctx.sema, let_stmt, range) } else { None @@ -67,18 +67,20 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' |builder| { let editor = builder.make_editor(source); let make = editor.make(); - if delete_let { - editor.delete(let_stmt.syntax()); + if delete_let && let Some(original) = ctx.sema.original_range_opt(let_stmt.syntax()) { + let place = cover_edit_range(source, original.range); + editor.delete_all(place.clone()); // Processing let-expr in let-chain + // FIXME: process let-expr in macro, but this case is very rare if let Some(bin_expr) = let_stmt.syntax().parent().and_then(ast::BinExpr::cast) && let Some(op_token) = bin_expr.op_token() { editor.delete(&op_token); remove_whitespace(op_token, Direction::Prev, &editor); - remove_whitespace(let_stmt.syntax(), Direction::Prev, &editor); + remove_whitespace(place.start(), Direction::Prev, &editor); } else { - remove_whitespace(let_stmt.syntax(), Direction::Next, &editor); + remove_whitespace(place.end(), Direction::Next, &editor); } } @@ -998,7 +1000,75 @@ fn f() { } "#, ); - // FIXME: supports let-stmt inside macro case + } + + #[test] + fn local_def_in_macro() { + check_assist( + inline_local_variable, + r#" +macro_rules! i { + ($($t:tt)*) => { $($t)* } +} +fn f() { + i!(let xyz = 0;); + _ = xyz$0; +} +"#, + r#" +macro_rules! i { + ($($t:tt)*) => { $($t)* } +} +fn f() { + i!(); + _ = 0; +} +"#, + ); + check_assist( + inline_local_variable, + r#" +macro_rules! i { + ($($t:tt)*) => { $($t)* } +} +fn f() { + i!(let xyz = 0;); + _ = xyz$0; + _ = xyz; +} +"#, + r#" +macro_rules! i { + ($($t:tt)*) => { $($t)* } +} +fn f() { + i!(let xyz = 0;); + _ = 0; + _ = xyz; +} +"#, + ); + check_assist( + inline_local_variable, + r#" +macro_rules! i { + ($($t:tt)*) => { $($t)* } +} +fn f() { + i!(let $0xyz = 0;); + _ = xyz; +} +"#, + r#" +macro_rules! i { + ($($t:tt)*) => { $($t)* } +} +fn f() { + i!(); + _ = 0; +} +"#, + ); } #[test] From 2f67dec5817ba1dcc2985f1edaa241069392b06b Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Thu, 11 Jun 2026 03:12:14 +0800 Subject: [PATCH 12/57] internal: git ignore .vim/coc-settings.json --- src/tools/rust-analyzer/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/rust-analyzer/.gitignore b/src/tools/rust-analyzer/.gitignore index 3beabe39bc7a3..32374dac91818 100644 --- a/src/tools/rust-analyzer/.gitignore +++ b/src/tools/rust-analyzer/.gitignore @@ -7,6 +7,7 @@ vendor/ *.log *.iml .vscode/settings.json +.vim/coc-settings.json .DS_Store /out/ /dump.lsif From 87443b694e86e8603270e442637d3df6bc372263 Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Wed, 10 Jun 2026 12:59:54 -0700 Subject: [PATCH 13/57] Remove docs about removed `analysis-bench` command This command as removed in 797185e1b66fb0d6ec1dedf206616890b5e3fef3. --- .../rust-analyzer/docs/book/src/contributing/README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/tools/rust-analyzer/docs/book/src/contributing/README.md b/src/tools/rust-analyzer/docs/book/src/contributing/README.md index bb2b6081ad956..d3e2acd9f7307 100644 --- a/src/tools/rust-analyzer/docs/book/src/contributing/README.md +++ b/src/tools/rust-analyzer/docs/book/src/contributing/README.md @@ -206,13 +206,6 @@ To measure time for from-scratch analysis, use something like this: cargo run --release -p rust-analyzer -- analysis-stats ../chalk/ ``` -For measuring time of incremental analysis, use either of these: - -```bash -cargo run --release -p rust-analyzer -- analysis-bench ../chalk/ --highlight ../chalk/chalk-engine/src/logic.rs -cargo run --release -p rust-analyzer -- analysis-bench ../chalk/ --complete ../chalk/chalk-engine/src/logic.rs:94:0 -``` - Look for `fn benchmark_xxx` tests for a quick way to reproduce performance problems. ## Release Process From 18e250053442cb40df17fa7c30534f2712fcc308 Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Wed, 10 Jun 2026 13:56:11 -0700 Subject: [PATCH 14/57] Create directory for xtask metrics rustc_tests In c0f428d55b425c8ba18039a3687cdcdc47e111d1 this code was adjusted to handle a change in `load_workspace`. The change assumed that the `ra-rustc-test` folder existed in the `std::env::temp_dir`, but this assumption is not always correct. --- .../rust-analyzer/crates/rust-analyzer/src/cli/rustc_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/rustc_tests.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/rustc_tests.rs index 49f28352b6cf6..69405b421eca5 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/rustc_tests.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/rustc_tests.rs @@ -64,6 +64,7 @@ impl Tester { fn new() -> Result { let mut path = AbsPathBuf::assert_utf8(std::env::temp_dir()); path.push("ra-rustc-test"); + std::fs::create_dir_all(&path)?; let tmp_file = path.join("ra-rustc-test.rs"); std::fs::write(&tmp_file, "")?; let cargo_config = CargoConfig { From 51f41825072a6245e3df612f7dfe76d87e1c9b30 Mon Sep 17 00:00:00 2001 From: The rustc-josh-sync Cronjob Bot Date: Thu, 11 Jun 2026 05:40:22 +0000 Subject: [PATCH 15/57] Prepare for merging from rust-lang/rust This updates the rust-version file to 485ec3fbcc12fa14ef6596dabb125ad710499c9e. --- src/tools/rust-analyzer/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/rust-version b/src/tools/rust-analyzer/rust-version index 387bd8edd2196..5bdff8eb64eec 100644 --- a/src/tools/rust-analyzer/rust-version +++ b/src/tools/rust-analyzer/rust-version @@ -1 +1 @@ -029c9e18dd1f4668e1d42bb187c1c263dfe20093 +485ec3fbcc12fa14ef6596dabb125ad710499c9e From 6536d21a9f1952ed7f0465f05321ca5123074010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauren=C8=9Biu=20Nicola?= Date: Thu, 11 Jun 2026 10:57:27 +0300 Subject: [PATCH 16/57] Revert default feature --- src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml b/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml index 72d394d37bfd4..68e0bb6ac8a50 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml @@ -35,7 +35,7 @@ line-index.workspace = true proc-macro-test.path = "./proc-macro-test" [features] -default = ["in-rust-tree"] +default = [] in-rust-tree = [] [lints] From cda17e1f777f2f823e60954ffa203b1171f04b20 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Thu, 11 Jun 2026 16:05:42 +0800 Subject: [PATCH 17/57] Add original file_id check --- .../src/handlers/inline_local_variable.rs | 29 +++++++++++-------- .../crates/ide-assists/src/utils.rs | 9 ++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs index 7fba1ef057bdf..7a82e8b2f3ac8 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_local_variable.rs @@ -1,5 +1,5 @@ use either::{Either, for_both}; -use hir::{PathResolution, Semantics}; +use hir::{EditionedFileId, PathResolution, Semantics}; use ide_db::{ RootDatabase, defs::Definition, @@ -14,7 +14,7 @@ use syntax::{ use crate::{ AssistId, assist_context::{AssistContext, Assists}, - utils::cover_edit_range, + utils::{cover_edit_range, original_range_in}, }; // Assist: inline_local_variable @@ -34,13 +34,14 @@ use crate::{ // } // ``` pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> { + let file_id = ctx.file_id(); let source = ctx.source_file().syntax(); let range = ctx.selection_trimmed(); let InlineData { let_stmt, delete_let, references, target } = if let Some(path_expr) = ctx.find_node_at_offset_with_descend::() { - inline_usage(&ctx.sema, path_expr, range) + inline_usage(&ctx.sema, path_expr, range, file_id) } else if let Some(let_stmt) = ctx.find_node_at_offset_with_descend() { - inline_let(&ctx.sema, let_stmt, range) + inline_let(&ctx.sema, let_stmt, range, file_id) } else { None }?; @@ -63,12 +64,14 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' acc.add( AssistId::refactor_inline("inline_local_variable"), "Inline variable", - target.range, + target, |builder| { let editor = builder.make_editor(source); let make = editor.make(); - if delete_let && let Some(original) = ctx.sema.original_range_opt(let_stmt.syntax()) { - let place = cover_edit_range(source, original.range); + if delete_let + && let Some(original) = original_range_in(file_id, &ctx.sema, let_stmt.syntax()) + { + let place = cover_edit_range(source, original); editor.delete_all(place.clone()); // Processing let-expr in let-chain @@ -115,7 +118,7 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext<'_, ' struct InlineData { let_stmt: Either, delete_let: bool, - target: hir::FileRange, + target: TextRange, references: Vec, } @@ -123,6 +126,7 @@ fn inline_let( sema: &Semantics<'_, RootDatabase>, let_stmt: Either, range: TextRange, + file_id: EditionedFileId, ) -> Option { let bind_pat = match for_both!(&let_stmt, it => it.pat())? { ast::Pat::IdentPat(pat) => pat, @@ -132,8 +136,8 @@ fn inline_let( cov_mark::hit!(test_not_inline_mut_variable); return None; } - let target = sema.original_range_opt(bind_pat.name()?.syntax())?; - if !target.range.contains_range(range) { + let target = original_range_in(file_id, sema, bind_pat.name()?.syntax())?; + if !target.contains_range(range) { cov_mark::hit!(not_applicable_outside_of_bind_pat); return None; } @@ -155,11 +159,12 @@ fn inline_usage( sema: &Semantics<'_, RootDatabase>, path_expr: ast::PathExpr, range: TextRange, + file_id: EditionedFileId, ) -> Option { let path = path_expr.path()?; let name = path.as_single_name_ref()?; - let target = sema.original_range_opt(name.syntax())?; - if !target.range.contains_range(range) { + let target = original_range_in(file_id, sema, name.syntax())?; + if !target.contains_range(range) { cov_mark::hit!(test_not_inline_selection_too_broad); return None; } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs b/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs index 1b6c9a579aba0..086f54ed17f69 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs @@ -1177,6 +1177,15 @@ pub fn is_body_const(sema: &Semantics<'_, RootDatabase>, expr: &ast::Expr) -> bo is_const } +pub(crate) fn original_range_in( + file_id: hir::EditionedFileId, + sema: &Semantics<'_, RootDatabase>, + value: &SyntaxNode, +) -> Option { + let original = sema.original_range_opt(value)?; + (original.file_id == file_id).then_some(original.range) +} + // FIXME: #20460 When hir-ty can analyze the `never` statement at the end of block, remove it pub(crate) fn is_never_block( sema: &Semantics<'_, RootDatabase>, From fe1dce17b002bd800293e64881536cf011184cf4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 11 Jun 2026 13:48:55 +0200 Subject: [PATCH 18/57] Fix destructuring assignments not introducing moves --- .../closure/analysis/expr_use_visitor.rs | 3 +- .../hir-ty/src/tests/closure_captures.rs | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/infer/closure/analysis/expr_use_visitor.rs b/src/tools/rust-analyzer/crates/hir-ty/src/infer/closure/analysis/expr_use_visitor.rs index 34f9508e763f0..d8a8cceee6583 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/infer/closure/analysis/expr_use_visitor.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/infer/closure/analysis/expr_use_visitor.rs @@ -1016,8 +1016,9 @@ impl<'a, 'b, 'db, D: Delegate<'db>> ExprUseVisitor<'a, 'b, 'db, D> { } } Pat::Expr(expr) => { - // Destructuring assignment. this.mutate_expr(expr)?; + // Destructuring assignment moves + this.consume_or_copy(place); } Pat::Or(_) | Pat::Box { .. } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/closure_captures.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/closure_captures.rs index 3942a7ae27afa..bf0e60cf21b80 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/closure_captures.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/closure_captures.rs @@ -617,3 +617,40 @@ fn foo(foo: &Foo) { expect!["102..126;85..88;114..117 ByRef(Immutable) *foo &' Foo"], ); } + +#[test] +fn method_call_field_access_regression() { + check_closure_captures( + r#" +//- minicore:copy, fn +struct NonCopy; + +struct Wrapper { + field: NonCopy, +} + +impl Wrapper { + fn wrapped(&self) -> NonCopy { + NonCopy + } +} + +pub struct Wrapper2 { + field1: NonCopy, + field2: NonCopy, +} + +fn fun(wrapper: Wrapper) { + pub fn update(_: impl FnOnce(&mut T)) { + todo!() + } + + update::(|this| { + this.field1 = wrapper.wrapped(); + this.field2 = wrapper.field; + }); +} + "#, + expect!["319..411;206..213;350..357,391..398 ByValue wrapper Wrapper"], + ); +} From e7be4162b00e1ca95f74cbe76677e5ed246a0179 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 13 May 2026 00:03:33 +0530 Subject: [PATCH 19/57] remove all the ted based and editor based API's from edit_in_place --- .../crates/syntax/src/ast/edit_in_place.rs | 308 +----------------- 1 file changed, 8 insertions(+), 300 deletions(-) diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs index d7f1844b132cf..4a38cb8198e3f 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs @@ -1,308 +1,16 @@ //! Structural editing for ast. - -use std::iter::{empty, once, successors}; - -use parser::T; - use crate::{ - AstNode, AstToken, Direction, - algo::{self, neighbor}, - ast::{self, make, syntax_factory::SyntaxFactory}, - syntax_editor::SyntaxEditor, + AstNode, + ast::{self, make}, ted, }; -use super::HasName; - -impl ast::GenericParamList { - /// Constructs a matching [`ast::GenericArgList`] - pub fn to_generic_args(&self, make: &SyntaxFactory) -> ast::GenericArgList { - let args = self.generic_params().filter_map(|param| match param { - ast::GenericParam::LifetimeParam(it) => { - Some(ast::GenericArg::LifetimeArg(make.lifetime_arg(it.lifetime()?))) - } - ast::GenericParam::TypeParam(it) => { - Some(ast::GenericArg::TypeArg(make.type_arg(make.ty_name(it.name()?)))) - } - ast::GenericParam::ConstParam(it) => { - // Name-only const params get parsed as `TypeArg`s - Some(ast::GenericArg::TypeArg(make.type_arg(make.ty_name(it.name()?)))) - } - }); - - make::generic_arg_list(args) - } -} - -pub trait Removable: AstNode { - fn remove(&self); -} - -impl Removable for ast::UseTree { - fn remove(&self) { - for dir in [Direction::Next, Direction::Prev] { - if let Some(next_use_tree) = neighbor(self, dir) { - let separators = self - .syntax() - .siblings_with_tokens(dir) - .skip(1) - .take_while(|it| it.as_node() != Some(next_use_tree.syntax())); - ted::remove_all_iter(separators); - break; - } - } - ted::remove(self.syntax()); - } -} - -impl ast::UseTree { - /// Editor variant of UseTree remove - fn remove_with_editor(&self, editor: &SyntaxEditor) { - for dir in [Direction::Next, Direction::Prev] { - if let Some(next_use_tree) = neighbor(self, dir) { - let separators = self - .syntax() - .siblings_with_tokens(dir) - .skip(1) - .take_while(|it| it.as_node() != Some(next_use_tree.syntax())); - for separator in separators { - editor.delete(separator); - } - break; - } - } - editor.delete(self.syntax()); - } - - /// Deletes the usetree node represented by the input. Recursively removes parents, including use nodes that become empty. - pub fn remove_recursive(self, editor: &SyntaxEditor) { - let parent = self.syntax().parent(); - - if let Some(u) = parent.clone().and_then(ast::Use::cast) { - u.remove(editor); - } else if let Some(u) = parent.and_then(ast::UseTreeList::cast) { - if u.use_trees().nth(1).is_none() - || u.use_trees().all(|use_tree| { - use_tree.syntax() == self.syntax() || editor.deleted(use_tree.syntax()) - }) - { - u.parent_use_tree().remove_recursive(editor); - return; - } - self.remove_with_editor(editor); - u.remove_unnecessary_braces(editor); - } - } - - pub fn get_or_create_use_tree_list(&self) -> ast::UseTreeList { - match self.use_tree_list() { - Some(it) => it, - None => { - let position = ted::Position::last_child_of(self.syntax()); - let use_tree_list = make::use_tree_list(empty()).clone_for_update(); - let mut elements = Vec::with_capacity(2); - if self.coloncolon_token().is_none() { - elements.push(make::token(T![::]).into()); - } - elements.push(use_tree_list.syntax().clone().into()); - ted::insert_all_raw(position, elements); - use_tree_list - } - } - } - - /// Splits off the given prefix, making it the path component of the use tree, - /// appending the rest of the path to all UseTreeList items. - /// - /// # Examples - /// - /// `prefix$0::suffix` -> `prefix::{suffix}` - /// - /// `prefix$0` -> `prefix::{self}` - /// - /// `prefix$0::*` -> `prefix::{*}` - pub fn split_prefix(&self, prefix: &ast::Path) { - debug_assert_eq!(self.path(), Some(prefix.top_path())); - let path = self.path().unwrap(); - if &path == prefix && self.use_tree_list().is_none() { - if self.star_token().is_some() { - // path$0::* -> * - if let Some(a) = self.coloncolon_token() { - ted::remove(a) - } - ted::remove(prefix.syntax()); - } else { - // path$0 -> self - let self_suffix = - make::path_unqualified(make::path_segment_self()).clone_for_update(); - ted::replace(path.syntax(), self_suffix.syntax()); - } - } else if split_path_prefix(prefix).is_none() { - return; - } - // At this point, prefix path is detached; _self_ use tree has suffix path. - // Next, transform 'suffix' use tree into 'prefix::{suffix}' - let subtree = self.clone_subtree().clone_for_update(); - ted::remove_all_iter(self.syntax().children_with_tokens()); - ted::insert(ted::Position::first_child_of(self.syntax()), prefix.syntax()); - self.get_or_create_use_tree_list().add_use_tree(subtree); - - fn split_path_prefix(prefix: &ast::Path) -> Option<()> { - let parent = prefix.parent_path()?; - let segment = parent.segment()?; - if algo::has_errors(segment.syntax()) { - return None; - } - for p in successors(parent.parent_path(), |it| it.parent_path()) { - p.segment()?; - } - if let Some(a) = prefix.parent_path().and_then(|p| p.coloncolon_token()) { - ted::remove(a) - } - ted::remove(prefix.syntax()); - Some(()) - } - } - - /// Editor variant of `split_prefix` - pub fn split_prefix_with_editor(&self, editor: &SyntaxEditor, prefix: &ast::Path) { - debug_assert_eq!(self.path(), Some(prefix.top_path())); - - let make = editor.make(); - let path = self.path().unwrap(); - let suffix = if path == *prefix && self.use_tree_list().is_none() { - if self.star_token().is_some() { - make.use_tree_glob() - } else { - let self_path = make.path_unqualified(make.path_segment_self()); - make.use_tree(self_path, None, None, false) - } - } else { - let suffix_segments = path.segments().skip(prefix.segments().count()); - let suffix_path = make.path_from_segments(suffix_segments, false); - make.use_tree( - suffix_path, - self.use_tree_list(), - self.rename(), - self.star_token().is_some(), - ) - }; - let use_tree_list = make.use_tree_list(once(suffix)); - let new_use_tree = make.use_tree(prefix.clone(), Some(use_tree_list), None, false); - - editor.replace(self.syntax(), new_use_tree.syntax()); - } - - /// Wraps the use tree in use tree list with no top level path (if it isn't already). - /// - /// # Examples - /// - /// `foo::bar` -> `{foo::bar}` - /// - /// `{foo::bar}` -> `{foo::bar}` - pub fn wrap_in_tree_list(&self) -> Option<()> { - if self.use_tree_list().is_some() - && self.path().is_none() - && self.star_token().is_none() - && self.rename().is_none() - { - return None; - } - let subtree = self.clone_subtree().clone_for_update(); - ted::remove_all_iter(self.syntax().children_with_tokens()); - ted::append_child( - self.syntax(), - make::use_tree_list(once(subtree)).clone_for_update().syntax(), - ); - Some(()) - } -} - -impl ast::UseTreeList { - pub fn add_use_tree(&self, use_tree: ast::UseTree) { - let (position, elements) = match self.use_trees().last() { - Some(last_tree) => ( - ted::Position::after(last_tree.syntax()), - vec![ - make::token(T![,]).into(), - make::tokens::single_space().into(), - use_tree.syntax.into(), - ], - ), - None => { - let position = match self.l_curly_token() { - Some(l_curly) => ted::Position::after(l_curly), - None => ted::Position::last_child_of(self.syntax()), - }; - (position, vec![use_tree.syntax.into()]) - } - }; - ted::insert_all_raw(position, elements); - } -} - -impl ast::Use { - fn remove(&self, editor: &SyntaxEditor) { - let make = editor.make(); - let next_ws = self - .syntax() - .next_sibling_or_token() - .and_then(|it| it.into_token()) - .and_then(ast::Whitespace::cast); - if let Some(next_ws) = next_ws { - let ws_text = next_ws.syntax().text(); - if let Some(rest) = ws_text.strip_prefix('\n') { - let next_use_removed = next_ws - .syntax() - .next_sibling_or_token() - .and_then(|it| it.into_node()) - .and_then(ast::Use::cast) - .and_then(|use_| use_.use_tree()) - .is_some_and(|use_tree| editor.deleted(use_tree.syntax())); - if rest.is_empty() || next_use_removed { - editor.delete(next_ws.syntax()); - } else { - editor.replace(next_ws.syntax(), make.whitespace(rest)); - } - } - } - let prev_ws = self - .syntax() - .prev_sibling_or_token() - .and_then(|it| it.into_token()) - .and_then(ast::Whitespace::cast); - if let Some(prev_ws) = prev_ws { - let ws_text = prev_ws.syntax().text(); - let prev_newline = ws_text.rfind('\n').map(|x| x + 1).unwrap_or(0); - let rest = &ws_text[0..prev_newline]; - if rest.is_empty() { - editor.delete(prev_ws.syntax()); - } else { - editor.replace(prev_ws.syntax(), make.whitespace(rest)); - } - } - - editor.delete(self.syntax()); - } -} - -impl ast::RecordExprField { - /// This will either replace the initializer, or in the case that this is a shorthand convert - /// the initializer into the name ref and insert the expr as the new initializer. - pub fn replace_expr(&self, editor: &SyntaxEditor, expr: ast::Expr) { - if self.name_ref().is_some() { - if let Some(prev) = self.expr() { - editor.replace(prev.syntax(), expr.syntax()); - } - } else if let Some(ast::Expr::PathExpr(path_expr)) = self.expr() - && let Some(path) = path_expr.path() - && let Some(name_ref) = path.as_single_name_ref() - { - // shorthand `{ x }` → expand to `{ x: expr }` - let new_field = editor - .make() - .record_expr_field(editor.make().name_ref(&name_ref.text()), Some(expr)); - editor.replace(self.syntax(), new_field.syntax()); +impl ast::Impl { + pub fn get_or_create_assoc_item_list(&self) -> ast::AssocItemList { + if self.assoc_item_list().is_none() { + let assoc_item_list = make::assoc_item_list(None).clone_for_update(); + ted::append_child(self.syntax(), assoc_item_list.syntax()); } + self.assoc_item_list().unwrap() } } From de273b809f56df77a9da3d84cda1aa56f51aaf26 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 13 May 2026 00:04:22 +0530 Subject: [PATCH 20/57] move all the editor variant of API's from edit-in-place to edit --- .../crates/syntax/src/ast/edit.rs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs index 2e3a4016ee89f..0155df8aa0b86 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs @@ -2,6 +2,7 @@ //! immutable, all function here return a fresh copy of the tree, instead of //! doing an in-place modification. use parser::T; +use rowan::Direction; use std::{ fmt, iter::{self, once}, @@ -12,6 +13,7 @@ use crate::{ AstToken, NodeOrToken, SyntaxElement, SyntaxKind::{ATTR, COMMENT, WHITESPACE}, SyntaxNode, SyntaxToken, + algo::neighbor, ast::{self, AstNode, HasName, make}, syntax_editor::{Position, SyntaxEditor, SyntaxMappingBuilder}, }; @@ -263,6 +265,172 @@ pub fn indent(node: &SyntaxNode, level: IndentLevel) -> SyntaxNode { level.clone_increase_indent(node) } +impl ast::GenericParamList { + /// Constructs a matching [`ast::GenericArgList`] + pub fn to_generic_args(&self, make: &SyntaxFactory) -> ast::GenericArgList { + let args = self.generic_params().filter_map(|param| match param { + ast::GenericParam::LifetimeParam(it) => { + Some(ast::GenericArg::LifetimeArg(make.lifetime_arg(it.lifetime()?))) + } + ast::GenericParam::TypeParam(it) => { + Some(ast::GenericArg::TypeArg(make.type_arg(make.ty_name(it.name()?)))) + } + ast::GenericParam::ConstParam(it) => { + // Name-only const params get parsed as `TypeArg`s + Some(ast::GenericArg::TypeArg(make.type_arg(make.ty_name(it.name()?)))) + } + }); + + make::generic_arg_list(args) + } +} + +impl ast::UseTree { + /// Editor variant of UseTree remove + fn remove_with_editor(&self, editor: &SyntaxEditor) { + for dir in [Direction::Next, Direction::Prev] { + if let Some(next_use_tree) = neighbor(self, dir) { + let separators = self + .syntax() + .siblings_with_tokens(dir) + .skip(1) + .take_while(|it| it.as_node() != Some(next_use_tree.syntax())); + for separator in separators { + editor.delete(separator); + } + break; + } + } + editor.delete(self.syntax()); + } + + /// Deletes the usetree node represented by the input. Recursively removes parents, including use nodes that become empty. + pub fn remove_recursive(self, editor: &SyntaxEditor) { + let parent = self.syntax().parent(); + + if let Some(u) = parent.clone().and_then(ast::Use::cast) { + u.remove(editor); + } else if let Some(u) = parent.and_then(ast::UseTreeList::cast) { + if u.use_trees().nth(1).is_none() + || u.use_trees().all(|use_tree| { + use_tree.syntax() == self.syntax() || editor.deleted(use_tree.syntax()) + }) + { + u.parent_use_tree().remove_recursive(editor); + return; + } + self.remove_with_editor(editor); + u.remove_unnecessary_braces(editor); + } + } + + /// Splits off the given prefix, making it the path component of the use tree, + /// appending the rest of the path to all UseTreeList items. + /// + /// # Examples + /// + /// `prefix$0::suffix` -> `prefix::{suffix}` + /// + /// `prefix$0` -> `prefix::{self}` + /// + /// `prefix$0::*` -> `prefix::{*}```` + pub fn split_prefix_with_editor(&self, editor: &SyntaxEditor, prefix: &ast::Path) { + debug_assert_eq!(self.path(), Some(prefix.top_path())); + + let make = editor.make(); + let path = self.path().unwrap(); + let suffix = if path == *prefix { + if self.use_tree_list().is_some() { + return; + } else if self.star_token().is_some() { + make.use_tree_glob() + } else { + let self_path = make.path_unqualified(make.path_segment_self()); + make.use_tree(self_path, None, self.rename(), false) + } + } else { + let suffix_segments = path.segments().skip(prefix.segments().count()); + let suffix_path = make.path_from_segments(suffix_segments, false); + make.use_tree( + suffix_path, + self.use_tree_list(), + self.rename(), + self.star_token().is_some(), + ) + }; + let use_tree_list = make.use_tree_list(once(suffix)); + let new_use_tree = make.use_tree(prefix.clone(), Some(use_tree_list), None, false); + + editor.replace(self.syntax(), new_use_tree.syntax()); + } +} + +impl ast::Use { + fn remove(&self, editor: &SyntaxEditor) { + let make = editor.make(); + let next_ws = self + .syntax() + .next_sibling_or_token() + .and_then(|it| it.into_token()) + .and_then(ast::Whitespace::cast); + if let Some(next_ws) = next_ws { + let ws_text = next_ws.syntax().text(); + if let Some(rest) = ws_text.strip_prefix('\n') { + let next_use_removed = next_ws + .syntax() + .next_sibling_or_token() + .and_then(|it| it.into_node()) + .and_then(ast::Use::cast) + .and_then(|use_| use_.use_tree()) + .is_some_and(|use_tree| editor.deleted(use_tree.syntax())); + if rest.is_empty() || next_use_removed { + editor.delete(next_ws.syntax()); + } else { + editor.replace(next_ws.syntax(), make.whitespace(rest)); + } + } + } + let prev_ws = self + .syntax() + .prev_sibling_or_token() + .and_then(|it| it.into_token()) + .and_then(ast::Whitespace::cast); + if let Some(prev_ws) = prev_ws { + let ws_text = prev_ws.syntax().text(); + let prev_newline = ws_text.rfind('\n').map(|x| x + 1).unwrap_or(0); + let rest = &ws_text[0..prev_newline]; + if rest.is_empty() { + editor.delete(prev_ws.syntax()); + } else { + editor.replace(prev_ws.syntax(), make.whitespace(rest)); + } + } + + editor.delete(self.syntax()); + } +} + +impl ast::RecordExprField { + /// This will either replace the initializer, or in the case that this is a shorthand convert + /// the initializer into the name ref and insert the expr as the new initializer. + pub fn replace_expr(&self, editor: &SyntaxEditor, expr: ast::Expr) { + if self.name_ref().is_some() { + if let Some(prev) = self.expr() { + editor.replace(prev.syntax(), expr.syntax()); + } + } else if let Some(ast::Expr::PathExpr(path_expr)) = self.expr() + && let Some(path) = path_expr.path() + && let Some(name_ref) = path.as_single_name_ref() + { + // shorthand `{ x }` → expand to `{ x: expr }` + let new_field = editor + .make() + .record_expr_field(editor.make().name_ref(&name_ref.text()), Some(expr)); + editor.replace(self.syntax(), new_field.syntax()); + } + } +} + #[test] fn test_increase_indent() { let arm_list = { From 90fe5efb272393b1ce2cef1cf91637f2b2513036 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 13 May 2026 00:04:51 +0530 Subject: [PATCH 21/57] update test to use editor variant of try_merge_imports --- .../crates/ide-db/src/imports/insert_use/tests.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs index 4fa05c4603461..33db7178d6510 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs @@ -1430,7 +1430,8 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior .find_map(ast::Use::cast) .unwrap(); - let result = try_merge_imports(&use0, &use1, mb); + let editor = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()).0; + let result = try_merge_imports(&editor, &use0, &use1, mb); assert_eq!(result.map(|u| u.to_string()), None); } @@ -1495,7 +1496,8 @@ fn check_merge(ra_fixture0: &str, ra_fixture1: &str, last: &str, mb: MergeBehavi .find_map(ast::Use::cast) .unwrap(); - let result = try_merge_imports(&use0, &use1, mb); + let editor = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()).0; + let result = try_merge_imports(&editor, &use0, &use1, mb); assert_eq!(result.map(|u| u.to_string().trim().to_owned()), Some(last.trim().to_owned())); } @@ -1525,7 +1527,8 @@ fn merge_gated_imports_with_different_values() { .find_map(ast::Use::cast) .unwrap(); - let result = try_merge_imports(&use0, &use1, MergeBehavior::Crate); + let editor = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()).0; + let result = try_merge_imports(&editor, &use0, &use1, MergeBehavior::Crate); assert_eq!(result, None); } From e248498127922d76c4ddaf65efc3004ce0b4e454 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 13 May 2026 00:05:39 +0530 Subject: [PATCH 22/57] update insert_use to use editor variant of merge_import methods --- .../rust-analyzer/crates/ide-db/src/imports/insert_use.rs | 6 +++--- .../crates/ide-db/src/imports/insert_use/tests.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs index c3949f871314f..1fd493fd2a730 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs @@ -17,7 +17,7 @@ use crate::{ RootDatabase, imports::merge_imports::{ MergeBehavior, NormalizationStyle, common_prefix, eq_attrs, eq_visibility, - try_merge_imports, use_tree_cmp, + try_merge_imports, use_tree_cmp, wrap_in_tree_list, }, }; @@ -251,7 +251,7 @@ fn insert_use_with_alias_option_with_editor( let mut use_tree = make.use_tree(path, None, alias, false); if mb == Some(MergeBehavior::One) && use_tree.path().is_some() - && let Some(wrapped) = use_tree.wrap_in_tree_list_with_editor() + && let Some(wrapped) = wrap_in_tree_list(&use_tree, make) { use_tree = wrapped; } @@ -263,7 +263,7 @@ fn insert_use_with_alias_option_with_editor( for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast).filter(filter) { - if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { + if let Some(merged) = try_merge_imports(syntax_editor, &existing_use, &use_item, mb) { syntax_editor.replace(existing_use.syntax(), merged.syntax()); return; } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs index 33db7178d6510..a30d290490210 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs @@ -1430,7 +1430,7 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior .find_map(ast::Use::cast) .unwrap(); - let editor = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()).0; + let (editor, _) = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()); let result = try_merge_imports(&editor, &use0, &use1, mb); assert_eq!(result.map(|u| u.to_string()), None); } @@ -1496,7 +1496,7 @@ fn check_merge(ra_fixture0: &str, ra_fixture1: &str, last: &str, mb: MergeBehavi .find_map(ast::Use::cast) .unwrap(); - let editor = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()).0; + let (editor, _) = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()); let result = try_merge_imports(&editor, &use0, &use1, mb); assert_eq!(result.map(|u| u.to_string().trim().to_owned()), Some(last.trim().to_owned())); } @@ -1527,7 +1527,7 @@ fn merge_gated_imports_with_different_values() { .find_map(ast::Use::cast) .unwrap(); - let editor = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()).0; + let (editor, _) = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()); let result = try_merge_imports(&editor, &use0, &use1, MergeBehavior::Crate); assert_eq!(result, None); } From 138846da0dee94c509fb8d5785271a8b734afaa8 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 13 May 2026 00:06:09 +0530 Subject: [PATCH 23/57] update normalize_import to use editor variant of merge_import methods --- .../crates/ide-assists/src/handlers/normalize_import.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs index f97a3e583f030..07571d74d8b8d 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs @@ -1,5 +1,5 @@ use ide_db::imports::merge_imports::try_normalize_import; -use syntax::{AstNode, ast}; +use syntax::{AstNode, ast, syntax_editor::SyntaxEditor}; use crate::{ AssistId, @@ -25,11 +25,13 @@ pub(crate) fn normalize_import(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - }; let target = use_item.syntax().text_range(); + let (editor, _) = SyntaxEditor::new(use_item.syntax().ancestors().last().unwrap()); let normalized_use_item = - try_normalize_import(&use_item, ctx.config.insert_use.granularity.into())?; + try_normalize_import(&editor, &use_item, ctx.config.insert_use.granularity.into())?; + editor.replace(use_item.syntax(), normalized_use_item.syntax()); acc.add(AssistId::refactor_rewrite("normalize_import"), "Normalize import", target, |builder| { - builder.replace_ast(use_item, normalized_use_item); + builder.add_file_edits(ctx.vfs_file_id(), editor); }) } From 7a7e02589e4081cd816f944c0942e7c3b2cf8104 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 13 May 2026 00:08:13 +0530 Subject: [PATCH 24/57] update merge_import to use editor variant of merge_import methods --- .../ide-assists/src/handlers/merge_imports.rs | 139 ++++++++---------- 1 file changed, 58 insertions(+), 81 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs index dc40a6a640e00..e942be33d94c5 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs @@ -1,10 +1,12 @@ -use either::Either; use ide_db::imports::{ insert_use::{ImportGranularity, InsertUseConfig}, merge_imports::{MergeBehavior, try_merge_imports, try_merge_trees}, }; use syntax::{ - AstNode, SyntaxElement, SyntaxNode, algo::neighbor, ast, match_ast, syntax_editor::Removable, + AstNode, SyntaxElement, + algo::neighbor, + ast, match_ast, + syntax_editor::{Removable, SyntaxEditor}, }; use crate::{ @@ -13,8 +15,6 @@ use crate::{ utils::next_prev, }; -use Edit::*; - // Assist: merge_imports // // Merges neighbor imports with a common prefix. @@ -28,16 +28,17 @@ use Edit::*; // use std::{fmt::Formatter, io}; // ``` pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> { - let (target, edits) = if ctx.has_empty_selection() { + let (target, editor) = if ctx.has_empty_selection() { // Merge a neighbor cov_mark::hit!(merge_with_use_item_neighbors); let tree = ctx.find_node_at_offset::()?.top_use_tree(); let target = tree.syntax().text_range(); let use_item = tree.syntax().parent().and_then(ast::Use::cast)?; - let mut neighbor = next_prev().find_map(|dir| neighbor(&use_item, dir)).into_iter(); - let edits = use_item.try_merge_from(&mut neighbor, &ctx.config.insert_use); - (target, edits?) + let neighbor = next_prev().find_map(|dir| neighbor(&use_item, dir))?; + let (editor, _) = SyntaxEditor::new(use_item.syntax().parent()?.ancestors().last()?); + merge_uses(use_item, vec![neighbor], &ctx.config.insert_use, &editor)?; + (target, editor) } else { // Merge selected let selection_range = ctx.selection_trimmed(); @@ -50,104 +51,80 @@ pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> O }); let first_selected = selected_nodes.next()?; - let edits = match_ast! { + let (editor, _) = SyntaxEditor::new(parent_node.ancestors().last().unwrap()); + match_ast! { match first_selected { ast::Use(use_item) => { cov_mark::hit!(merge_with_selected_use_item_neighbors); - use_item.try_merge_from(&mut selected_nodes.filter_map(ast::Use::cast), &ctx.config.insert_use) + merge_uses( + use_item, + selected_nodes.filter_map(ast::Use::cast).collect(), + &ctx.config.insert_use, + &editor, + )?; }, ast::UseTree(use_tree) => { cov_mark::hit!(merge_with_selected_use_tree_neighbors); - use_tree.try_merge_from(&mut selected_nodes.filter_map(ast::UseTree::cast), &ctx.config.insert_use) + merge_use_trees( + use_tree, + selected_nodes.filter_map(ast::UseTree::cast).collect(), + &editor, + )?; }, _ => return None, } - }; - (selection_range, edits?) - }; - - let parent_node = match ctx.covering_element() { - SyntaxElement::Node(n) => n, - SyntaxElement::Token(t) => t.parent()?, + } + (selection_range, editor) }; acc.add(AssistId::refactor_rewrite("merge_imports"), "Merge imports", target, |builder| { - let editor = builder.make_editor(&parent_node); - - for edit in edits { - match edit { - Remove(it) => { - let node = it.as_ref(); - if let Some(left) = node.left() { - left.remove(&editor); - } else if let Some(right) = node.right() { - right.remove(&editor); - } - } - Replace(old, new) => { - editor.replace(old, &new); - } - } - } builder.add_file_edits(ctx.vfs_file_id(), editor); }) } -trait Merge: AstNode + Clone { - fn try_merge_from( - self, - items: &mut dyn Iterator, - cfg: &InsertUseConfig, - ) -> Option> { - let mut edits = Vec::new(); - let mut merged = self.clone(); - for item in items { - merged = merged.try_merge(&item, cfg)?; - edits.push(Edit::Remove(item.into_either())); - } - if !edits.is_empty() { - edits.push(Edit::replace(self, merged)); - Some(edits) - } else { - None - } +fn merge_uses( + first: ast::Use, + rest: Vec, + cfg: &InsertUseConfig, + editor: &SyntaxEditor, +) -> Option<()> { + if rest.is_empty() { + return None; } - fn try_merge(&self, other: &Self, cfg: &InsertUseConfig) -> Option; - fn into_either(self) -> Either; -} -impl Merge for ast::Use { - fn try_merge(&self, other: &Self, cfg: &InsertUseConfig) -> Option { - let mb = match cfg.granularity { - ImportGranularity::One => MergeBehavior::One, - _ => MergeBehavior::Crate, - }; - try_merge_imports(self, other, mb) + let mb = match cfg.granularity { + ImportGranularity::One => MergeBehavior::One, + _ => MergeBehavior::Crate, + }; + let mut merged = first.clone(); + for item in &rest { + merged = try_merge_imports(editor, &merged, item, mb)?; } - fn into_either(self) -> Either { - Either::Left(self) + for item in rest { + item.remove(editor); } + editor.replace(first.syntax(), merged.syntax()); + Some(()) } -impl Merge for ast::UseTree { - fn try_merge(&self, other: &Self, _: &InsertUseConfig) -> Option { - try_merge_trees(self, other, MergeBehavior::Crate) - } - fn into_either(self) -> Either { - Either::Right(self) +fn merge_use_trees( + first: ast::UseTree, + rest: Vec, + editor: &SyntaxEditor, +) -> Option<()> { + if rest.is_empty() { + return None; } -} - -#[derive(Debug)] -enum Edit { - Remove(Either), - Replace(SyntaxNode, SyntaxNode), -} -impl Edit { - fn replace(old: impl AstNode, new: impl AstNode) -> Self { - Edit::Replace(old.syntax().clone(), new.syntax().clone()) + let mut merged = first.clone(); + for item in &rest { + merged = try_merge_trees(editor, &merged, item, MergeBehavior::Crate)?; + } + for item in rest { + item.remove(editor); } + editor.replace(first.syntax(), merged.syntax()); + Some(()) } #[cfg(test)] From bf226d8cfd2fec2da50e37f52e30a8bbe047f0ae Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Mon, 25 May 2026 23:02:14 +0530 Subject: [PATCH 25/57] migrate merge_import to syntax_editor --- .../ide-db/src/imports/merge_imports.rs | 673 +++++++++++------- 1 file changed, 405 insertions(+), 268 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs index bbd351a4cc14c..95e654df174f5 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs @@ -4,12 +4,12 @@ use std::cmp::Ordering; use itertools::{EitherOrBoth, Itertools}; use parser::T; use syntax::{ - Direction, SyntaxElement, ToSmolStr, algo, + ToSmolStr, ast::{ - self, AstNode, HasAttrs, HasName, HasVisibility, PathSegmentKind, edit_in_place::Removable, - make, + self, AstNode, HasAttrs, HasName, HasVisibility, PathSegmentKind, + syntax_factory::SyntaxFactory, }, - ted::{self, Position}, + syntax_editor::{Position, SyntaxEditor}, }; use crate::syntax_helpers::node_ext::vis_eq; @@ -39,8 +39,8 @@ impl MergeBehavior { } /// Merge `rhs` into `lhs` keeping both intact. -/// Returned AST is mutable. pub fn try_merge_imports( + editor: &SyntaxEditor, lhs: &ast::Use, rhs: &ast::Use, merge_behavior: MergeBehavior, @@ -53,39 +53,41 @@ pub fn try_merge_imports( return None; } - let lhs = lhs.clone_subtree().clone_for_update(); - let rhs = rhs.clone_subtree().clone_for_update(); + let make = editor.make(); let lhs_tree = lhs.use_tree()?; let rhs_tree = rhs.use_tree()?; - try_merge_trees_mut(&lhs_tree, &rhs_tree, merge_behavior)?; + let merged_tree = try_merge_trees_with_factory(lhs_tree, rhs_tree, merge_behavior, make)?; // Ignore `None` result because normalization should not affect the merge result. - try_normalize_use_tree_mut(&lhs_tree, merge_behavior.into()); + let use_tree = try_normalize_use_tree(merged_tree.clone(), merge_behavior.into(), make) + .unwrap_or(merged_tree); - Some(lhs) + make_use_with_tree(lhs, use_tree) } /// Merge `rhs` into `lhs` keeping both intact. -/// Returned AST is mutable. pub fn try_merge_trees( + editor: &SyntaxEditor, lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior, ) -> Option { - let lhs = lhs.clone_subtree().clone_for_update(); - let rhs = rhs.clone_subtree().clone_for_update(); - try_merge_trees_mut(&lhs, &rhs, merge)?; + let make = editor.make(); + let merged = try_merge_trees_with_factory(lhs.clone(), rhs.clone(), merge, make)?; // Ignore `None` result because normalization should not affect the merge result. - try_normalize_use_tree_mut(&lhs, merge.into()); - - Some(lhs) + Some(try_normalize_use_tree(merged.clone(), merge.into(), make).unwrap_or(merged)) } -fn try_merge_trees_mut(lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior) -> Option<()> { +fn try_merge_trees_with_factory( + mut lhs: ast::UseTree, + mut rhs: ast::UseTree, + merge: MergeBehavior, + make: &SyntaxFactory, +) -> Option { if merge == MergeBehavior::One { - lhs.wrap_in_tree_list(); - rhs.wrap_in_tree_list(); + lhs = wrap_in_tree_list(&lhs, make).unwrap_or(lhs); + rhs = wrap_in_tree_list(&rhs, make).unwrap_or(rhs); } else { let lhs_path = lhs.path()?; let rhs_path = rhs.path()?; @@ -98,48 +100,59 @@ fn try_merge_trees_mut(lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehav { // we can't merge if the renames are different (`A as a` and `A as b`), // and we can safely return here - let lhs_name = lhs.rename().and_then(|lhs_name| lhs_name.name()); - let rhs_name = rhs.rename().and_then(|rhs_name| rhs_name.name()); + let lhs_name = lhs + .rename() + .and_then(|lhs_name| lhs_name.name()) + .map(|name| name.text().to_string()); + let rhs_name = rhs + .rename() + .and_then(|rhs_name| rhs_name.name()) + .map(|name| name.text().to_string()); if lhs_name != rhs_name { return None; } - ted::replace(lhs.syntax(), rhs.syntax()); - // we can safely return here, in this case `recursive_merge` doesn't do anything - return Some(()); + return Some(rhs); } else { - lhs.split_prefix(&lhs_prefix); - rhs.split_prefix(&rhs_prefix); + lhs = split_prefix(&lhs, &lhs_prefix, make)?; + rhs = split_prefix(&rhs, &rhs_prefix, make)?; } } - recursive_merge(lhs, rhs, merge) + recursive_merge(lhs, rhs, merge, make) } /// Recursively merges rhs to lhs #[must_use] -fn recursive_merge(lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior) -> Option<()> { +fn recursive_merge( + lhs: ast::UseTree, + rhs: ast::UseTree, + merge: MergeBehavior, + make: &SyntaxFactory, +) -> Option { let mut use_trees: Vec = lhs - .use_tree_list() - .into_iter() - .flat_map(|list| list.use_trees()) - // We use Option here to early return from this function(this is not the - // same as a `filter` op). + .use_tree_list()? + .use_trees() + // We use Option here to early return from this function. This is not the + // same as a `filter` op. .map(|tree| merge.is_tree_allowed(&tree).then_some(tree)) .collect::>()?; + // Sorts the use trees similar to rustfmt's algorithm for ordering imports // (see `use_tree_cmp` doc). use_trees.sort_unstable_by(use_tree_cmp); - for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) { + + for rhs_t in rhs.use_tree_list()?.use_trees() { if !merge.is_tree_allowed(&rhs_t) { return None; } match use_trees.binary_search_by(|lhs_t| use_tree_cmp_bin_search(lhs_t, &rhs_t)) { Ok(idx) => { - let lhs_t = &mut use_trees[idx]; + let mut lhs_t = use_trees[idx].clone(); let lhs_path = lhs_t.path()?; let rhs_path = rhs_t.path()?; let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; + if lhs_prefix == lhs_path && rhs_prefix == rhs_path { let tree_is_self = |tree: &ast::UseTree| { tree.path().as_ref().map(path_is_self).unwrap_or(false) @@ -157,20 +170,20 @@ fn recursive_merge(lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior) }; if lhs_t.rename().and_then(|x| x.underscore_token()).is_some() { - ted::replace(lhs_t.syntax(), rhs_t.syntax()); - *lhs_t = rhs_t; + use_trees[idx] = rhs_t; continue; } - match (tree_contains_self(lhs_t), tree_contains_self(&rhs_t)) { + match (tree_contains_self(&lhs_t), tree_contains_self(&rhs_t)) { (Some(true), None) => { - remove_subtree_if_only_self(lhs_t); + lhs_t = remove_subtree_if_only_self(lhs_t, make)?; + use_trees[idx] = lhs_t; continue; } (None, Some(true)) => { - ted::replace(lhs_t.syntax(), rhs_t.syntax()); - *lhs_t = rhs_t; - remove_subtree_if_only_self(lhs_t); + lhs_t = rhs_t; + lhs_t = remove_subtree_if_only_self(lhs_t, make)?; + use_trees[idx] = lhs_t; continue; } _ => (), @@ -180,9 +193,11 @@ fn recursive_merge(lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior) continue; } } - lhs_t.split_prefix(&lhs_prefix); - rhs_t.split_prefix(&rhs_prefix); - recursive_merge(lhs_t, &rhs_t, merge)?; + + lhs_t = split_prefix(&lhs_t, &lhs_prefix, make)?; + let rhs_t = split_prefix(&rhs_t, &rhs_prefix, make)?; + lhs_t = recursive_merge(lhs_t, rhs_t, merge, make)?; + use_trees[idx] = lhs_t; } Err(_) if merge == MergeBehavior::Module @@ -192,15 +207,12 @@ fn recursive_merge(lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior) return None; } Err(insert_idx) => { - use_trees.insert(insert_idx, rhs_t.clone()); - // We simply add the use tree to the end of tree list. Ordering of use trees - // and imports is done by the `try_normalize_*` functions. The sorted `use_trees` - // vec is only used for binary search. - lhs.get_or_create_use_tree_list().add_use_tree(rhs_t); + use_trees.insert(insert_idx, rhs_t); } } } - Some(()) + + with_use_tree_list(&lhs, use_trees, make) } /// Style to follow when normalizing a use tree. @@ -250,241 +262,217 @@ impl From for NormalizationStyle { /// - `foo::{bar::Qux, bar::{self}}` -> `{foo::bar::{self, Qux}}` /// - `foo::bar::{self}` -> `{foo::bar}` /// - `foo::bar` -> `{foo::bar}` -pub fn try_normalize_import(use_item: &ast::Use, style: NormalizationStyle) -> Option { - let use_item = use_item.clone_subtree().clone_for_update(); - try_normalize_use_tree_mut(&use_item.use_tree()?, style)?; - Some(use_item) +pub fn try_normalize_import( + editor: &SyntaxEditor, + use_item: &ast::Use, + style: NormalizationStyle, +) -> Option { + let make = editor.make(); + let use_tree = try_normalize_use_tree(use_item.use_tree()?, style, make)?; + + make_use_with_tree(use_item, use_tree) } -fn try_normalize_use_tree_mut(use_tree: &ast::UseTree, style: NormalizationStyle) -> Option<()> { +fn try_normalize_use_tree( + use_tree: ast::UseTree, + style: NormalizationStyle, + make: &SyntaxFactory, +) -> Option { if style == NormalizationStyle::One { + let mut use_tree = use_tree; let mut modified = false; - modified |= use_tree.wrap_in_tree_list().is_some(); - modified |= recursive_normalize(use_tree, style).is_some(); - if !modified { - // Either the use tree was already normalized or its semantically empty. - return None; + if let Some(wrapped) = wrap_in_tree_list(&use_tree, make) { + use_tree = wrapped; + modified = true; } - } else { - recursive_normalize(use_tree, NormalizationStyle::Default)?; + if let Some(normalized) = recursive_normalize(use_tree.clone(), style, make) { + use_tree = normalized; + modified = true; + } + return modified.then_some(use_tree); } - Some(()) + + recursive_normalize(use_tree, NormalizationStyle::Default, make) } /// Recursively normalizes a use tree and its subtrees (if any). -fn recursive_normalize(use_tree: &ast::UseTree, style: NormalizationStyle) -> Option<()> { +fn recursive_normalize( + use_tree: ast::UseTree, + style: NormalizationStyle, + make: &SyntaxFactory, +) -> Option { let use_tree_list = use_tree.use_tree_list()?; - let merge_subtree_into_parent_tree = |single_subtree: &ast::UseTree| { - let subtree_is_only_self = single_subtree.path().as_ref().is_some_and(path_is_self); - - let merged_path = match (use_tree.path(), single_subtree.path()) { - // If the subtree is `{self}` then we cannot merge: `use - // foo::bar::{self}` is not equivalent to `use foo::bar`. See - // https://github.com/rust-lang/rust-analyzer/pull/17140#issuecomment-2079189725. - _ if subtree_is_only_self => None, - - (None, None) => None, - (Some(outer), None) => Some(outer), - (None, Some(inner)) => Some(inner), - (Some(outer), Some(inner)) => Some(make::path_concat(outer, inner).clone_for_update()), - }; - - if merged_path.is_some() - || single_subtree.use_tree_list().is_some() - || single_subtree.star_token().is_some() + let mut subtrees = use_tree_list.use_trees().collect::>(); + if subtrees.len() == 1 { + if style == NormalizationStyle::One { + let subtree = subtrees.pop()?; + let normalized = recursive_normalize(subtree, NormalizationStyle::Default, make)?; + return with_use_tree_list(&use_tree, vec![normalized], make); + } + + let merged = merge_single_subtree_into_parent_tree(use_tree, make)?; + return Some(recursive_normalize(merged.clone(), style, make).unwrap_or(merged)); + } + + let mut modified = false; + let mut new_use_tree_list = Vec::new(); + for subtree in subtrees { + if one_style_tree_list(&subtree).is_some() { + let mut elements = Vec::new(); + flatten_one_style_tree(subtree, &mut elements, &mut modified, make); + new_use_tree_list.extend(elements); + modified = true; + } else if let Some(normalized) = + recursive_normalize(subtree.clone(), NormalizationStyle::Default, make) { - ted::remove_all_iter(use_tree.syntax().children_with_tokens()); - if let Some(path) = merged_path { - ted::insert_raw(Position::first_child_of(use_tree.syntax()), path.syntax()); - if single_subtree.use_tree_list().is_some() || single_subtree.star_token().is_some() + new_use_tree_list.push(normalized); + modified = true; + } else { + new_use_tree_list.push(subtree); + } + } + + let mut use_tree = + if modified { with_use_tree_list(&use_tree, new_use_tree_list, make)? } else { use_tree }; + + let mut use_tree_list = use_tree.use_tree_list()?.use_trees().collect::>(); + let mut anchor_idx = 0; + let mut merged_any = false; + while anchor_idx < use_tree_list.len() { + let mut candidate_idx = anchor_idx + 1; + while candidate_idx < use_tree_list.len() { + if let Some(mut merged) = try_merge_trees_with_factory( + use_tree_list[anchor_idx].clone(), + use_tree_list[candidate_idx].clone(), + MergeBehavior::Crate, + make, + ) { + if let Some(normalized) = + recursive_normalize(merged.clone(), NormalizationStyle::Default, make) { - ted::insert_raw( - Position::last_child_of(use_tree.syntax()), - make::token(T![::]), - ); + merged = normalized; } + + use_tree_list[anchor_idx] = merged; + use_tree_list.remove(candidate_idx); + merged_any = true; + } else { + candidate_idx += 1; } - if let Some(inner_use_tree_list) = single_subtree.use_tree_list() { - ted::insert_raw( - Position::last_child_of(use_tree.syntax()), - inner_use_tree_list.syntax(), - ); - } else if single_subtree.star_token().is_some() { - ted::insert_raw(Position::last_child_of(use_tree.syntax()), make::token(T![*])); - } else if let Some(rename) = single_subtree.rename() { - ted::insert_raw( - Position::last_child_of(use_tree.syntax()), - make::tokens::single_space(), - ); - ted::insert_raw(Position::last_child_of(use_tree.syntax()), rename.syntax()); - } - Some(()) - } else { - // Bail on semantically empty use trees. - None } - }; - let one_style_tree_list = |subtree: &ast::UseTree| match ( - subtree.path().is_none() && subtree.star_token().is_none() && subtree.rename().is_none(), - subtree.use_tree_list(), - ) { - (true, tree_list) => tree_list, - _ => None, - }; - let add_element_to_list = |elem: SyntaxElement, elements: &mut Vec| { - if !elements.is_empty() { - elements.push(make::token(T![,]).into()); - elements.push(make::tokens::single_space().into()); + + anchor_idx += 1; + } + if merged_any { + use_tree = with_use_tree_list(&use_tree, use_tree_list, make)?; + modified = true; + } + + if style != NormalizationStyle::One { + let subtrees = use_tree.use_tree_list()?.use_trees().collect::>(); + if subtrees.len() == 1 + && let Some(merged) = merge_single_subtree_into_parent_tree(use_tree.clone(), make) + { + use_tree = merged; + modified = true; } - elements.push(elem); - }; - if let Some((single_subtree,)) = use_tree_list.use_trees().collect_tuple() { - if style == NormalizationStyle::One { - // Only normalize descendant subtrees if the normalization style is "one". - recursive_normalize(&single_subtree, NormalizationStyle::Default)?; - } else { - // Otherwise, merge the single subtree into it's parent (if possible) - // and then normalize the result. - merge_subtree_into_parent_tree(&single_subtree)?; - recursive_normalize(use_tree, style); + } + + if let Some(list) = use_tree.use_tree_list() { + let mut use_tree_list = list.use_trees().collect::>(); + if use_tree_list + .windows(2) + .any(|trees| use_tree_cmp_bin_search(&trees[0], &trees[1]).is_gt()) + { + use_tree_list.sort_unstable_by(use_tree_cmp_bin_search); + use_tree = with_use_tree_list(&use_tree, use_tree_list, make)?; + modified = true; } - } else { - // Tracks whether any changes have been made to the use tree. - let mut modified = false; + } - // Recursively un-nests (if necessary) and then normalizes each subtree in the tree list. - for subtree in use_tree_list.use_trees() { - if let Some(one_tree_list) = one_style_tree_list(&subtree) { - let mut elements = Vec::new(); - let mut one_tree_list_iter = one_tree_list.use_trees(); - let mut prev_skipped = Vec::new(); - loop { - let mut prev_skipped_iter = prev_skipped.into_iter(); - let mut curr_skipped = Vec::new(); - - while let Some(sub_sub_tree) = - one_tree_list_iter.next().or(prev_skipped_iter.next()) - { - if let Some(sub_one_tree_list) = one_style_tree_list(&sub_sub_tree) { - curr_skipped.extend(sub_one_tree_list.use_trees()); - } else { - modified |= - recursive_normalize(&sub_sub_tree, NormalizationStyle::Default) - .is_some(); - add_element_to_list( - sub_sub_tree.syntax().clone().into(), - &mut elements, - ); - } - } + modified.then_some(use_tree) +} - if curr_skipped.is_empty() { - // Un-nesting is complete. - break; - } - prev_skipped = curr_skipped; - } +fn flatten_one_style_tree( + subtree: ast::UseTree, + elements: &mut Vec, + modified: &mut bool, + make: &SyntaxFactory, +) { + let Some(one_tree_list) = one_style_tree_list(&subtree) else { return }; + let mut one_tree_list_iter = one_tree_list.use_trees(); + let mut prev_skipped = Vec::new(); + loop { + let mut prev_skipped_iter = prev_skipped.into_iter(); + let mut curr_skipped = Vec::new(); - // Either removes the subtree (if its semantically empty) or replaces it with - // the un-nested elements. - if elements.is_empty() { - subtree.remove(); - } else { - ted::replace_with_many(subtree.syntax(), elements); - } - // Silence unused assignment warning on `modified`. - let _ = modified; - modified = true; + while let Some(sub_sub_tree) = + one_tree_list_iter.next().or_else(|| prev_skipped_iter.next()) + { + if let Some(sub_one_tree_list) = one_style_tree_list(&sub_sub_tree) { + curr_skipped.extend(sub_one_tree_list.use_trees()); + } else if let Some(normalized) = + recursive_normalize(sub_sub_tree.clone(), NormalizationStyle::Default, make) + { + *modified = true; + elements.push(normalized); } else { - modified |= recursive_normalize(&subtree, NormalizationStyle::Default).is_some(); + elements.push(sub_sub_tree); } } - // Merge all merge-able subtrees. - let mut tree_list_iter = use_tree_list.use_trees(); - let mut anchor = tree_list_iter.next()?; - let mut prev_skipped = Vec::new(); - loop { - let mut has_merged = false; - let mut prev_skipped_iter = prev_skipped.into_iter(); - let mut next_anchor = None; - let mut curr_skipped = Vec::new(); - - while let Some(candidate) = tree_list_iter.next().or(prev_skipped_iter.next()) { - let result = try_merge_trees_mut(&anchor, &candidate, MergeBehavior::Crate); - if result.is_some() { - // Remove merged subtree. - candidate.remove(); - has_merged = true; - } else if next_anchor.is_none() { - next_anchor = Some(candidate); - } else { - curr_skipped.push(candidate); - } - } - - if has_merged { - // Normalize the merge result. - recursive_normalize(&anchor, NormalizationStyle::Default); - modified = true; - } + if curr_skipped.is_empty() { + break; + } + prev_skipped = curr_skipped; + } +} - let (Some(next_anchor), true) = (next_anchor, !curr_skipped.is_empty()) else { - // Merging is complete. - break; - }; +fn merge_single_subtree_into_parent_tree( + use_tree: ast::UseTree, + make: &SyntaxFactory, +) -> Option { + let single_subtree = get_single_subtree(&use_tree)?; + let subtree_is_only_self = single_subtree.path().as_ref().is_some_and(path_is_self); + + let merged_path = match (use_tree.path(), single_subtree.path()) { + _ if subtree_is_only_self => None, + (None, None) => None, + (Some(outer), None) => Some(outer), + (None, Some(inner)) => Some(inner), + (Some(outer), Some(inner)) => Some(make.path_concat(outer, inner)), + }; - // Try to merge the remaining subtrees in the next iteration. - anchor = next_anchor; - prev_skipped = curr_skipped; - } + let list = single_subtree.use_tree_list(); + let list_is_none = list.is_none(); + let star = single_subtree.star_token().is_some(); + if merged_path.is_some() || list.is_some() || star { + let rename = (!star && list_is_none).then(|| single_subtree.rename()).flatten(); + make_use_tree_from_parts(make, merged_path, list, rename, star) + } else { + None + } +} - let mut subtrees: Vec<_> = use_tree_list.use_trees().collect(); - // Merge the remaining subtree into its parent, if its only one and - // the normalization style is not "one". - if subtrees.len() == 1 && style != NormalizationStyle::One { - modified |= merge_subtree_into_parent_tree(&subtrees[0]).is_some(); - } - // Order the remaining subtrees (if necessary). - if subtrees.len() > 1 { - let mut did_sort = false; - subtrees.sort_unstable_by(|a, b| { - let order = use_tree_cmp_bin_search(a, b); - if !did_sort && order == Ordering::Less { - did_sort = true; - } - order - }); - if did_sort { - let start = use_tree_list - .l_curly_token() - .and_then(|l_curly| algo::non_trivia_sibling(l_curly.into(), Direction::Next)) - .filter(|it| it.kind() != T!['}']); - let end = use_tree_list - .r_curly_token() - .and_then(|r_curly| algo::non_trivia_sibling(r_curly.into(), Direction::Prev)) - .filter(|it| it.kind() != T!['{']); - if let Some((start, end)) = start.zip(end) { - // Attempt to insert elements while preserving preceding and trailing trivia. - let mut elements = Vec::new(); - for subtree in subtrees { - add_element_to_list(subtree.syntax().clone().into(), &mut elements); - } - ted::replace_all(start..=end, elements); - } else { - let new_use_tree_list = make::use_tree_list(subtrees).clone_for_update(); - ted::replace(use_tree_list.syntax(), new_use_tree_list.syntax()); - } - modified = true; - } - } +fn one_style_tree_list(subtree: &ast::UseTree) -> Option { + (subtree.path().is_none() && subtree.star_token().is_none() && subtree.rename().is_none()) + .then(|| subtree.use_tree_list()) + .flatten() +} - if !modified { - // Either the use tree was already normalized or its semantically empty. - return None; +fn remove_subtree_if_only_self( + use_tree: ast::UseTree, + make: &SyntaxFactory, +) -> Option { + let Some(single_subtree) = get_single_subtree(&use_tree) else { + return Some(use_tree); + }; + match (use_tree.path(), single_subtree.path()) { + (Some(path), Some(inner)) if path_is_self(&inner) => { + Some(make.use_tree(path, None, use_tree.rename(), false)) } + _ => Some(use_tree), } - Some(()) } /// Traverses both paths until they differ, returning the common prefix of both. @@ -513,10 +501,9 @@ pub fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast fn use_tree_cmp_bin_search(lhs: &ast::UseTree, rhs: &ast::UseTree) -> Ordering { let lhs_is_simple_path = lhs.is_simple_path() && lhs.rename().is_none(); let rhs_is_simple_path = rhs.is_simple_path() && rhs.rename().is_none(); - match ( - lhs.path().as_ref().and_then(ast::Path::first_segment), - rhs.path().as_ref().and_then(ast::Path::first_segment), - ) { + let lhs_segment = lhs.path().and_then(|path| path.first_segment()); + let rhs_segment = rhs.path().and_then(|path| path.first_segment()); + match (lhs_segment, rhs_segment) { (None, None) => match (lhs_is_simple_path, rhs_is_simple_path) { (true, true) => Ordering::Equal, (true, false) => Ordering::Less, @@ -701,14 +688,164 @@ fn get_single_subtree(use_tree: &ast::UseTree) -> Option { .map(|(single_subtree,)| single_subtree) } -fn remove_subtree_if_only_self(use_tree: &ast::UseTree) { - let Some(single_subtree) = get_single_subtree(use_tree) else { return }; - match (use_tree.path(), single_subtree.path()) { - (Some(_), Some(inner)) if path_is_self(&inner) => { - ted::remove_all_iter(single_subtree.syntax().children_with_tokens()); - } - _ => (), +fn make_use_with_tree(original: &ast::Use, use_tree: ast::UseTree) -> Option { + let (editor, use_item) = SyntaxEditor::with_ast_node(original); + let original_tree = use_item.use_tree()?; + editor.replace(original_tree.syntax(), use_tree.syntax()); + let edit = editor.finish(); + ast::Use::cast(edit.new_root().clone()) +} + +fn make_use_tree_list( + make: &SyntaxFactory, + use_trees: Vec, + style_source: Option<&ast::UseTreeList>, +) -> Option { + let use_tree_list = make.use_tree_list(use_trees); + let Some(style_source) = style_source else { + return Some(use_tree_list); + }; + + let source_l_curly = style_source.l_curly_token()?; + let source_r_curly = style_source.r_curly_token()?; + + let leading_ws = source_l_curly.next_token().filter(|token| token.kind().is_trivia()); + + let trailing_ws = source_r_curly.prev_token().filter(|token| token.kind().is_trivia()); + + let source_trailing_token = trailing_ws + .as_ref() + .and_then(|token| token.prev_token()) + .or_else(|| source_r_curly.prev_token()); + + let source_has_trailing_comma = + source_trailing_token.is_some_and(|token| token.kind() == T![,]); + + let (editor, use_tree_list) = SyntaxEditor::with_ast_node(&use_tree_list); + let make = editor.make(); + + if let Some(leading_ws) = leading_ws { + editor.insert( + Position::after(use_tree_list.l_curly_token()?), + make.whitespace(leading_ws.text()), + ); } + + let r_curly = use_tree_list.r_curly_token()?; + + let generated_has_trailing_comma = r_curly + .prev_token() + .and_then(|token| if token.kind().is_trivia() { token.prev_token() } else { Some(token) }) + .is_some_and(|token| token.kind() == T![,]); + + let mut trailing = Vec::new(); + + if source_has_trailing_comma + && !generated_has_trailing_comma + && use_tree_list.use_trees().next().is_some() + { + trailing.push(make.token(T![,]).into()); + } + + if let Some(trailing_ws) = trailing_ws { + trailing.push(make.whitespace(trailing_ws.text()).into()); + } + + if !trailing.is_empty() { + editor.insert_all(Position::before(r_curly), trailing); + } + + let edit = editor.finish(); + ast::UseTreeList::cast(edit.new_root().clone()) +} + +fn make_use_tree_from_list(make: &SyntaxFactory, list: ast::UseTreeList) -> Option { + let placeholder = make.use_tree_glob(); + let (editor, use_tree) = SyntaxEditor::with_ast_node(&placeholder); + let first_child = use_tree.syntax().first_child_or_token()?; + let last_child = use_tree.syntax().last_child_or_token()?; + editor.replace_all(first_child..=last_child, vec![list.syntax().clone().into()]); + let edit = editor.finish(); + ast::UseTree::cast(edit.new_root().clone()) +} + +fn make_use_tree_from_parts( + make: &SyntaxFactory, + path: Option, + list: Option, + rename: Option, + star: bool, +) -> Option { + match (path, list, star) { + (Some(path), list, star) => Some(make.use_tree(path, list, rename, star)), + (None, Some(list), false) if rename.is_none() => make_use_tree_from_list(make, list), + (None, None, true) if rename.is_none() => Some(make.use_tree_glob()), + (None, None, false) if rename.is_none() => None, + _ => None, + } +} + +fn with_use_tree_list( + use_tree: &ast::UseTree, + use_trees: Vec, + make: &SyntaxFactory, +) -> Option { + let list = make_use_tree_list(make, use_trees, use_tree.use_tree_list().as_ref())?; + make_use_tree_from_parts( + make, + use_tree.path(), + Some(list), + use_tree.rename(), + use_tree.star_token().is_some(), + ) +} + +pub(crate) fn wrap_in_tree_list( + use_tree: &ast::UseTree, + make: &SyntaxFactory, +) -> Option { + if use_tree.path().is_none() + && use_tree.use_tree_list().is_some() + && use_tree.rename().is_none() + && use_tree.star_token().is_none() + { + return None; + } + + let list = make_use_tree_list(make, vec![use_tree.clone()], None)?; + make_use_tree_from_list(make, list) +} + +fn split_prefix( + use_tree: &ast::UseTree, + prefix: &ast::Path, + make: &SyntaxFactory, +) -> Option { + let path = use_tree.path()?; + if path == *prefix && use_tree.use_tree_list().is_some() { + return Some(use_tree.clone()); + } + + let suffix = if path == *prefix { + if use_tree.star_token().is_some() { + make.use_tree_glob() + } else { + let self_path = make.path_unqualified(make.path_segment_self()); + make.use_tree(self_path, None, use_tree.rename(), false) + } + } else { + let suffix_segments = path.segments().skip(prefix.segments().count()); + let suffix_path = make.path_from_segments(suffix_segments, false); + make.use_tree( + suffix_path, + use_tree.use_tree_list(), + use_tree.rename(), + use_tree.star_token().is_some(), + ) + }; + + let list = make_use_tree_list(make, vec![suffix], None)?; + Some(make.use_tree(prefix.clone(), Some(list), None, false)) } // Taken from rustfmt From 368efc5755c25d792b35478124ce7577389d37fb Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 12 Jun 2026 15:06:34 +0530 Subject: [PATCH 26/57] Remove edit in place --- src/tools/rust-analyzer/crates/syntax/src/ast.rs | 1 - .../crates/syntax/src/ast/edit_in_place.rs | 16 ---------------- 2 files changed, 17 deletions(-) delete mode 100644 src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast.rs b/src/tools/rust-analyzer/crates/syntax/src/ast.rs index dc592a43727b4..d8c7e1583031d 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast.rs @@ -1,7 +1,6 @@ //! Abstract Syntax Tree, layered on top of untyped `SyntaxNode`s pub mod edit; -pub mod edit_in_place; mod expr_ext; mod generated; pub mod make; diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs deleted file mode 100644 index 4a38cb8198e3f..0000000000000 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/edit_in_place.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Structural editing for ast. -use crate::{ - AstNode, - ast::{self, make}, - ted, -}; - -impl ast::Impl { - pub fn get_or_create_assoc_item_list(&self) -> ast::AssocItemList { - if self.assoc_item_list().is_none() { - let assoc_item_list = make::assoc_item_list(None).clone_for_update(); - ted::append_child(self.syntax(), assoc_item_list.syntax()); - } - self.assoc_item_list().unwrap() - } -} From 39b7e48eba51f7af03734a885e29bd36abd33ead Mon Sep 17 00:00:00 2001 From: "chad@gazdeb" Date: Fri, 12 Jun 2026 06:58:46 -0400 Subject: [PATCH 27/57] Remove unnecessary feature flags from tests The `bindings_after_at`, `if_let_guard`, and `trait_upcasting` features are stable, so the tests for these features no longer require enabling them. --- .../ide-assists/src/handlers/destructure_tuple_binding.rs | 4 ---- .../ide-assists/src/handlers/replace_if_let_with_match.rs | 6 ------ .../crates/ide-diagnostics/src/handlers/invalid_cast.rs | 1 - 3 files changed, 11 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/destructure_tuple_binding.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/destructure_tuple_binding.rs index 2755962362677..a272351e134cc 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/destructure_tuple_binding.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/destructure_tuple_binding.rs @@ -1236,15 +1236,11 @@ fn main { fn destructure_in_sub_pattern() { check_sub_pattern_assist( r#" -#![feature(bindings_after_at)] - fn main() { let $0t = (1,2); } "#, r#" -#![feature(bindings_after_at)] - fn main() { let t @ ($0_0, _1) = (1,2); } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs index ff2d0544b2a18..6959988db7f03 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs @@ -712,13 +712,11 @@ impl VariantData { check_assist( replace_if_let_with_match, r#" -#![feature(if_let_guard)] fn main() { if $0let true = true && let Some(1) = None {} else { other() } } "#, r#" -#![feature(if_let_guard)] fn main() { match true { true if let Some(1) = None => {} @@ -731,7 +729,6 @@ fn main() { check_assist( replace_if_let_with_match, r#" -#![feature(if_let_guard)] fn main() { if true { $0if let ParenExpr(expr) = cond @@ -758,7 +755,6 @@ fn main() { } "#, r#" -#![feature(if_let_guard)] fn main() { if true { match cond { @@ -816,13 +812,11 @@ fn main() { check_assist( replace_if_let_with_match, r#" -#![feature(if_let_guard)] fn main() { if $0let true = true && let Some(1) = None {} } "#, r#" -#![feature(if_let_guard)] fn main() { match true { true if let Some(1) = None => {} diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs index bd8fa69e28707..e1c2053289b96 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs @@ -1025,7 +1025,6 @@ fn _slice(bar: &[i32]) -> bool { check_diagnostics( r#" //- minicore: coerce_unsized, dispatch_from_dyn -#![feature(trait_upcasting)] trait Foo {} trait Bar: Foo {} From 3bfb4225f1b69a0ce9260b952aa1f913f074fcb7 Mon Sep 17 00:00:00 2001 From: dfireBird Date: Mon, 8 Jun 2026 18:05:13 +0530 Subject: [PATCH 28/57] fix: use package id as argument to `--package` if package is not unique --- .../rust-analyzer/crates/rust-analyzer/src/target_spec.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs index 81d9786809b72..8e57d2df5296a 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs @@ -319,7 +319,11 @@ impl CargoTargetSpec { pub(crate) fn push_to(self, buf: &mut Vec, kind: &RunnableKind) { buf.push("--package".to_owned()); - buf.push(self.package); + if self.package.contains(":") { + buf.push(self.package_id.to_string()); + } else { + buf.push(self.package); + } // Can't mix --doc with other target flags if let RunnableKind::DocTest { .. } = kind { From 40dc6719e051b1bedf2d80b8d96ba6e4ccb5def9 Mon Sep 17 00:00:00 2001 From: Lynn Gabbay Date: Fri, 12 Jun 2026 21:36:31 +0700 Subject: [PATCH 29/57] Don't count C-variadic `...` as a parameter for fn pointers When lowering a C-variadic function-pointer type such as `unsafe extern "C" fn(u8, ...) -> i32`, the `...` slot was being included in the lowered parameter list as a real input. The arity check then required the variadic slot to be supplied, producing a spurious E0107 ("wrong number of arguments") at the call site even though rustc accepts the code. The function-declaration lowering path already filters the variadic param out; this applies the same filtering to the fn-pointer path. Fixes rust-lang/rust-analyzer#22573 --- .../crates/hir-def/src/expr_store/lower.rs | 1 + .../src/handlers/mismatched_arg_count.rs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs index 3094e08a53c3c..bbc3f5333a14f 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs @@ -671,6 +671,7 @@ impl<'db> ExprCollector<'db> { } pl.params() + .filter(|it| it.dotdotdot_token().is_none()) .map(|it| { let type_ref = self.lower_type_ref_opt(it.ty(), impl_trait_lower_fn); let name = match it.pat() { diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs index f6293e35d0c37..d1c3d1c5df096 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs @@ -354,6 +354,30 @@ fn f() { ) } + #[test] + fn varargs_fn_pointer() { + check_diagnostics( + r#" +struct Funcs { + f: unsafe extern "C" fn(u8, u8, ...) -> i32, + g: unsafe extern "C" fn(...) -> i32, +} + +fn f(funcs: Funcs) { + unsafe { + (funcs.f)(0, 1); + (funcs.f)(0, 1, 2); + (funcs.f)(0); + //^ error: expected 2 arguments, found 1 + (funcs.g)(); + (funcs.g)(0); + (funcs.g)(0, 1); + } +} + "#, + ) + } + #[test] fn arg_count_lambda() { check_diagnostics( From a52265f79332b2c31259cef5a90655a7b256904a Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 13 Jun 2026 09:53:36 +0200 Subject: [PATCH 30/57] perf: defer initial workspace flycheck until cache priming completes On startup the initial `cargo check` flycheck was launched in the same `became_quiescent` block as cache priming, so the two ran concurrently and fought for CPU. Neither is single-threaded: cache priming fans crate work across worker threads and `cargo check` spawns up to one rustc job per core, so running them together oversubscribes the cores. The effect is felt particularly on macOS, where rust-analyzer's priming threads run at `QOS_CLASS_UTILITY` (`ThreadIntent::Worker`) while the `cargo check` subprocess runs at the default, higher QoS -- so the scheduler starves priming (the work that makes the editor usable) in favour of the check. Move the initial workspace flycheck out of `became_quiescent` into the `PrimeCachesProgress::End` handler via a `pending_initial_flycheck` flag, so the check only starts once priming has finished; fall back to firing it immediately when cache priming is disabled. The flycheck still runs (cargo check begins right after indexing); diagnostics are deferred a beat, not lost. The impact depends on how much the startup `cargo check` has to compile: - Warm check (steady-state reopen): contention is negligible, so this is effectively neutral -- and harmless, deferring a fast check costs nothing. - Cold check (first open after clone / `git pull` / branch switch / toolchain change, where startup already hurts): large. Time-to-ready (cache priming complete) measured on macOS with a cold check, build scripts and proc-macros disabled to isolate the priming/check contention: rust-analyzer 74.8s -> 7.9s (~9.5x) slint 103.1s -> 26.6s (~3.9x) Flycheck slow-tests pass. Disclosure: prototyped with assistance from Claude (Anthropic); changes reviewed and owned by the committer. --- .../crates/rust-analyzer/src/main_loop.rs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs index edf3da5e6c07a..628e708c6a83d 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs @@ -387,6 +387,16 @@ impl GlobalState { if cancelled { self.prime_caches_queue .request_op("restart after cancellation".to_owned(), ()); + } else if self.config.check_on_save(None) + && self.config.flycheck_workspace(None) + && !self.fetch_build_data_queue.op_requested() + { + // Priming finished; now run the deferred initial workspace flycheck + // (kept off the critical path so `cargo check` doesn't contend with + // cache priming for CPU). + self.flycheck + .iter() + .for_each(|flycheck| flycheck.restart_workspace(None)); } if let Some((message, fraction, title)) = last_report.take() { self.report_progress( @@ -482,13 +492,6 @@ impl GlobalState { if self.is_quiescent() { let became_quiescent = !was_quiescent; if became_quiescent { - if self.config.check_on_save(None) - && self.config.flycheck_workspace(None) - && !self.fetch_build_data_queue.op_requested() - { - // Project has loaded properly, kick off initial flycheck - self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); - } // delay initial cache priming until proc macros are loaded, or we will load up a bunch of garbage into salsa let proc_macros_loaded = self.config.prefill_caches() && (!self.config.expand_proc_macros() @@ -496,6 +499,19 @@ impl GlobalState { if proc_macros_loaded { self.prime_caches_queue.request_op("became quiescent".to_owned(), ()); } + if self.config.check_on_save(None) + && self.config.flycheck_workspace(None) + && !self.fetch_build_data_queue.op_requested() + { + if !self.config.prefill_caches() { + self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); + } else if proc_macros_loaded + && !self.prime_caches_queue.op_in_progress() + && !self.prime_caches_queue.op_requested() + { + self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); + } + } } let client_refresh = became_quiescent || state_changed; From cfdecac6a17e6d6f32a3bb3d1a860b4b1539848b Mon Sep 17 00:00:00 2001 From: eihqnh Date: Sun, 14 Jun 2026 00:59:37 +0800 Subject: [PATCH 31/57] fix: offer inline macro in macro call and proc macro --- .../ide-assists/src/handlers/inline_macro.rs | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_macro.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_macro.rs index 002791b88df64..f4ac75d2290dd 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_macro.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/inline_macro.rs @@ -1,8 +1,11 @@ use hir::db::ExpandDatabase; use ide_db::syntax_helpers::prettify_macro_expansion; -use syntax::ast::{self, AstNode, edit::AstNodeEdit}; +use syntax::ast::{self, AstNode, edit::IndentLevel}; -use crate::{AssistContext, AssistId, Assists}; +use crate::{ + AssistContext, AssistId, Assists, + utils::{cover_edit_range, original_range_in}, +}; // Assist: inline_macro // @@ -36,24 +39,40 @@ use crate::{AssistContext, AssistId, Assists}; // } // ``` pub(crate) fn inline_macro(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> { - let unexpanded = ctx.find_node_at_offset::()?; - let macro_call = ctx.sema.to_def(&unexpanded)?; + let source = ctx.source_file().syntax(); + let sel = ctx.selection_trimmed(); + let (macro_call, text_range) = ctx + .sema + .find_nodes_at_offset_with_descend::(source, ctx.offset()) + .find_map(|macro_call_node| { + let macro_call = ctx.sema.to_def(¯o_call_node)?; + let original_range = + original_range_in(ctx.file_id(), &ctx.sema, macro_call_node.syntax())?; + original_range.contains_range(sel).then_some((macro_call, original_range)) + })?; let target_crate_id = ctx.sema.file_to_module_def(ctx.vfs_file_id())?.krate(ctx.db()).into(); - let text_range = unexpanded.syntax().text_range(); acc.add( AssistId::refactor_inline("inline_macro"), "Inline macro".to_owned(), text_range, |builder| { - let editor = builder.make_editor(unexpanded.syntax()); let expanded = ctx.sema.parse_or_expand(macro_call.into()); let span_map = ctx.sema.db.expansion_span_map(macro_call); // Don't call `prettify_macro_expansion()` outside the actual assist action; it does some heavy rowan tree manipulation, // which can be very costly for big macros when it is done *even without the assist being invoked*. let expanded = prettify_macro_expansion(ctx.db(), expanded, span_map, target_crate_id); - let expanded = ast::edit::indent(&expanded, unexpanded.indent_level()); - editor.replace(unexpanded.syntax(), expanded); + + // macro_call is from an expansion, use source position for indent + let indent = source + .token_at_offset(text_range.start()) + .right_biased() + .map_or_else(IndentLevel::zero, |t| IndentLevel::from_token(&t)); + let expanded = ast::edit::indent(&expanded, indent); + + let editor = builder.make_editor(source); + let place = cover_edit_range(source, text_range); + editor.replace_all(place, vec![expanded.into()]); builder.add_file_edits(ctx.vfs_file_id(), editor); }, ) @@ -103,6 +122,7 @@ macro_rules! num { "# }; } + #[test] fn inline_macro_target() { check_assist_target( @@ -376,6 +396,37 @@ fn bar() { fn bar() { a::Foo; } +"#, + ); + } + + #[test] + fn inline_macro_in_macro() { + check_assist( + inline_macro, + r#" +macro_rules! foo { () => { 2 }; } +macro_rules! m { ($($tt:tt)*) => { $($tt)* }; } +fn f() { m! { $0foo!(); } } +"#, + r#" +macro_rules! foo { () => { 2 }; } +macro_rules! m { ($($tt:tt)*) => { $($tt)* }; } +fn f() { m! { 2; } } +"#, + ); + check_assist( + inline_macro, + r#" +//- proc_macros: identity +macro_rules! foo { () => { 2 }; } +#[proc_macros::identity] +fn f() { $0foo!(); } +"#, + r#" +macro_rules! foo { () => { 2 }; } +#[proc_macros::identity] +fn f() { 2; } "#, ); } From 95894614e7f7846b13935f990b1556af4bf2cf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=BA=E4=B8=8D=E5=8F=AB=E5=8C=96=E5=92=95=E9=BE=99?= <1801943622@qq.com> Date: Sun, 14 Jun 2026 11:23:56 +0800 Subject: [PATCH 32/57] Use ASCII lowercase for file extensions --- .../crates/project-model/src/build_dependencies.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/project-model/src/build_dependencies.rs b/src/tools/rust-analyzer/crates/project-model/src/build_dependencies.rs index e44af96f36435..352f41a82636a 100644 --- a/src/tools/rust-analyzer/crates/project-model/src/build_dependencies.rs +++ b/src/tools/rust-analyzer/crates/project-model/src/build_dependencies.rs @@ -560,7 +560,7 @@ impl WorkspaceBuildScripts { // FIXME: Find a better way to know if it is a dylib. fn is_dylib(path: &Utf8Path) -> bool { - match path.extension().map(|e| e.to_owned().to_lowercase()) { + match path.extension().map(|e| e.to_ascii_lowercase()) { None => false, Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"), } From d5755ebf8359ded61e26107c5c8112583174b095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Sun, 14 Jun 2026 15:14:53 +0200 Subject: [PATCH 33/57] Add funding links --- src/tools/rust-analyzer/.github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/tools/rust-analyzer/.github/FUNDING.yml diff --git a/src/tools/rust-analyzer/.github/FUNDING.yml b/src/tools/rust-analyzer/.github/FUNDING.yml new file mode 100644 index 0000000000000..1d270e78949f8 --- /dev/null +++ b/src/tools/rust-analyzer/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: rustfoundation +custom: ["rust-lang.org/funding"] From dc39c189e68eec68a76a504fd102ac7d52209881 Mon Sep 17 00:00:00 2001 From: dfireBird Date: Sat, 13 Jun 2026 02:52:47 +0530 Subject: [PATCH 34/57] fix: prefer bench command when target is bench to avoid cargo run --- .../rust-analyzer/crates/rust-analyzer/src/target_spec.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs index 81d9786809b72..31f798d0c9548 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/target_spec.rs @@ -153,6 +153,9 @@ impl CargoTargetSpec { Some(CargoTargetSpec { target_kind: TargetKind::Test, .. }) => { config.test_command } + Some(CargoTargetSpec { target_kind: TargetKind::Bench, .. }) => { + config.bench_command + } _ => "run".to_owned(), }; cargo_args.push(subcommand); @@ -225,6 +228,9 @@ impl CargoTargetSpec { Some(CargoTargetSpec { target_kind: TargetKind::Test, .. }) => { (config.test_override_command, None) } + Some(CargoTargetSpec { target_kind: TargetKind::Bench, .. }) => { + (config.bench_override_command, None) + } _ => (None, None), }, }; From 527465693f233d4c72ca30d2a8835f9e9d9389c7 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Tue, 16 Jun 2026 15:41:47 +0100 Subject: [PATCH 35/57] fix: MIR eval mixed bit and byte sizes MIR eval was using .bit_width() and then Size::from_bytes(), leading to sizes that were out by a factor of 8. This led to crashes like "expected int of size 64, but got size 8" during const eval. I initially noticed this with u64 (a type of known size, so .bit_size() returned a non-None value) but I've fixed all the bit/byte mismatches in MIR evaluation. Also added a unit test with a minimal repro of the case I originally saw. Fixes rust-lang/rust-analyzer#22593 AI disclosure: Partially written with Codex and GPT 5.5. --- .../crates/hir-ty/src/consteval/tests.rs | 13 +++++ .../crates/hir-ty/src/mir/eval.rs | 52 +++++++++++-------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/consteval/tests.rs b/src/tools/rust-analyzer/crates/hir-ty/src/consteval/tests.rs index 5421b97db2386..0e51594a78d06 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/consteval/tests.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/consteval/tests.rs @@ -2633,6 +2633,19 @@ fn const_generic_subst_fn() { ); } +#[test] +fn const_generic_fixed_width() { + check_number( + r#" + const fn m() -> u64 { + N + } + const GOAL: u64 = m::<0>(); + "#, + 0, + ); +} + #[test] fn layout_of_type_with_associated_type_field_defined_inside_body() { check_number( diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/mir/eval.rs b/src/tools/rust-analyzer/crates/hir-ty/src/mir/eval.rs index eb0e4c60200f2..c69febd019672 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/mir/eval.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/mir/eval.rs @@ -1935,25 +1935,37 @@ impl<'a, 'db: 'a> Evaluator<'a, 'db> { Ok(Interval::new(addr, 4)) } TyKind::Int(int_ty) => { - let size = int_ty.bit_width().unwrap_or(self.ptr_size() as u64); - let value = valtree.inner().to_leaf().to_int(Size::from_bytes(size)); - let addr = self.heap_allocate(size as usize, size as usize)?; - self.write_memory(addr, &value.to_le_bytes()[..size as usize])?; - Ok(Interval::new(addr, size as usize)) + let size = int_ty + .bit_width() + .map(Size::from_bits) + .unwrap_or_else(|| Size::from_bytes(self.ptr_size() as u64)); + let bytes = size.bytes_usize(); + + let value = valtree.inner().to_leaf().to_int(size); + let addr = self.heap_allocate(bytes, bytes)?; + self.write_memory(addr, &value.to_le_bytes()[..bytes])?; + Ok(Interval::new(addr, bytes)) } TyKind::Uint(uint_ty) => { - let size = uint_ty.bit_width().unwrap_or(self.ptr_size() as u64); - let value = valtree.inner().to_leaf().to_uint(Size::from_bytes(size)); - let addr = self.heap_allocate(size as usize, size as usize)?; - self.write_memory(addr, &value.to_le_bytes()[..size as usize])?; - Ok(Interval::new(addr, size as usize)) + let size = uint_ty + .bit_width() + .map(Size::from_bits) + .unwrap_or_else(|| Size::from_bytes(self.ptr_size() as u64)); + let bytes = size.bytes_usize(); + + let value = valtree.inner().to_leaf().to_uint(size); + let addr = self.heap_allocate(bytes, bytes)?; + self.write_memory(addr, &value.to_le_bytes()[..bytes])?; + Ok(Interval::new(addr, bytes)) } TyKind::Float(float_ty) => { - let size = float_ty.bit_width(); - let value = valtree.inner().to_leaf().to_uint(Size::from_bytes(size)); - let addr = self.heap_allocate(size as usize, size as usize)?; - self.write_memory(addr, &value.to_le_bytes()[..size as usize])?; - Ok(Interval::new(addr, size as usize)) + let size = Size::from_bits(float_ty.bit_width()); + let bytes = size.bytes_usize(); + + let value = valtree.inner().to_leaf().to_uint(size); + let addr = self.heap_allocate(bytes, bytes)?; + self.write_memory(addr, &value.to_le_bytes()[..bytes])?; + Ok(Interval::new(addr, bytes)) } TyKind::RawPtr(..) => { let size = self.ptr_size(); @@ -1995,15 +2007,13 @@ impl<'a, 'db: 'a> Evaluator<'a, 'db> { _ => not_supported!("unsupported const"), }) .collect::>>()?; + let item_size = item_layout.size.bytes_usize(); let items_addr = self.heap_allocate( - items.len() * (item_layout.size.bits() as usize), - item_layout.align.bits_usize(), + items.len() * item_size, + item_layout.align.bytes() as usize, )?; for (i, item) in items.iter().enumerate() { - self.copy_from_interval( - items_addr.offset(i * (item_layout.size.bits() as usize)), - *item, - )?; + self.copy_from_interval(items_addr.offset(i * item_size), *item)?; } let ref_addr = self.heap_allocate(self.ptr_size() * 2, self.ptr_size())?; self.write_memory(ref_addr, &items_addr.to_bytes())?; From d7a883109c40693febe6c0bf26e2718393676054 Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Fri, 12 Jun 2026 19:02:48 +0200 Subject: [PATCH 36/57] internal: decide not to use `'db` in `NavigationTarget.docs` --- src/tools/rust-analyzer/crates/ide/src/navigation_target.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs b/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs index b8c14dc09f9a2..24031429db88f 100644 --- a/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs +++ b/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs @@ -50,7 +50,6 @@ pub struct NavigationTarget { pub kind: Option, pub container_name: Option, pub description: Option, - // FIXME: Use the database lifetime here. pub docs: Option>, /// In addition to a `name` field, a `NavigationTarget` may also be aliased /// In such cases we want a `NavigationTarget` to be accessible by its alias From 07fa0eadcf07d76dabc8e156b2902f48165f4d64 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Wed, 17 Jun 2026 12:01:29 +0100 Subject: [PATCH 37/57] fix: Crash on static constants in array length positions We were missing a match case in the solver interner: `SolverDefId::StaticId` wasn't covered, so we hit the `unimplemented!()` base case. This occurred when coercing array lengths with statics, see the new test case. Fixes rust-lang/rust-analyzer#22600 AI disclosure: Written with help by Codex and GPT 5.5. --- .../crates/hir-ty/src/next_solver/interner.rs | 3 +++ .../crates/hir-ty/src/tests/regression/new_solver.rs | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs b/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs index b466fe0f1810b..9875a915dcdda 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs @@ -1128,6 +1128,9 @@ impl<'db> Interner for DbInterner<'db> { SolverDefId::ConstId(def_id) => { AliasTermKind::UnevaluatedConst { def_id: GeneralConstIdWrapper(def_id.into()) } } + SolverDefId::StaticId(def_id) => { + AliasTermKind::UnevaluatedConst { def_id: GeneralConstIdWrapper(def_id.into()) } + } SolverDefId::AnonConstId(def_id) => { AliasTermKind::UnevaluatedConst { def_id: GeneralConstIdWrapper(def_id.into()) } } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs index c77b20f4b54c1..17127b2771d2e 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs @@ -394,6 +394,16 @@ fn main() { ); } +#[test] +fn static_as_array_len_does_not_panic() { + check_no_mismatches( + r#" +static S: usize = 8; +const A: [u8; S] = [0; 8]; + "#, + ); +} + #[test] fn another_20654_case() { check_no_mismatches( From f6653135737dddff55b3db1fa90fef774dfe0d3b Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Sat, 18 Apr 2026 23:55:32 +0200 Subject: [PATCH 38/57] misc improvements --- .../rust-analyzer/crates/hir-ty/src/next_solver/infer/mod.rs | 2 +- src/tools/rust-analyzer/crates/ide-completion/src/render.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/infer/mod.rs b/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/infer/mod.rs index 6b6dd549b34e0..2c2f7dbf67301 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/infer/mod.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/infer/mod.rs @@ -843,7 +843,7 @@ impl<'db> InferCtxt<'db> { GenericArgs::for_item(self.interner, def_id, |_index, kind, _| self.var_for_def(kind, span)) } - /// Like `fresh_args_for_item()`, but first uses the args from `first`. + /// Like [`Self::fresh_args_for_item`], but first uses the args from `first`. pub fn fill_rest_fresh_args( &self, span: Span, diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs index e48847c983b4e..7cb1eaa061f3c 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs @@ -2372,7 +2372,7 @@ impl S { } fn foo(s: S) { s.$0 } "#, - CompletionItemKind::SymbolKind(SymbolKind::Method), + SymbolKind::Method, expect![[r#" [ CompletionItem { From 2e19915718905a341e712e36ebe823f58de39445 Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Sat, 18 Apr 2026 23:55:32 +0200 Subject: [PATCH 39/57] add failing test --- .../ide-completion/src/tests/expression.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs index 57f067dc94768..4e18a3641c684 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs @@ -4179,3 +4179,32 @@ fn main() { "#]], ); } + +#[test] +fn no_await_on_error_type() { + check( + r#" +//- minicore: future +fn foo(t: T) { + let _ = t.$0; +} + "#, + expect![[r#" + kw await expr.await + sn box Box::new(expr) + sn call function(expr) + sn const const {} + sn dbg dbg!(expr) + sn dbgr dbg!(&expr) + sn deref *expr + sn if if expr {} + sn match match expr {} + sn not !expr + sn ref &expr + sn refm &mut expr + sn return return expr + sn unsafe unsafe {} + sn while while expr {} + "#]], + ); +} From 7e95dd2ae0014aa9ea6ed3792267119348af840a Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Fri, 29 May 2026 17:53:12 +0200 Subject: [PATCH 40/57] fix: return `false` from `implements_trait_unique` for error types --- src/tools/rust-analyzer/crates/hir-ty/src/traits.rs | 5 ++++- .../crates/ide-completion/src/tests/expression.rs | 1 - .../test_data/highlight_general.html | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs b/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs index 4c76ae901da8c..3b5890e5e4fe1 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs @@ -20,7 +20,7 @@ use hir_expand::name::Name; use intern::sym; use rustc_type_ir::{ TypeVisitableExt, TypingMode, - inherent::{BoundExistentialPredicates, IntoKind}, + inherent::{BoundExistentialPredicates, GenericArg as _, IntoKind, Ty as _}, }; use crate::{ @@ -153,6 +153,9 @@ pub fn implements_trait_unique_with_infcx<'db>( let infcx = interner.infer_ctxt().build(TypingMode::non_body_analysis()); let args = create_args(&infcx); + if args.iter().filter_map(|it| it.as_type()).any(|ty| ty.is_ty_error()) { + return false; + } let trait_ref = rustc_type_ir::TraitRef::new_from_args(interner, trait_.into(), args); let obligation = Obligation::new(interner, ObligationCause::dummy(), env.param_env, trait_ref); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs index 4e18a3641c684..adf4dda18441f 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs @@ -4190,7 +4190,6 @@ fn foo(t: T) { } "#, expect![[r#" - kw await expr.await sn box Box::new(expr) sn call function(expr) sn const const {} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html index 1184739cc2589..2f2c7f251a703 100644 --- a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html @@ -128,9 +128,9 @@ let y = &mut x; let z = &y; - let Foo { x: z, y } = Foo { x: z, y }; + let Foo { x: z, y } = Foo { x: z, y }; - y; + y; let mut foo = Foo { x, y: x }; let foo2 = Foo { x, y: x }; @@ -143,7 +143,7 @@ copy.qux(); copy.baz(copy); - let a = |x| x; + let a = |x| x; let bar = Foo::baz; let baz = (-42,); @@ -173,13 +173,13 @@ } async fn learn_and_sing() { - let song = learn_song().await; - sing_song(song).await; + let song = learn_song().await; + sing_song(song).await; } async fn async_main() { let f1 = learn_and_sing(); - let f2 = dance(); + let f2 = dance(); futures::join!(f1, f2); } From 7d9bf81611f4949ad788019b154b45d638bd0c03 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Wed, 17 Jun 2026 16:09:04 +0100 Subject: [PATCH 41/57] internal: Don't rely on FxHashSet order Previously, SCIP generation would iterate over all the external symbols by calling `.difference()` on an FxHashSet and iterating over them. If any tokens had a moniker but no definition, the loop would terminate early. AFAICS this code is unreachable today (all the monikers also have definitions), but the code is clearly wrong. Ensure we process all tokens regardless of the order. --- src/tools/rust-analyzer/crates/rust-analyzer/src/cli/scip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/scip.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/scip.rs index bca38ed82fdd5..d3acbb934d5bb 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/scip.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/scip.rs @@ -237,7 +237,7 @@ impl flags::Scip { let token = si.tokens.get(id).unwrap(); let Some(definition) = token.definition else { - break; + continue; }; let file_id = definition.file_id; From faa57cdd9b779817ea287741fa6a2b3fbda5ccff Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 17 Jun 2026 13:02:12 +0300 Subject: [PATCH 42/57] Check for #[cfg]s in tail expression macros --- .../crates/hir-def/src/expr_store/lower.rs | 8 +++--- .../hir-def/src/expr_store/tests/body.rs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs index bbc3f5333a14f..a69755baf632c 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs @@ -2404,6 +2404,10 @@ impl<'db> ExprCollector<'db> { statements: &mut Vec, mac: ast::MacroExpr, ) -> Option { + if !self.check_cfg(&ast::Expr::MacroExpr(mac.clone())) { + return None; + } + let mac_call = mac.macro_call()?; let syntax_ptr = AstPtr::new(&ast::Expr::from(mac)); let macro_ptr = AstPtr::new(&mac_call); @@ -2447,10 +2451,6 @@ impl<'db> ExprCollector<'db> { } ast::Stmt::ExprStmt(stmt) => { let expr = stmt.expr(); - match &expr { - Some(expr) if !self.check_cfg(expr) => return, - _ => (), - } let has_semi = stmt.semicolon_token().is_some(); // Note that macro could be expanded to multiple statements if let Some(ast::Expr::MacroExpr(mac)) = expr { diff --git a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/tests/body.rs b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/tests/body.rs index e97718ca22c32..da412a620d0a7 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/tests/body.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/tests/body.rs @@ -688,3 +688,31 @@ fn foo() { }"#]], ); } + +#[test] +fn foo() { + pretty_print( + r#" +macro_rules! foo { + () => { + 1 + }; +} + +fn foo() -> i64 { + #[cfg(true)] + { + 5 + } + #[cfg(false)] + foo!() +} + "#, + expect![[r#" + fn foo() { + { + 5 + } + }"#]], + ); +} From 26ea5fe825127457ee2f0a516dca58a3e7731803 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 19 Jun 2026 08:32:09 +0200 Subject: [PATCH 43/57] internal: Do not load unnecessary path dependency files into VFS --- .../crates/project-model/src/workspace.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/project-model/src/workspace.rs b/src/tools/rust-analyzer/crates/project-model/src/workspace.rs index 675533645d0db..c9bf803da347b 100644 --- a/src/tools/rust-analyzer/crates/project-model/src/workspace.rs +++ b/src/tools/rust-analyzer/crates/project-model/src/workspace.rs @@ -811,6 +811,7 @@ impl ProjectWorkspace { .packages() .map(|pkg| { let is_local = cargo[pkg].is_local; + let is_member = cargo[pkg].is_member; let pkg_root = cargo[pkg].manifest.parent().to_path_buf(); let mut include = vec![pkg_root.clone()]; @@ -844,9 +845,11 @@ impl ProjectWorkspace { let mut exclude = vec![pkg_root.join(".git")]; if is_local { include.extend(self.extra_includes.iter().cloned()); - exclude.push(pkg_root.join("target")); - } else { + } + if !is_member { + // For non-workspace-members, we only resolve library targets, + // so none of these need to be loaded into the VFS. exclude.push(pkg_root.join("tests")); exclude.push(pkg_root.join("examples")); exclude.push(pkg_root.join("benches")); @@ -874,6 +877,7 @@ impl ProjectWorkspace { .chain(cargo_script.iter().flat_map(|(cargo, build_scripts, _)| { cargo.packages().map(|pkg| { let is_local = cargo[pkg].is_local; + let is_member = cargo[pkg].is_member; let pkg_root = cargo[pkg].manifest.parent().to_path_buf(); let mut include = vec![pkg_root.clone()]; @@ -909,7 +913,10 @@ impl ProjectWorkspace { include.extend(self.extra_includes.iter().cloned()); exclude.push(pkg_root.join("target")); - } else { + } + if !is_member { + // For non-workspace-members, we only resolve library targets, + // so none of these need to be loaded into the VFS. exclude.push(pkg_root.join("tests")); exclude.push(pkg_root.join("examples")); exclude.push(pkg_root.join("benches")); From ae7ee13b657518204d7c3455f6197c0abe7c46f7 Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Fri, 12 Jun 2026 22:22:48 +0200 Subject: [PATCH 44/57] internal: remove unused structs --- .../rust-analyzer/crates/hir-expand/src/db.rs | 5 ----- src/tools/rust-analyzer/crates/hir-ty/src/db.rs | 17 ++--------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-expand/src/db.rs b/src/tools/rust-analyzer/crates/hir-expand/src/db.rs index 513115684f872..33672d10fa6d5 100644 --- a/src/tools/rust-analyzer/crates/hir-expand/src/db.rs +++ b/src/tools/rust-analyzer/crates/hir-expand/src/db.rs @@ -141,11 +141,6 @@ pub trait ExpandDatabase: SourceDatabase { fn syntax_context(&self, file: HirFileId, edition: Edition) -> SyntaxContext; } -#[salsa_macros::interned(no_lifetime, id = span::SyntaxContext, revisions = usize::MAX)] -pub struct SyntaxContextWrapper { - pub data: SyntaxContext, -} - fn syntax_context(db: &dyn ExpandDatabase, file: HirFileId, edition: Edition) -> SyntaxContext { match file { HirFileId::FileId(_) => SyntaxContext::root(edition), diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/db.rs b/src/tools/rust-analyzer/crates/hir-ty/src/db.rs index baf8bbd56fb1e..93ae26abce88b 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/db.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/db.rs @@ -6,8 +6,8 @@ use base_db::{Crate, target::TargetLoadError}; use either::Either; use hir_def::{ AdtId, BuiltinDeriveImplId, CallableDefId, ConstId, ConstParamId, EnumVariantId, - ExpressionStoreOwnerId, FunctionId, GenericDefId, HasModule, ImplId, LifetimeParamId, - LocalFieldId, ModuleId, StaticId, TraitId, TypeAliasId, VariantId, + ExpressionStoreOwnerId, FunctionId, GenericDefId, HasModule, ImplId, LocalFieldId, ModuleId, + StaticId, TraitId, TypeAliasId, VariantId, builtin_derive::BuiltinDeriveImplMethod, db::DefDatabase, expr_store::ExpressionStore, @@ -294,19 +294,6 @@ fn hir_database_is_dyn_compatible() { fn _assert_dyn_compatible(_: &dyn HirDatabase) {} } -#[salsa_macros::interned(no_lifetime, debug, revisions = usize::MAX)] -#[derive(PartialOrd, Ord)] -pub struct InternedLifetimeParamId { - /// This stores the param and its index. - pub loc: (LifetimeParamId, u32), -} - -#[salsa_macros::interned(no_lifetime, debug, revisions = usize::MAX)] -#[derive(PartialOrd, Ord)] -pub struct InternedConstParamId { - pub loc: ConstParamId, -} - #[salsa_macros::interned(no_lifetime, debug, revisions = usize::MAX)] #[derive(PartialOrd, Ord)] pub struct InternedOpaqueTyId { From 8ae83150ee7a10528b7880612799ede4c3097ebe Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Fri, 19 Jun 2026 11:09:30 +0200 Subject: [PATCH 45/57] fix(implements_trait_unique_with_infcx): only forbid the self type from being an error type --- src/tools/rust-analyzer/crates/hir-ty/src/traits.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs b/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs index 3b5890e5e4fe1..2ca9ebe070bc9 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/traits.rs @@ -20,7 +20,7 @@ use hir_expand::name::Name; use intern::sym; use rustc_type_ir::{ TypeVisitableExt, TypingMode, - inherent::{BoundExistentialPredicates, GenericArg as _, IntoKind, Ty as _}, + inherent::{BoundExistentialPredicates, IntoKind, Ty as _}, }; use crate::{ @@ -153,10 +153,10 @@ pub fn implements_trait_unique_with_infcx<'db>( let infcx = interner.infer_ctxt().build(TypingMode::non_body_analysis()); let args = create_args(&infcx); - if args.iter().filter_map(|it| it.as_type()).any(|ty| ty.is_ty_error()) { + let trait_ref = rustc_type_ir::TraitRef::new_from_args(interner, trait_.into(), args); + if trait_ref.self_ty().is_ty_error() { return false; } - let trait_ref = rustc_type_ir::TraitRef::new_from_args(interner, trait_.into(), args); let obligation = Obligation::new(interner, ObligationCause::dummy(), env.param_env, trait_ref); infcx.predicate_must_hold_modulo_regions(&obligation) From 542e70fa844da86dfbc33f0481d7e82983026d3f Mon Sep 17 00:00:00 2001 From: shulaoda <165626830+shulaoda@users.noreply.github.com> Date: Sat, 20 Jun 2026 08:10:14 +0800 Subject: [PATCH 46/57] fix: Don't panic on out-of-range integer literals in const positions --- .../rust-analyzer/crates/hir-ty/src/consteval.rs | 16 ++++++++++++---- .../hir-ty/src/tests/regression/new_solver.rs | 10 ++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/consteval.rs b/src/tools/rust-analyzer/crates/hir-ty/src/consteval.rs index d6580d3752f6b..a880ae5353b66 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/consteval.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/consteval.rs @@ -87,18 +87,24 @@ fn intern_const_ref<'db>( let valtree = match (ty.kind(), value) { (TyKind::Uint(uint), Literal::Uint(value, _)) => { let size = uint.bit_width().map(Size::from_bits).unwrap_or(data_layout.pointer_size()); - let scalar = ScalarInt::try_from_uint(*value, size).unwrap(); + let Some(scalar) = ScalarInt::try_from_uint(*value, size) else { + return Ok(Const::error(interner)); + }; ValTreeKind::Leaf(scalar) } (TyKind::Uint(uint), Literal::Int(value, _)) => { // `Literal::Int` is the default, so we also need to account for the type being uint. let size = uint.bit_width().map(Size::from_bits).unwrap_or(data_layout.pointer_size()); - let scalar = ScalarInt::try_from_uint(*value as u128, size).unwrap(); + let Some(scalar) = ScalarInt::try_from_uint(*value as u128, size) else { + return Ok(Const::error(interner)); + }; ValTreeKind::Leaf(scalar) } (TyKind::Int(int), Literal::Int(value, _)) => { let size = int.bit_width().map(Size::from_bits).unwrap_or(data_layout.pointer_size()); - let scalar = ScalarInt::try_from_int(*value, size).unwrap(); + let Some(scalar) = ScalarInt::try_from_int(*value, size) else { + return Ok(Const::error(interner)); + }; ValTreeKind::Leaf(scalar) } (TyKind::Bool, Literal::Bool(value)) => ValTreeKind::Leaf(ScalarInt::from(*value)), @@ -219,7 +225,9 @@ pub fn usize_const<'db>(db: &'db dyn HirDatabase, value: Option, krate: Cr return Const::error(interner); }; let usize_ty = interner.default_types().types.usize; - let scalar = ScalarInt::try_from_uint(value, data_layout.pointer_size()).unwrap(); + let Some(scalar) = ScalarInt::try_from_uint(value, data_layout.pointer_size()) else { + return Const::error(interner); + }; Const::new_valtree(interner, usize_ty, ValTreeKind::Leaf(scalar)) } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs index 17127b2771d2e..fb00a755fa055 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression/new_solver.rs @@ -404,6 +404,16 @@ const A: [u8; S] = [0; 8]; ); } +#[test] +fn oversized_array_len_does_not_panic() { + // The array length literal does not fit in `usize`; interning it must not panic. + check_no_mismatches( + r#" +fn f(_: [u8; 18446744073709551616]) {} + "#, + ); +} + #[test] fn another_20654_case() { check_no_mismatches( From af7cb85aa9677c57e224d6c25bc562f0fa604f59 Mon Sep 17 00:00:00 2001 From: Raushan kumar Date: Sat, 20 Jun 2026 04:52:21 +0000 Subject: [PATCH 47/57] fix(toolchain): use std::env::home_dir instead of home crate --- src/tools/rust-analyzer/Cargo.lock | 10 ---------- src/tools/rust-analyzer/crates/toolchain/Cargo.toml | 1 - src/tools/rust-analyzer/crates/toolchain/src/lib.rs | 2 +- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/tools/rust-analyzer/Cargo.lock b/src/tools/rust-analyzer/Cargo.lock index 5e39dfe87004b..38aa3050f4f82 100644 --- a/src/tools/rust-analyzer/Cargo.lock +++ b/src/tools/rust-analyzer/Cargo.lock @@ -1002,15 +1002,6 @@ dependencies = [ "typed-arena", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "icu_collections" version = "2.2.0" @@ -3148,7 +3139,6 @@ name = "toolchain" version = "0.0.0" dependencies = [ "camino", - "home", ] [[package]] diff --git a/src/tools/rust-analyzer/crates/toolchain/Cargo.toml b/src/tools/rust-analyzer/crates/toolchain/Cargo.toml index f561c1c0e2b0e..d0d5840e785a0 100644 --- a/src/tools/rust-analyzer/crates/toolchain/Cargo.toml +++ b/src/tools/rust-analyzer/crates/toolchain/Cargo.toml @@ -13,7 +13,6 @@ rust-version.workspace = true doctest = false [dependencies] -home = "0.5.11" camino.workspace = true [lints] diff --git a/src/tools/rust-analyzer/crates/toolchain/src/lib.rs b/src/tools/rust-analyzer/crates/toolchain/src/lib.rs index 39319886cfe4a..ea08f7aacac4e 100644 --- a/src/tools/rust-analyzer/crates/toolchain/src/lib.rs +++ b/src/tools/rust-analyzer/crates/toolchain/src/lib.rs @@ -119,7 +119,7 @@ fn get_cargo_home() -> Option { return Utf8PathBuf::try_from(PathBuf::from(path)).ok(); } - if let Some(mut path) = home::home_dir() { + if let Some(mut path) = env::home_dir() { path.push(".cargo"); return Utf8PathBuf::try_from(path).ok(); } From b860d7b6c2bd99fe9ddb275fea93232f4ee9ed0f Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Sat, 20 Jun 2026 07:15:51 +0200 Subject: [PATCH 48/57] Fix release pipeline by pinning node docker image. See nodejs/node#63989 --- .../rust-analyzer/.github/actions/github-release/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/.github/actions/github-release/Dockerfile b/src/tools/rust-analyzer/.github/actions/github-release/Dockerfile index 5849eac7d246a..ad296ce74152f 100644 --- a/src/tools/rust-analyzer/.github/actions/github-release/Dockerfile +++ b/src/tools/rust-analyzer/.github/actions/github-release/Dockerfile @@ -1,4 +1,4 @@ -FROM node:slim +FROM node:24.16-slim@sha256:ca520832af80fa37a57c14077ed0fcdd83b5aefccc356059fdc3a9a05b78ae1f COPY . /action WORKDIR /action From 837f5bf3eceb8daeefd953625f3514d812a55b8a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 20 Jun 2026 13:43:48 +0200 Subject: [PATCH 49/57] Track salsa cancellation time in loop turn warning --- .../crates/ide-db/src/apply_change.rs | 7 +- src/tools/rust-analyzer/crates/ide/src/lib.rs | 5 +- .../crates/rust-analyzer/src/global_state.rs | 8 +- .../crates/rust-analyzer/src/main_loop.rs | 85 ++++++++++++------- .../crates/rust-analyzer/src/reload.rs | 44 +++++----- .../crates/rust-analyzer/src/task_pool.rs | 4 - 6 files changed, 91 insertions(+), 62 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs b/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs index b77a18f56ea21..7a3c466daa50c 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs @@ -1,16 +1,21 @@ //! Applies changes to the IDE state transactionally. +use std::time::{Duration, Instant}; + use profile::Bytes; use salsa::Database as _; use crate::{ChangeWithProcMacros, RootDatabase}; impl RootDatabase { - pub fn apply_change(&mut self, change: ChangeWithProcMacros) { + pub fn apply_change(&mut self, change: ChangeWithProcMacros) -> Duration { let _p = tracing::info_span!("RootDatabase::apply_change").entered(); + let now = Instant::now(); self.trigger_cancellation(); + let elapsed = now.elapsed(); tracing::trace!("apply_change {:?}", change); change.apply(self); + elapsed } // Feature: Memory Usage diff --git a/src/tools/rust-analyzer/crates/ide/src/lib.rs b/src/tools/rust-analyzer/crates/ide/src/lib.rs index 88cb570c6b0f3..dded01520ffbc 100644 --- a/src/tools/rust-analyzer/crates/ide/src/lib.rs +++ b/src/tools/rust-analyzer/crates/ide/src/lib.rs @@ -60,6 +60,7 @@ mod view_mir; mod view_syntax_tree; use std::panic::{AssertUnwindSafe, UnwindSafe}; +use std::time::Duration; use cfg::CfgOptions; use fetch_crates::CrateInfo; @@ -197,8 +198,8 @@ impl AnalysisHost { /// Applies changes to the current state of the world. If there are /// outstanding snapshots, they will be canceled. - pub fn apply_change(&mut self, change: ChangeWithProcMacros) { - self.db.apply_change(change); + pub fn apply_change(&mut self, change: ChangeWithProcMacros) -> Duration { + self.db.apply_change(change) } /// NB: this clears the database diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/global_state.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/global_state.rs index afd4162de6227..f91e9532aaf02 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/global_state.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/global_state.rs @@ -330,7 +330,7 @@ impl GlobalState { this } - pub(crate) fn process_changes(&mut self) -> bool { + pub(crate) fn process_changes(&mut self) -> (bool, Option) { let _p = span!(Level::INFO, "GlobalState::process_changes").entered(); // We cannot directly resolve a change in a ratoml file to a format // that can be used by the config module because config talks @@ -343,7 +343,7 @@ impl GlobalState { let mut guard = self.vfs.write(); let changed_files = guard.0.take_changes(); if changed_files.is_empty() { - return false; + return (false, None); } let (change, modified_rust_files, workspace_structure_change) = @@ -439,7 +439,7 @@ impl GlobalState { (change, modified_rust_files, workspace_structure_change) }); - self.analysis_host.apply_change(change); + let cancellation_time = self.analysis_host.apply_change(change); if !modified_ratoml_files.is_empty() || !self.config.same_source_root_parent_map(&self.local_roots_parent_map) @@ -561,7 +561,7 @@ impl GlobalState { } } - true + (true, Some(cancellation_time)) } pub(crate) fn snapshot(&self) -> GlobalStateSnapshot { diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs index 628e708c6a83d..d966e29ede0c4 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs @@ -307,15 +307,11 @@ impl GlobalState { let _p = tracing::info_span!("GlobalState::handle_event", event = %event).entered(); let event_dbg_msg = format!("{event:?}"); - tracing::debug!(?loop_start, ?event, "handle_event"); - if tracing::enabled!(tracing::Level::TRACE) { - let task_queue_len = self.task_pool.handle.len(); - if task_queue_len > 0 { - tracing::trace!("task queue len: {}", task_queue_len); - } - } + tracing::debug!(?event, "handle_event"); let was_quiescent = self.is_quiescent(); + + let mut cancellation_time = None; match event { Event::Lsp(msg) => match msg { lsp_server::Message::Request(req) => self.on_new_request(loop_start, req), @@ -326,7 +322,9 @@ impl GlobalState { let _p = tracing::info_span!("GlobalState::handle_event/queued_task").entered(); self.handle_deferred_task(task); // Coalesce multiple deferred task events into one loop turn - while let Ok(task) = self.deferred_task_queue.receiver.try_recv() { + while loop_start.elapsed() < Duration::from_millis(50) + && let Ok(task) = self.deferred_task_queue.receiver.try_recv() + { self.handle_deferred_task(task); } } @@ -334,14 +332,16 @@ impl GlobalState { let _p = tracing::info_span!("GlobalState::handle_event/task").entered(); let mut prime_caches_progress = Vec::new(); - self.handle_task(&mut prime_caches_progress, task); + cancellation_time = self.handle_task(&mut prime_caches_progress, task); // Coalesce multiple task events into one loop turn - while let Ok(task) = self.task_pool.receiver.try_recv() { + while loop_start.elapsed() < Duration::from_millis(50) + && let Ok(task) = self.task_pool.receiver.try_recv() + { self.handle_task(&mut prime_caches_progress, task); } let title = "Indexing"; - let cancel_token = Some("rustAnalyzer/cachePriming".to_owned()); + let cancel_token = || Some("rustAnalyzer/cachePriming".to_owned()); let mut last_report = None; for progress in prime_caches_progress { @@ -352,7 +352,7 @@ impl GlobalState { Progress::Begin, None, Some(0.0), - cancel_token.clone(), + cancel_token(), ); } PrimeCachesProgress::Report(report) => { @@ -404,7 +404,7 @@ impl GlobalState { Progress::Report, message, Some(fraction), - cancel_token.clone(), + cancel_token(), ); } self.report_progress( @@ -412,7 +412,7 @@ impl GlobalState { Progress::End, None, Some(1.0), - cancel_token.clone(), + cancel_token(), ); } }; @@ -423,7 +423,7 @@ impl GlobalState { Progress::Report, message, Some(fraction), - cancel_token.clone(), + cancel_token(), ); } } @@ -432,7 +432,9 @@ impl GlobalState { let mut last_progress_report = None; self.handle_vfs_msg(message, &mut last_progress_report); // Coalesce many VFS event into a single loop turn - while let Ok(message) = self.loader.receiver.try_recv() { + while loop_start.elapsed() < Duration::from_millis(50) + && let Ok(message) = self.loader.receiver.try_recv() + { self.handle_vfs_msg(message, &mut last_progress_report); } if let Some((message, fraction)) = last_progress_report { @@ -449,7 +451,9 @@ impl GlobalState { let mut cargo_finished = false; self.handle_flycheck_msg(message, &mut cargo_finished); // Coalesce many flycheck updates into a single loop turn - while let Ok(message) = self.flycheck_receiver.try_recv() { + while loop_start.elapsed() < Duration::from_millis(50) + && let Ok(message) = self.flycheck_receiver.try_recv() + { self.handle_flycheck_msg(message, &mut cargo_finished); } if cargo_finished { @@ -463,14 +467,18 @@ impl GlobalState { let _p = tracing::info_span!("GlobalState::handle_event/test_result").entered(); self.handle_cargo_test_msg(message); // Coalesce many test result event into a single loop turn - while let Ok(message) = self.test_run_receiver.try_recv() { + while loop_start.elapsed() < Duration::from_millis(50) + && let Ok(message) = self.test_run_receiver.try_recv() + { self.handle_cargo_test_msg(message); } } Event::DiscoverProject(message) => { self.handle_discover_msg(message); // Coalesce many project discovery events into a single loop turn. - while let Ok(message) = self.discover_receiver.try_recv() { + while loop_start.elapsed() < Duration::from_millis(50) + && let Ok(message) = self.discover_receiver.try_recv() + { self.handle_discover_msg(message); } } @@ -479,13 +487,23 @@ impl GlobalState { } } let event_handling_duration = loop_start.elapsed(); - let (state_changed, memdocs_added_or_removed) = if self.vfs_done { - if let Some(cause) = self.wants_to_switch.take() { - self.switch_workspaces(cause); - } - (self.process_changes(), self.mem_docs.take_changes()) - } else { - (false, false) + let ((state_changed, changes_cancellation_time), memdocs_added_or_removed) = + if self.vfs_done { + if let Some(cause) = self.wants_to_switch.take() { + cancellation_time = match (cancellation_time, self.switch_workspaces(cause)) { + (Some(a), Some(b)) => Some(a + b), + (Some(d), None) | (None, Some(d)) => Some(d), + (None, None) => None, + }; + } + (self.process_changes(), self.mem_docs.take_changes()) + } else { + ((false, None), false) + }; + cancellation_time = match (cancellation_time, changes_cancellation_time) { + (Some(a), Some(b)) => Some(a + b), + (Some(d), None) | (None, Some(d)) => Some(d), + (None, None) => None, }; let mut gc_elapsed = None; @@ -609,11 +627,13 @@ impl GlobalState { tracing::warn!( "overly long loop turn took {loop_duration:?}:\n\ (event handling took {event_handling_duration:?}): {event_dbg_msg}\n\ + (cancellation took {cancellation_time:?}) (garbage collection took {gc_elapsed:?})" ); self.poke_rust_analyzer_developer(format!( "overly long loop turn took {loop_duration:?}:\n\ (event handling took {event_handling_duration:?}): {event_dbg_msg}\n\ + (cancellation took {cancellation_time:?}) (garbage collection took {gc_elapsed:?})" )); } @@ -819,7 +839,12 @@ impl GlobalState { } } - fn handle_task(&mut self, prime_caches_progress: &mut Vec, task: Task) { + fn handle_task( + &mut self, + prime_caches_progress: &mut Vec, + task: Task, + ) -> Option { + let mut cancellation_time = None; match task { Task::Response(response) => self.respond(response), // Only retry requests that haven't been cancelled. Otherwise we do unnecessary work. @@ -922,8 +947,9 @@ impl GlobalState { ProcMacroProgress::Report(msg) => (Some(Progress::Report), Some(msg)), ProcMacroProgress::End(change) => { self.fetch_proc_macros_queue.op_completed(true); - self.analysis_host.apply_change(change); - self.finish_loading_crate_graph(); + cancellation_time = Some(self.analysis_host.apply_change(change)); + // FIXME This feels a bit off, this should go through similar machinery as build scripts? + _ = self.finish_loading_crate_graph(); (Some(Progress::End), None) } }; @@ -937,6 +963,7 @@ impl GlobalState { self.send_notification::(tests); } } + cancellation_time } fn handle_vfs_msg( diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs index 74fd0e653398b..4940defed2ac2 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs @@ -13,7 +13,7 @@ //! project is currently loading and we don't have a full project model, we //! still want to respond to various requests. // FIXME: This is a mess that needs some untangling work -use std::{iter, mem, sync::atomic::AtomicUsize}; +use std::{iter, mem, sync::atomic::AtomicUsize, time::Duration}; use hir::{ChangeWithProcMacros, ProcMacrosBuilder, db::DefDatabase}; use ide_db::{ @@ -468,25 +468,22 @@ impl GlobalState { }); } - pub(crate) fn switch_workspaces(&mut self, cause: Cause) { + pub(crate) fn switch_workspaces(&mut self, cause: Cause) -> Option { let _p = tracing::info_span!("GlobalState::switch_workspaces").entered(); tracing::info!(%cause, "will switch workspaces"); - let Some(FetchWorkspaceResponse { workspaces, force_crate_graph_reload }) = - self.fetch_workspaces_queue.last_op_result() - else { - return; - }; + let FetchWorkspaceResponse { workspaces, force_crate_graph_reload } = + self.fetch_workspaces_queue.last_op_result()?; let switching_from_empty_workspace = self.workspaces.is_empty(); info!(%cause, ?force_crate_graph_reload, %switching_from_empty_workspace); if self.fetch_workspace_error().is_err() && !switching_from_empty_workspace { if *force_crate_graph_reload { - self.recreate_crate_graph(cause, false); + return self.recreate_crate_graph(cause, false); } // It only makes sense to switch to a partially broken workspace // if we don't have any workspace at all yet. - return; + return None; } let workspaces = @@ -501,7 +498,7 @@ impl GlobalState { if same_workspaces { if switching_from_empty_workspace { // Switching from empty to empty is a no-op - return; + return None; } if let Some(FetchBuildDataResponse { workspaces, build_scripts }) = self.fetch_build_data_queue.last_op_result() @@ -524,20 +521,20 @@ impl GlobalState { } else { info!("build scripts do not match the version of the active workspace"); if *force_crate_graph_reload { - self.recreate_crate_graph(cause, switching_from_empty_workspace); + return self.recreate_crate_graph(cause, switching_from_empty_workspace); } // Current build scripts do not match the version of the active // workspace, so there's nothing for us to update. - return; + return None; } } else { if *force_crate_graph_reload { - self.recreate_crate_graph(cause, switching_from_empty_workspace); + return self.recreate_crate_graph(cause, switching_from_empty_workspace); } // No build scripts but unchanged workspaces, nothing to do here - return; + return None; } } else { info!("abandon build scripts for workspaces"); @@ -560,7 +557,7 @@ impl GlobalState { // `switch_workspaces()` will be called again when build scripts already run, which should // take a short time. If we update the workspace now we will invalidate proc macros and cfgs, // and then when build scripts complete we will invalidate them again. - return; + return None; } } } @@ -733,13 +730,15 @@ impl GlobalState { self.local_roots_parent_map = Arc::new(self.source_root_config.source_root_parent_map()); info!(?cause, "recreating the crate graph"); - self.recreate_crate_graph(cause, switching_from_empty_workspace); + let cancellation_time = self.recreate_crate_graph(cause, switching_from_empty_workspace); info!("did switch workspaces"); + cancellation_time } - fn recreate_crate_graph(&mut self, cause: String, initial_build: bool) { + fn recreate_crate_graph(&mut self, cause: String, initial_build: bool) -> Option { info!(?cause, "Building Crate Graph"); + let mut cancellation_time = None; self.report_progress( "Building CrateGraph", crate::lsp::utils::Progress::Begin, @@ -795,9 +794,8 @@ impl GlobalState { } change.set_crate_graph(crate_graph); - self.analysis_host.apply_change(change); - - self.finish_loading_crate_graph(); + cancellation_time = Some(self.analysis_host.apply_change(change)); + _ = self.finish_loading_crate_graph(); } else { change.set_crate_graph(crate_graph); self.fetch_proc_macros_queue.request_op(cause, (change, proc_macro_paths)); @@ -810,11 +808,13 @@ impl GlobalState { None, None, ); + cancellation_time } - pub(crate) fn finish_loading_crate_graph(&mut self) { - self.process_changes(); + pub(crate) fn finish_loading_crate_graph(&mut self) -> Option { + let (_, cancellation_time) = self.process_changes(); self.reload_flycheck(); + cancellation_time } pub(super) fn fetch_workspace_error(&self) -> Result<(), String> { diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/task_pool.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/task_pool.rs index 104cd3d2eae9e..2da52f7480d8a 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/task_pool.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/task_pool.rs @@ -40,10 +40,6 @@ impl TaskPool { }) } - pub(crate) fn len(&self) -> usize { - self.pool.len() - } - pub(crate) fn is_empty(&self) -> bool { self.pool.is_empty() } From 303b159ae7f7acf4998ee7871da01c27376aa2e6 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 20 Jun 2026 17:33:14 +0200 Subject: [PATCH 50/57] Take &SyntaxFactory instead of &SyntaxEditor in merge/normalize import APIs try_merge_imports, try_merge_trees, and try_normalize_import only ever used the editor to obtain its SyntaxFactory; they never edited through it. --- .../ide-assists/src/handlers/merge_imports.rs | 4 ++-- .../src/handlers/normalize_import.rs | 2 +- .../crates/ide-db/src/imports/insert_use.rs | 4 +++- .../ide-db/src/imports/insert_use/tests.rs | 13 ++++++----- .../ide-db/src/imports/merge_imports.rs | 23 +++++++------------ .../rust-analyzer/crates/parser/src/output.rs | 2 +- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs index e942be33d94c5..5d81f49b18507 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs @@ -98,7 +98,7 @@ fn merge_uses( }; let mut merged = first.clone(); for item in &rest { - merged = try_merge_imports(editor, &merged, item, mb)?; + merged = try_merge_imports(editor.make(), &merged, item, mb)?; } for item in rest { item.remove(editor); @@ -118,7 +118,7 @@ fn merge_use_trees( let mut merged = first.clone(); for item in &rest { - merged = try_merge_trees(editor, &merged, item, MergeBehavior::Crate)?; + merged = try_merge_trees(editor.make(), &merged, item, MergeBehavior::Crate)?; } for item in rest { item.remove(editor); diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs index 07571d74d8b8d..906f38cba407f 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/normalize_import.rs @@ -27,7 +27,7 @@ pub(crate) fn normalize_import(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - let target = use_item.syntax().text_range(); let (editor, _) = SyntaxEditor::new(use_item.syntax().ancestors().last().unwrap()); let normalized_use_item = - try_normalize_import(&editor, &use_item, ctx.config.insert_use.granularity.into())?; + try_normalize_import(editor.make(), &use_item, ctx.config.insert_use.granularity.into())?; editor.replace(use_item.syntax(), normalized_use_item.syntax()); acc.add(AssistId::refactor_rewrite("normalize_import"), "Normalize import", target, |builder| { diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs index 1fd493fd2a730..27e3ed6bdb52e 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs @@ -263,7 +263,9 @@ fn insert_use_with_alias_option_with_editor( for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast).filter(filter) { - if let Some(merged) = try_merge_imports(syntax_editor, &existing_use, &use_item, mb) { + if let Some(merged) = + try_merge_imports(syntax_editor.make(), &existing_use, &use_item, mb) + { syntax_editor.replace(existing_use.syntax(), merged.syntax()); return; } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs index a30d290490210..34ff7c8d5b426 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs @@ -1,4 +1,5 @@ use stdx::trim_indent; +use syntax::ast::syntax_factory::SyntaxFactory; use test_fixture::WithFixture; use test_utils::{CURSOR_MARKER, assert_eq_text}; @@ -1430,8 +1431,8 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior .find_map(ast::Use::cast) .unwrap(); - let (editor, _) = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()); - let result = try_merge_imports(&editor, &use0, &use1, mb); + let make = SyntaxFactory::without_mappings(); + let result = try_merge_imports(&make, &use0, &use1, mb); assert_eq!(result.map(|u| u.to_string()), None); } @@ -1496,8 +1497,8 @@ fn check_merge(ra_fixture0: &str, ra_fixture1: &str, last: &str, mb: MergeBehavi .find_map(ast::Use::cast) .unwrap(); - let (editor, _) = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()); - let result = try_merge_imports(&editor, &use0, &use1, mb); + let make = SyntaxFactory::without_mappings(); + let result = try_merge_imports(&make, &use0, &use1, mb); assert_eq!(result.map(|u| u.to_string().trim().to_owned()), Some(last.trim().to_owned())); } @@ -1527,8 +1528,8 @@ fn merge_gated_imports_with_different_values() { .find_map(ast::Use::cast) .unwrap(); - let (editor, _) = SyntaxEditor::new(use0.syntax().ancestors().last().unwrap()); - let result = try_merge_imports(&editor, &use0, &use1, MergeBehavior::Crate); + let make = SyntaxFactory::without_mappings(); + let result = try_merge_imports(&make, &use0, &use1, MergeBehavior::Crate); assert_eq!(result, None); } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs index 95e654df174f5..9b68c27ae3443 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/merge_imports.rs @@ -40,7 +40,7 @@ impl MergeBehavior { /// Merge `rhs` into `lhs` keeping both intact. pub fn try_merge_imports( - editor: &SyntaxEditor, + make: &SyntaxFactory, lhs: &ast::Use, rhs: &ast::Use, merge_behavior: MergeBehavior, @@ -53,7 +53,6 @@ pub fn try_merge_imports( return None; } - let make = editor.make(); let lhs_tree = lhs.use_tree()?; let rhs_tree = rhs.use_tree()?; let merged_tree = try_merge_trees_with_factory(lhs_tree, rhs_tree, merge_behavior, make)?; @@ -67,12 +66,11 @@ pub fn try_merge_imports( /// Merge `rhs` into `lhs` keeping both intact. pub fn try_merge_trees( - editor: &SyntaxEditor, + make: &SyntaxFactory, lhs: &ast::UseTree, rhs: &ast::UseTree, merge: MergeBehavior, ) -> Option { - let make = editor.make(); let merged = try_merge_trees_with_factory(lhs.clone(), rhs.clone(), merge, make)?; // Ignore `None` result because normalization should not affect the merge result. @@ -100,15 +98,11 @@ fn try_merge_trees_with_factory( { // we can't merge if the renames are different (`A as a` and `A as b`), // and we can safely return here - let lhs_name = lhs - .rename() - .and_then(|lhs_name| lhs_name.name()) - .map(|name| name.text().to_string()); - let rhs_name = rhs - .rename() - .and_then(|rhs_name| rhs_name.name()) - .map(|name| name.text().to_string()); - if lhs_name != rhs_name { + let lhs_name = lhs.rename().and_then(|lhs_name| lhs_name.name()); + let rhs_name = rhs.rename().and_then(|rhs_name| rhs_name.name()); + if lhs_name.as_ref().map(|name| name.text()) + != rhs_name.as_ref().map(|name| name.text()) + { return None; } @@ -263,11 +257,10 @@ impl From for NormalizationStyle { /// - `foo::bar::{self}` -> `{foo::bar}` /// - `foo::bar` -> `{foo::bar}` pub fn try_normalize_import( - editor: &SyntaxEditor, + make: &SyntaxFactory, use_item: &ast::Use, style: NormalizationStyle, ) -> Option { - let make = editor.make(); let use_tree = try_normalize_use_tree(use_item.use_tree()?, style, make)?; make_use_with_tree(use_item, use_tree) diff --git a/src/tools/rust-analyzer/crates/parser/src/output.rs b/src/tools/rust-analyzer/crates/parser/src/output.rs index 2f09b1121891b..ce64db8adae90 100644 --- a/src/tools/rust-analyzer/crates/parser/src/output.rs +++ b/src/tools/rust-analyzer/crates/parser/src/output.rs @@ -18,7 +18,7 @@ pub struct Output { /// /// ```text /// |16 bit kind|8 bit n_input_tokens|4 bit tag|4 bit leftover| - /// `````` + /// ``` event: Vec, error: Vec, } From a408c10cc061ccbf5de24177c447d205b878f73e Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 20 Jun 2026 17:36:56 +0200 Subject: [PATCH 51/57] Consolidate Use/UseTree editor removal into the Removable trait ast::edit had private inherent ast::Use::remove and ast::UseTree::remove_with_editor methods that duplicated the --- .../crates/syntax/src/ast/edit.rs | 69 +------------------ .../crates/syntax/src/syntax_editor/edits.rs | 24 ++++++- 2 files changed, 25 insertions(+), 68 deletions(-) diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs index 0155df8aa0b86..2448809a840b8 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs @@ -2,7 +2,6 @@ //! immutable, all function here return a fresh copy of the tree, instead of //! doing an in-place modification. use parser::T; -use rowan::Direction; use std::{ fmt, iter::{self, once}, @@ -13,9 +12,8 @@ use crate::{ AstToken, NodeOrToken, SyntaxElement, SyntaxKind::{ATTR, COMMENT, WHITESPACE}, SyntaxNode, SyntaxToken, - algo::neighbor, ast::{self, AstNode, HasName, make}, - syntax_editor::{Position, SyntaxEditor, SyntaxMappingBuilder}, + syntax_editor::{Position, Removable, SyntaxEditor, SyntaxMappingBuilder}, }; use super::syntax_factory::SyntaxFactory; @@ -286,24 +284,6 @@ impl ast::GenericParamList { } impl ast::UseTree { - /// Editor variant of UseTree remove - fn remove_with_editor(&self, editor: &SyntaxEditor) { - for dir in [Direction::Next, Direction::Prev] { - if let Some(next_use_tree) = neighbor(self, dir) { - let separators = self - .syntax() - .siblings_with_tokens(dir) - .skip(1) - .take_while(|it| it.as_node() != Some(next_use_tree.syntax())); - for separator in separators { - editor.delete(separator); - } - break; - } - } - editor.delete(self.syntax()); - } - /// Deletes the usetree node represented by the input. Recursively removes parents, including use nodes that become empty. pub fn remove_recursive(self, editor: &SyntaxEditor) { let parent = self.syntax().parent(); @@ -319,7 +299,7 @@ impl ast::UseTree { u.parent_use_tree().remove_recursive(editor); return; } - self.remove_with_editor(editor); + self.remove(editor); u.remove_unnecessary_braces(editor); } } @@ -365,51 +345,6 @@ impl ast::UseTree { } } -impl ast::Use { - fn remove(&self, editor: &SyntaxEditor) { - let make = editor.make(); - let next_ws = self - .syntax() - .next_sibling_or_token() - .and_then(|it| it.into_token()) - .and_then(ast::Whitespace::cast); - if let Some(next_ws) = next_ws { - let ws_text = next_ws.syntax().text(); - if let Some(rest) = ws_text.strip_prefix('\n') { - let next_use_removed = next_ws - .syntax() - .next_sibling_or_token() - .and_then(|it| it.into_node()) - .and_then(ast::Use::cast) - .and_then(|use_| use_.use_tree()) - .is_some_and(|use_tree| editor.deleted(use_tree.syntax())); - if rest.is_empty() || next_use_removed { - editor.delete(next_ws.syntax()); - } else { - editor.replace(next_ws.syntax(), make.whitespace(rest)); - } - } - } - let prev_ws = self - .syntax() - .prev_sibling_or_token() - .and_then(|it| it.into_token()) - .and_then(ast::Whitespace::cast); - if let Some(prev_ws) = prev_ws { - let ws_text = prev_ws.syntax().text(); - let prev_newline = ws_text.rfind('\n').map(|x| x + 1).unwrap_or(0); - let rest = &ws_text[0..prev_newline]; - if rest.is_empty() { - editor.delete(prev_ws.syntax()); - } else { - editor.replace(prev_ws.syntax(), make.whitespace(rest)); - } - } - - editor.delete(self.syntax()); - } -} - impl ast::RecordExprField { /// This will either replace the initializer, or in the case that this is a shorthand convert /// the initializer into the name ref and insert the expr as the new initializer. diff --git a/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs b/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs index f2b979eb9d1ae..9fab8716b412f 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs @@ -459,13 +459,35 @@ impl Removable for ast::Use { if let Some(next_ws) = next_ws { let ws_text = next_ws.syntax().text(); if let Some(rest) = ws_text.strip_prefix('\n') { - if rest.is_empty() { + let next_use_removed = next_ws + .syntax() + .next_sibling_or_token() + .and_then(|it| it.into_node()) + .and_then(ast::Use::cast) + .and_then(|use_| use_.use_tree()) + .is_some_and(|use_tree| editor.deleted(use_tree.syntax())); + if rest.is_empty() || next_use_removed { editor.delete(next_ws.syntax()); } else { editor.replace(next_ws.syntax(), make.whitespace(rest)); } } } + let prev_ws = self + .syntax() + .prev_sibling_or_token() + .and_then(|it| it.into_token()) + .and_then(ast::Whitespace::cast); + if let Some(prev_ws) = prev_ws { + let ws_text = prev_ws.syntax().text(); + let prev_newline = ws_text.rfind('\n').map(|x| x + 1).unwrap_or(0); + let rest = &ws_text[0..prev_newline]; + if rest.is_empty() { + editor.delete(prev_ws.syntax()); + } else { + editor.replace(prev_ws.syntax(), make.whitespace(rest)); + } + } editor.delete(self.syntax()); } From 9e66c39de321917a9d19f68a64e6fd1155679b48 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 20 Jun 2026 17:45:04 +0200 Subject: [PATCH 52/57] Add regression tests for UseTree::split_prefix_with_editor --- .../crates/syntax/src/ast/edit.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs index 2448809a840b8..eaa36903bf26f 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs @@ -313,7 +313,7 @@ impl ast::UseTree { /// /// `prefix$0` -> `prefix::{self}` /// - /// `prefix$0::*` -> `prefix::{*}```` + /// `prefix$0::*` -> `prefix::{*}` pub fn split_prefix_with_editor(&self, editor: &SyntaxEditor, prefix: &ast::Path) { debug_assert_eq!(self.path(), Some(prefix.top_path())); @@ -388,3 +388,29 @@ fn test_increase_indent() { }" ); } + +#[test] +fn split_prefix_inserts_self() { + check_split_prefix("use foo;", "foo::{self}"); +} + +#[test] +fn split_prefix_preserves_rename() { + check_split_prefix("use foo as bar;", "foo::{self as bar}"); +} + +#[test] +fn split_prefix_wraps_glob() { + check_split_prefix("use foo::*;", "foo::{*}"); +} + +#[cfg(test)] +fn check_split_prefix(before: &str, expected: &str) { + let source = crate::SourceFile::parse(before, parser::Edition::CURRENT).tree(); + let use_tree = source.syntax().descendants().find_map(ast::UseTree::cast).unwrap(); + let (editor, use_tree) = SyntaxEditor::with_ast_node(&use_tree); + let prefix = use_tree.path().unwrap(); + use_tree.split_prefix_with_editor(&editor, &prefix); + let edit = editor.finish(); + assert_eq!(edit.new_root().to_string(), expected); +} From d873a79b8a310cb6c18f3a96edcfd050a5ed0e81 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 3 Jun 2026 08:53:16 +0530 Subject: [PATCH 53/57] bye bye ted --- .../rust-analyzer/crates/syntax/src/lib.rs | 1 - .../rust-analyzer/crates/syntax/src/ted.rs | 227 ------------------ 2 files changed, 228 deletions(-) delete mode 100644 src/tools/rust-analyzer/crates/syntax/src/ted.rs diff --git a/src/tools/rust-analyzer/crates/syntax/src/lib.rs b/src/tools/rust-analyzer/crates/syntax/src/lib.rs index cda3e69b7c625..924e72ee40397 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/lib.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/lib.rs @@ -39,7 +39,6 @@ pub mod ast; pub mod fuzz; pub mod hacks; pub mod syntax_editor; -pub mod ted; pub mod utils; use std::{marker::PhantomData, ops::Range}; diff --git a/src/tools/rust-analyzer/crates/syntax/src/ted.rs b/src/tools/rust-analyzer/crates/syntax/src/ted.rs deleted file mode 100644 index 5c286479c4e3d..0000000000000 --- a/src/tools/rust-analyzer/crates/syntax/src/ted.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Primitive tree editor, ed for trees. -//! -//! The `_raw`-suffixed functions insert elements as is, unsuffixed versions fix -//! up elements around the edges. -use std::{mem, ops::RangeInclusive}; - -use parser::T; -use rowan::TextSize; - -use crate::{ - SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, - ast::{self, AstNode, edit::IndentLevel, make}, -}; - -/// Utility trait to allow calling `ted` functions with references or owned -/// nodes. Do not use outside of this module. -pub trait Element { - fn syntax_element(self) -> SyntaxElement; -} - -impl Element for &'_ E { - fn syntax_element(self) -> SyntaxElement { - self.clone().syntax_element() - } -} -impl Element for SyntaxElement { - fn syntax_element(self) -> SyntaxElement { - self - } -} -impl Element for SyntaxNode { - fn syntax_element(self) -> SyntaxElement { - self.into() - } -} -impl Element for SyntaxToken { - fn syntax_element(self) -> SyntaxElement { - self.into() - } -} - -#[derive(Debug)] -pub struct Position { - repr: PositionRepr, -} - -#[derive(Debug)] -enum PositionRepr { - FirstChild(SyntaxNode), - After(SyntaxElement), -} - -impl Position { - pub fn after(elem: impl Element) -> Position { - let repr = PositionRepr::After(elem.syntax_element()); - Position { repr } - } - pub fn before(elem: impl Element) -> Position { - let elem = elem.syntax_element(); - let repr = match elem.prev_sibling_or_token() { - Some(it) => PositionRepr::After(it), - None => PositionRepr::FirstChild(elem.parent().unwrap()), - }; - Position { repr } - } - pub fn first_child_of(node: &(impl Into + Clone)) -> Position { - let repr = PositionRepr::FirstChild(node.clone().into()); - Position { repr } - } - pub fn last_child_of(node: &(impl Into + Clone)) -> Position { - let node = node.clone().into(); - let repr = match node.last_child_or_token() { - Some(it) => PositionRepr::After(it), - None => PositionRepr::FirstChild(node), - }; - Position { repr } - } - pub fn offset(&self) -> TextSize { - match &self.repr { - PositionRepr::FirstChild(node) => node.text_range().start(), - PositionRepr::After(elem) => elem.text_range().end(), - } - } -} - -pub fn insert(position: Position, elem: impl Element) { - insert_all(position, vec![elem.syntax_element()]); -} -pub fn insert_raw(position: Position, elem: impl Element) { - insert_all_raw(position, vec![elem.syntax_element()]); -} -pub fn insert_all(position: Position, mut elements: Vec) { - if let Some(first) = elements.first() - && let Some(ws) = ws_before(&position, first) - { - elements.insert(0, ws.into()); - } - if let Some(last) = elements.last() - && let Some(ws) = ws_after(&position, last) - { - elements.push(ws.into()); - } - insert_all_raw(position, elements); -} -pub fn insert_all_raw(position: Position, elements: Vec) { - let (parent, index) = match position.repr { - PositionRepr::FirstChild(parent) => (parent, 0), - PositionRepr::After(child) => (child.parent().unwrap(), child.index() + 1), - }; - parent.splice_children(index..index, elements); -} - -pub fn remove(elem: impl Element) { - elem.syntax_element().detach(); -} -pub fn remove_all(range: RangeInclusive) { - replace_all(range, Vec::new()); -} -pub fn remove_all_iter(range: impl IntoIterator) { - let mut it = range.into_iter(); - if let Some(mut first) = it.next() { - match it.last() { - Some(mut last) => { - if first.index() > last.index() { - mem::swap(&mut first, &mut last); - } - remove_all(first..=last); - } - None => remove(first), - } - } -} - -pub fn replace(old: impl Element, new: impl Element) { - replace_with_many(old, vec![new.syntax_element()]); -} -pub fn replace_with_many(old: impl Element, new: Vec) { - let old = old.syntax_element(); - replace_all(old.clone()..=old, new); -} -pub fn replace_all(range: RangeInclusive, new: Vec) { - let start = range.start().index(); - let end = range.end().index(); - let parent = range.start().parent().unwrap(); - parent.splice_children(start..end + 1, new); -} - -pub fn append_child(node: &(impl Into + Clone), child: impl Element) { - let position = Position::last_child_of(node); - insert(position, child); -} -pub fn append_child_raw(node: &(impl Into + Clone), child: impl Element) { - let position = Position::last_child_of(node); - insert_raw(position, child); -} - -pub fn prepend_child(node: &(impl Into + Clone), child: impl Element) { - let position = Position::first_child_of(node); - insert(position, child); -} - -fn ws_before(position: &Position, new: &SyntaxElement) -> Option { - let prev = match &position.repr { - PositionRepr::FirstChild(_) => return None, - PositionRepr::After(it) => it, - }; - - if prev.kind() == T!['{'] - && new.kind() == SyntaxKind::USE - && let Some(item_list) = prev.parent().and_then(ast::ItemList::cast) - { - let mut indent = IndentLevel::from_element(&item_list.syntax().clone().into()); - indent.0 += 1; - return Some(make::tokens::whitespace(&format!("\n{indent}"))); - } - - if prev.kind() == T!['{'] - && ast::Stmt::can_cast(new.kind()) - && let Some(stmt_list) = prev.parent().and_then(ast::StmtList::cast) - { - let mut indent = IndentLevel::from_element(&stmt_list.syntax().clone().into()); - indent.0 += 1; - return Some(make::tokens::whitespace(&format!("\n{indent}"))); - } - - ws_between(prev, new) -} -fn ws_after(position: &Position, new: &SyntaxElement) -> Option { - let next = match &position.repr { - PositionRepr::FirstChild(parent) => parent.first_child_or_token()?, - PositionRepr::After(sibling) => sibling.next_sibling_or_token()?, - }; - ws_between(new, &next) -} -fn ws_between(left: &SyntaxElement, right: &SyntaxElement) -> Option { - if left.kind() == SyntaxKind::WHITESPACE || right.kind() == SyntaxKind::WHITESPACE { - return None; - } - if right.kind() == T![;] || right.kind() == T![,] { - return None; - } - if left.kind() == T![<] || right.kind() == T![>] { - return None; - } - if left.kind() == T![&] && right.kind() == SyntaxKind::LIFETIME { - return None; - } - if right.kind() == SyntaxKind::GENERIC_ARG_LIST { - return None; - } - - if right.kind() == SyntaxKind::USE { - let mut indent = IndentLevel::from_element(left); - if left.kind() == SyntaxKind::USE { - indent.0 = IndentLevel::from_element(right).0.max(indent.0); - } - return Some(make::tokens::whitespace(&format!("\n{indent}"))); - } - if left.kind() == SyntaxKind::ATTR { - let mut indent = IndentLevel::from_element(right); - if right.kind() == SyntaxKind::ATTR { - indent.0 = IndentLevel::from_element(left).0.max(indent.0); - } - return Some(make::tokens::whitespace(&format!("\n{indent}"))); - } - Some(make::tokens::single_space()) -} From 8dae4577de433e1dbe30c77bb81023784ee9d43d Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Sun, 21 Jun 2026 00:28:08 +0800 Subject: [PATCH 54/57] internal: add merge imports indent tests --- .../ide-assists/src/handlers/merge_imports.rs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs index 5d81f49b18507..4234a6090dc3f 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/merge_imports.rs @@ -535,6 +535,62 @@ use foo::{bar, baz}; ); } + #[test] + fn mod_indent_whitespace() { + check_assist( + merge_imports, + r" +mod tests { + use foo$0::bar; + use foo::baz; + fn feature() {} +} +", + r" +mod tests { + use foo::{bar, baz}; + fn feature() {} +} +", + ); + check_assist( + merge_imports, + r" +mod tests { + use foo$0::bar; + use foo::baz; + + fn feature() {} +} +", + r" +mod tests { + use foo::{bar, baz}; + + fn feature() {} +} +", + ); + check_assist( + merge_imports, + r" +mod tests { + use foo::bar; + use foo$0::baz; + + fn feature() {} +} +", + r" +mod tests { + use foo::{bar, baz}; + + fn feature() {} +} +", + ); + } + #[test] fn works_with_trailing_comma() { check_assist( From b93879b81c9529155b4fa68885f0ab882f867f89 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 20 Jun 2026 18:28:13 +0200 Subject: [PATCH 55/57] Do not visit nodes in GC multiple times --- .../crates/hir-ty/src/next_solver/interner.rs | 4 +- .../rust-analyzer/crates/intern/Cargo.toml | 1 - .../rust-analyzer/crates/intern/src/gc.rs | 44 +++++++++++++------ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs b/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs index 9875a915dcdda..ef626fc0c8869 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/next_solver/interner.rs @@ -2629,8 +2629,8 @@ macro_rules! impl_gc_visit_slice { } #[inline] - fn visit_slice(header: &[::SliceType], gc: &mut ::intern::GarbageCollector) { - header.generic_visit_with(gc); + fn visit_slice(slice: &[::SliceType], gc: &mut ::intern::GarbageCollector) { + slice.generic_visit_with(gc); } } )* diff --git a/src/tools/rust-analyzer/crates/intern/Cargo.toml b/src/tools/rust-analyzer/crates/intern/Cargo.toml index 2ba802f05706c..adf49ca7b6c29 100644 --- a/src/tools/rust-analyzer/crates/intern/Cargo.toml +++ b/src/tools/rust-analyzer/crates/intern/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true [lib] doctest = false - [dependencies] dashmap.workspace = true # We need to freeze the version of the crate, as it needs to match with dashmap diff --git a/src/tools/rust-analyzer/crates/intern/src/gc.rs b/src/tools/rust-analyzer/crates/intern/src/gc.rs index f4e8f75e7194b..596b05eb1cb13 100644 --- a/src/tools/rust-analyzer/crates/intern/src/gc.rs +++ b/src/tools/rust-analyzer/crates/intern/src/gc.rs @@ -34,9 +34,7 @@ impl Storage for InternedStorage { for item in storage { let item = item.key(); let addr = Arc::as_ptr(item).addr(); - if Arc::strong_count(item) > 1 { - // The item is referenced from the outside. - gc.alive.insert(addr); + if Arc::strong_count(item) > 1 && gc.alive.insert(addr) { item.visit_with(gc); } } @@ -60,9 +58,7 @@ impl Storage for InternedSliceStorage for item in storage { let item = item.key(); let addr = ThinArc::as_ptr(item).addr(); - if ThinArc::strong_count(item) > 1 { - // The item is referenced from the outside. - gc.alive.insert(addr); + if ThinArc::strong_count(item) > 1 && gc.alive.insert(addr) { T::visit_header(&item.header.header, gc); T::visit_slice(&item.slice, gc); } @@ -81,7 +77,7 @@ pub trait GcInternedVisit { pub trait GcInternedSliceVisit: SliceInternable { fn visit_header(header: &Self::Header, gc: &mut GarbageCollector); - fn visit_slice(header: &[Self::SliceType], gc: &mut GarbageCollector); + fn visit_slice(slice: &[Self::SliceType], gc: &mut GarbageCollector); } #[derive(Default)] @@ -103,11 +99,13 @@ impl GarbageCollector { self.storages.push(&InternedSliceStorage::(PhantomData)); } + /// Collects unreachable GC-managed interned values. + /// /// # Safety /// /// - This cannot be called if there are some not-yet-recorded type values. - /// - All relevant storages must have been added; that is, within the full graph of values, - /// the added storages must form a DAG. + /// - All storages that can contain live GC-managed values must have been added, and those + /// storages must be closed over the GC-managed values reachable from them. /// - [`GcInternedVisit`] and [`GcInternedSliceVisit`] must mark all values reachable from the node. pub unsafe fn collect(mut self) { if cfg!(feature = "prevent-gc") { @@ -136,8 +134,9 @@ impl GarbageCollector { &mut self, interned: InternedRef<'_, T>, ) -> ControlFlow<()> { + const { assert!(T::USE_GC) }; + if interned.strong_count() > 1 { - // It will be visited anyway, so short-circuit return ControlFlow::Break(()); } let addr = interned.as_raw().addr(); @@ -148,8 +147,9 @@ impl GarbageCollector { &mut self, interned: InternedSliceRef<'_, T>, ) -> ControlFlow<()> { + const { assert!(T::USE_GC) }; + if interned.strong_count() > 1 { - // It will be visited anyway, so short-circuit return ControlFlow::Break(()); } let addr = interned.as_raw().addr(); @@ -240,7 +240,7 @@ mod tests { impl GcInternedSliceVisit for StringSlice { fn visit_header(_header: &Self::Header, _gc: &mut GarbageCollector) {} - fn visit_slice(_header: &[Self::SliceType], _gc: &mut GarbageCollector) {} + fn visit_slice(_slice: &[Self::SliceType], _gc: &mut GarbageCollector) {} } let (a, d) = { @@ -276,6 +276,10 @@ mod tests { gc.add_storage::(); unsafe { gc.collect() }; + if !cfg!(feature = "prevent-gc") { + assert_eq!(::storage().get().len(), 1); + assert_eq!(::storage().get().len(), 1); + } assert_eq!(a.0, "abc"); assert_eq!(d.header.length, 2); assert_eq!(d.header.header, "abc"); @@ -288,6 +292,11 @@ mod tests { gc.add_slice_storage::(); gc.add_storage::(); unsafe { gc.collect() }; + + if !cfg!(feature = "prevent-gc") { + assert_eq!(::storage().get().len(), 0); + assert_eq!(::storage().get().len(), 0); + } } #[test] @@ -309,7 +318,7 @@ mod tests { impl GcInternedSliceVisit for StringSlice { fn visit_header(_header: &Self::Header, _gc: &mut GarbageCollector) {} - fn visit_slice(_header: &[Self::SliceType], _gc: &mut GarbageCollector) {} + fn visit_slice(_slice: &[Self::SliceType], _gc: &mut GarbageCollector) {} } let outer = { @@ -322,6 +331,10 @@ mod tests { gc.add_storage::(); unsafe { gc.collect() }; + if !cfg!(feature = "prevent-gc") { + assert_eq!(::storage().get().len(), 1); + assert_eq!(::storage().get().len(), 1); + } assert_eq!(outer.0.header.header, "abc"); assert_eq!(outer.0.slice, [123, 456, 789]); @@ -331,5 +344,10 @@ mod tests { gc.add_slice_storage::(); gc.add_storage::(); unsafe { gc.collect() }; + + if !cfg!(feature = "prevent-gc") { + assert_eq!(::storage().get().len(), 0); + assert_eq!(::storage().get().len(), 0); + } } } From fc7e417bf6e69250a8e48024a07500ae29f1e6d0 Mon Sep 17 00:00:00 2001 From: Ada Alakbarova Date: Thu, 18 Jun 2026 17:02:50 +0200 Subject: [PATCH 56/57] fix(assists/replace_match_with_if_let): don't parenthesize if-let guards --- .../src/handlers/replace_if_let_with_match.rs | 73 ++++++++++++++++++- .../crates/ide-assists/src/utils.rs | 20 +++++ .../crates/syntax/src/ast/prec.rs | 23 ++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs index 6959988db7f03..5225202177111 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_if_let_with_match.rs @@ -17,7 +17,7 @@ use crate::{ AssistContext, AssistId, Assists, utils::{ does_pat_match_variant, does_pat_variant_nested_or_literal, unwrap_trivial_block, - wrap_paren, + wrap_paren_in_guard_chain, }, }; @@ -303,7 +303,7 @@ pub(crate) fn replace_match_with_if_let( _ => make.expr_let(if_let_pat, scrutinee).into(), }; let condition = if let Some(guard) = guard { - let guard = wrap_paren(guard, make, ast::prec::ExprPrecedence::LAnd); + let guard = wrap_paren_in_guard_chain(guard, make); make.expr_bin(condition, ast::BinaryOp::LogicOp(ast::LogicOp::And), guard).into() } else { condition @@ -2454,7 +2454,7 @@ fn main() { } #[test] - fn test_replace_match_with_if_let_chain() { + fn test_replace_match_with_if_let_with_simple_guard() { check_assist( replace_match_with_if_let, r#" @@ -2498,6 +2498,73 @@ fn main() { ); } + #[test] + fn test_replace_match_with_if_let_with_if_let_guard() { + check_assist( + replace_match_with_if_let, + r#" +fn main() { + match$0 Some(0) { + Some(n) if let Some(m) = n.checked_add(1) => (), + _ => code(), + } +} +"#, + r#" +fn main() { + if let Some(n) = Some(0) && let Some(m) = n.checked_add(1) { + () + } else { + code() + } +} +"#, + ); + + check_assist( + replace_match_with_if_let, + r#" +fn main() { + match$0 Some(0) { + Some(n) if let Some(m) = n.checked_add(1) && m > 5 => (), + _ => code(), + } +} + "#, + r#" +fn main() { + if let Some(n) = Some(0) && let Some(m) = n.checked_add(1) && m > 5 { + () + } else { + code() + } +} + "#, + ); + + // what if the `let` expr is not the first one in the guard? + check_assist( + replace_match_with_if_let, + r#" +fn main() { + match$0 Some(0) { + Some(n) if n > 5 && let Some(m) = n.checked_add(1) => (), + _ => code(), + } +} + "#, + r#" +fn main() { + if let Some(n) = Some(0) && n > 5 && let Some(m) = n.checked_add(1) { + () + } else { + code() + } +} + "#, + ); + } + #[test] fn test_replace_match_with_if_let_not_applicable_pat2_is_ident_pat() { check_assist_not_applicable( diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs b/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs index 086f54ed17f69..2c4fb5f405a77 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/utils.rs @@ -110,6 +110,26 @@ fn needs_parens_in_call(make: &SyntaxFactory, param: &ast::Expr) -> bool { param.needs_parens_in_place_of(call.syntax(), callable.syntax()) } +pub(crate) fn wrap_paren_in_guard_chain(guard: ast::Expr, make: &SyntaxFactory) -> ast::Expr { + if needs_parens_in_guard_chain(make, &guard) { make.expr_paren(guard).into() } else { guard } +} + +fn needs_parens_in_guard_chain(make: &SyntaxFactory, guard: &ast::Expr) -> bool { + let ast::Expr::BinExpr(if_let_and_guard) = make.expr_bin_op( + make.expr_unit(), + ast::BinaryOp::LogicOp(ast::LogicOp::And), + make.expr_unit(), + ) else { + stdx::never!("`SyntaxFactory::expr_bin_op` returns a `BinExpr`"); + return false; + }; + let Some(fake_guard) = if_let_and_guard.rhs() else { + stdx::never!("invalid make call"); + return false; + }; + guard.needs_parens_in_place_of(if_let_and_guard.syntax(), fake_guard.syntax()) +} + /// This is a method with a heuristics to support test methods annotated with custom test annotations, such as /// `#[test_case(...)]`, `#[tokio::test]` and similar. /// Also a regular `#[test]` annotation is supported. diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/prec.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/prec.rs index 8411275350542..2a50d233c3bfb 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/prec.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/prec.rs @@ -264,6 +264,14 @@ impl Expr { return false; } + // Special-case `cond && ` + if let ast::Expr::BinExpr(parent) = parent + && parent.op_kind() == Some(ast::BinaryOp::LogicOp(ast::LogicOp::And)) + && self.contains_let_expr() + { + return false; + } + let (left, right, inv) = match self.is_ordered_before_parent_in_place_of(parent, place_of) { true => (self, parent, false), false => (parent, self, true), @@ -551,4 +559,19 @@ impl Expr { ForExpr(_) | IfExpr(_) | MatchExpr(_) | WhileExpr(_) | IncludeBytesExpr(_) => true, } } + + fn contains_let_expr(&self) -> bool { + use Expr::*; + + match self { + LetExpr(_) => true, + BinExpr(e) => { + // if we find something other than a `&&`, then this can't be a let chain + e.op_kind() == Some(ast::BinaryOp::LogicOp(ast::LogicOp::And)) + && (e.lhs().is_none_or(|it| it.contains_let_expr()) + || e.rhs().is_none_or(|it| it.contains_let_expr())) + } + _ => false, + } + } } From 9b6c684064e3ce89a966836e29573050a8168c85 Mon Sep 17 00:00:00 2001 From: The rustc-josh-sync Cronjob Bot Date: Mon, 22 Jun 2026 06:00:38 +0000 Subject: [PATCH 57/57] Prepare for merging from rust-lang/rust This updates the rust-version file to 942ac9ce4116d4ea784c9882659372b34978b1f8. --- src/tools/rust-analyzer/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/rust-version b/src/tools/rust-analyzer/rust-version index 5bdff8eb64eec..5db47ca8fc59b 100644 --- a/src/tools/rust-analyzer/rust-version +++ b/src/tools/rust-analyzer/rust-version @@ -1 +1 @@ -485ec3fbcc12fa14ef6596dabb125ad710499c9e +942ac9ce4116d4ea784c9882659372b34978b1f8