diff --git a/editors/code/src/ast_inspector.ts b/editors/code/src/ast_inspector.ts index 176040120f4..fa963d8eb99 100644 --- a/editors/code/src/ast_inspector.ts +++ b/editors/code/src/ast_inspector.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { Ctx, Disposable } from "./ctx"; import { RustEditor, isRustEditor } from "./util"; +import { unwrapUndefinable } from "./undefinable"; // FIXME: consider implementing this via the Tree View API? // https://code.visualstudio.com/api/extension-guides/tree-view @@ -164,8 +165,9 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv if (!parsedRange) return; const [begin, end] = parsedRange.slice(1).map((off) => this.positionAt(doc, +off)); - - return new vscode.Range(begin, end); + const actualBegin = unwrapUndefinable(begin); + const actualEnd = unwrapUndefinable(end); + return new vscode.Range(actualBegin, actualEnd); } // Memoize the last value, otherwise the CPU is at 100% single core diff --git a/editors/code/src/bootstrap.ts b/editors/code/src/bootstrap.ts index b38fa06a85c..7b831a8a695 100644 --- a/editors/code/src/bootstrap.ts +++ b/editors/code/src/bootstrap.ts @@ -36,7 +36,7 @@ async function getServer( config: Config, state: PersistentState ): Promise { - const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; + const explicitPath = process.env["__RA_LSP_SERVER_DEBUG"] ?? config.serverPath; if (explicitPath) { if (explicitPath.startsWith("~/")) { return os.homedir() + explicitPath.slice("~".length); diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index f721fcce766..7b151c804af 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -9,6 +9,7 @@ import { WorkspaceEdit } from "vscode"; import { Config, prepareVSCodeConfig } from "./config"; import { randomUUID } from "crypto"; import { sep as pathSeparator } from "path"; +import { unwrapUndefinable } from "./undefinable"; export interface Env { [name: string]: string; @@ -323,10 +324,12 @@ export async function createClient( } for (const [group, { index, items }] of groups) { if (items.length === 1) { - result[index] = items[0]; + const item = unwrapUndefinable(items[0]); + result[index] = item; } else { const action = new vscode.CodeAction(group); - action.kind = items[0].kind; + const item = unwrapUndefinable(items[0]); + action.kind = item.kind; action.command = { command: "rust-analyzer.applyActionGroup", title: "", diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 98ccd50dc04..3c6105e89fe 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -20,6 +20,7 @@ import { startDebugSession, makeDebugConfig } from "./debug"; import { LanguageClient } from "vscode-languageclient/node"; import { LINKED_COMMANDS } from "./client"; import { DependencyId } from "./dependencies_provider"; +import { unwrapUndefinable } from "./undefinable"; export * from "./ast_inspector"; export * from "./run"; @@ -129,7 +130,8 @@ export function matchingBrace(ctx: CtxInit): Cmd { ), }); editor.selections = editor.selections.map((sel, idx) => { - const active = client.protocol2CodeConverter.asPosition(response[idx]); + const position = unwrapUndefinable(response[idx]); + const active = client.protocol2CodeConverter.asPosition(position); const anchor = sel.isEmpty ? active : sel.anchor; return new vscode.Selection(anchor, active); }); @@ -231,7 +233,7 @@ export function parentModule(ctx: CtxInit): Cmd { if (!locations) return; if (locations.length === 1) { - const loc = locations[0]; + const loc = unwrapUndefinable(locations[0]); const uri = client.protocol2CodeConverter.asUri(loc.targetUri); const range = client.protocol2CodeConverter.asRange(loc.targetRange); @@ -331,7 +333,13 @@ async function revealParentChain(document: RustDocument, ctx: CtxInit) { } while (!ctx.dependencies?.contains(documentPath)); parentChain.reverse(); for (const idx in parentChain) { - await ctx.treeView?.reveal(parentChain[idx], { select: true, expand: true }); + const treeView = ctx.treeView; + if (!treeView) { + continue; + } + + const dependency = unwrapUndefinable(parentChain[idx]); + await treeView.reveal(dependency, { select: true, expand: true }); } } diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 15a1d4e0f1d..deea958f8da 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -4,6 +4,7 @@ import * as path from "path"; import * as vscode from "vscode"; import { Env } from "./client"; import { log } from "./util"; +import { expectNotUndefined, unwrapUndefinable } from "./undefinable"; export type RunnableEnvCfg = | undefined @@ -338,7 +339,7 @@ export function substituteVariablesInEnv(env: Env): Env { const depRe = new RegExp(/\${(?.+?)}/g); let match = undefined; while ((match = depRe.exec(value))) { - const depName = match.groups!.depName; + const depName = unwrapUndefinable(match.groups?.["depName"]); deps.add(depName); // `depName` at this point can have a form of `expression` or // `prefix:expression` @@ -356,7 +357,7 @@ export function substituteVariablesInEnv(env: Env): Env { if (match) { const { prefix, body } = match.groups!; if (prefix === "env") { - const envName = body; + const envName = unwrapUndefinable(body); envWithDeps[dep] = { value: process.env[envName] ?? "", deps: [], @@ -384,13 +385,12 @@ export function substituteVariablesInEnv(env: Env): Env { do { leftToResolveSize = toResolve.size; for (const key of toResolve) { - if (envWithDeps[key].deps.every((dep) => resolved.has(dep))) { - envWithDeps[key].value = envWithDeps[key].value.replace( - /\${(?.+?)}/g, - (_wholeMatch, depName) => { - return envWithDeps[depName].value; - } - ); + const item = unwrapUndefinable(envWithDeps[key]); + if (item.deps.every((dep) => resolved.has(dep))) { + item.value = item.value.replace(/\${(?.+?)}/g, (_wholeMatch, depName) => { + const item = unwrapUndefinable(envWithDeps[depName]); + return item.value; + }); resolved.add(key); toResolve.delete(key); } @@ -399,7 +399,8 @@ export function substituteVariablesInEnv(env: Env): Env { const resolvedEnv: Env = {}; for (const key of Object.keys(env)) { - resolvedEnv[key] = envWithDeps[`env:${key}`].value; + const item = unwrapUndefinable(envWithDeps[`env:${key}`]); + resolvedEnv[key] = item.value; } return resolvedEnv; } @@ -418,20 +419,19 @@ function substituteVSCodeVariableInString(val: string): string { function computeVscodeVar(varName: string): string | null { const workspaceFolder = () => { const folders = vscode.workspace.workspaceFolders ?? []; - if (folders.length === 1) { - // TODO: support for remote workspaces? - return folders[0].uri.fsPath; - } else if (folders.length > 1) { - // could use currently opened document to detect the correct - // workspace. However, that would be determined by the document - // user has opened on Editor startup. Could lead to - // unpredictable workspace selection in practice. - // It's better to pick the first one - return folders[0].uri.fsPath; - } else { - // no workspace opened - return ""; - } + const folder = folders[0]; + // TODO: support for remote workspaces? + const fsPath: string = + folder === undefined + ? // no workspace opened + "" + : // could use currently opened document to detect the correct + // workspace. However, that would be determined by the document + // user has opened on Editor startup. Could lead to + // unpredictable workspace selection in practice. + // It's better to pick the first one + folder.uri.fsPath; + return fsPath; }; // https://code.visualstudio.com/docs/editor/variables-reference const supportedVariables: { [k: string]: () => string } = { @@ -448,13 +448,17 @@ function computeVscodeVar(varName: string): string | null { // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81 // or // https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56 - execPath: () => process.env.VSCODE_EXEC_PATH ?? process.execPath, + execPath: () => process.env["VSCODE_EXEC_PATH"] ?? process.execPath, pathSeparator: () => path.sep, }; if (varName in supportedVariables) { - return supportedVariables[varName](); + const fn = expectNotUndefined( + supportedVariables[varName], + `${varName} should not be undefined here` + ); + return fn(); } else { // return "${" + varName + "}"; return null; diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts index 8fbd427039e..29758feafe4 100644 --- a/editors/code/src/debug.ts +++ b/editors/code/src/debug.ts @@ -6,6 +6,7 @@ import * as ra from "./lsp_ext"; import { Cargo, getRustcId, getSysroot } from "./toolchain"; import { Ctx } from "./ctx"; import { prepareEnv } from "./run"; +import { unwrapUndefinable } from "./undefinable"; const debugOutput = vscode.window.createOutputChannel("Debug"); type DebugConfigProvider = ( @@ -105,12 +106,13 @@ async function getDebugConfiguration( const workspaceFolders = vscode.workspace.workspaceFolders!; const isMultiFolderWorkspace = workspaceFolders.length > 1; const firstWorkspace = workspaceFolders[0]; - const workspace = + const maybeWorkspace = !isMultiFolderWorkspace || !runnable.args.workspaceRoot ? firstWorkspace : workspaceFolders.find((w) => runnable.args.workspaceRoot?.includes(w.uri.fsPath)) || firstWorkspace; + const workspace = unwrapUndefinable(maybeWorkspace); const wsFolder = path.normalize(workspace.uri.fsPath); const workspaceQualifier = isMultiFolderWorkspace ? `:${workspace.name}` : ""; function simplifyPath(p: string): string { @@ -130,12 +132,8 @@ async function getDebugConfiguration( sourceFileMap[`/rustc/${commitHash}/`] = rustlib; } - const debugConfig = knownEngines[debugEngine.id]( - runnable, - simplifyPath(executable), - env, - sourceFileMap - ); + const provider = unwrapUndefinable(knownEngines[debugEngine.id]); + const debugConfig = provider(runnable, simplifyPath(executable), env, sourceFileMap); if (debugConfig.type in debugOptions.engineSettings) { const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; for (var key in settingsMap) { @@ -149,8 +147,9 @@ async function getDebugConfiguration( debugConfig.name = `run ${path.basename(executable)}`; } - if (debugConfig.cwd) { - debugConfig.cwd = simplifyPath(debugConfig.cwd); + const cwd = debugConfig["cwd"]; + if (cwd) { + debugConfig["cwd"] = simplifyPath(cwd); } return debugConfig; diff --git a/editors/code/src/dependencies_provider.ts b/editors/code/src/dependencies_provider.ts index d67345258ec..51ba11ecc92 100644 --- a/editors/code/src/dependencies_provider.ts +++ b/editors/code/src/dependencies_provider.ts @@ -4,6 +4,7 @@ import * as fs from "fs"; import { CtxInit } from "./ctx"; import * as ra from "./lsp_ext"; import { FetchDependencyListResult } from "./lsp_ext"; +import { unwrapUndefinable } from "./undefinable"; export class RustDependenciesProvider implements vscode.TreeDataProvider @@ -49,7 +50,12 @@ export class RustDependenciesProvider } getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable { - if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!]; + const dependenciesMap = this.dependenciesMap; + const elementId = element.id!; + if (elementId in dependenciesMap) { + const dependency = unwrapUndefinable(dependenciesMap[elementId]); + return dependency; + } return element; } diff --git a/editors/code/src/diagnostics.ts b/editors/code/src/diagnostics.ts index 9695d8bf26d..a7e0845a278 100644 --- a/editors/code/src/diagnostics.ts +++ b/editors/code/src/diagnostics.ts @@ -2,6 +2,7 @@ import * as anser from "anser"; import * as vscode from "vscode"; import { ProviderResult, Range, TextEditorDecorationType, ThemeColor, window } from "vscode"; import { Ctx } from "./ctx"; +import { unwrapUndefinable } from "./undefinable"; export const URI_SCHEME = "rust-analyzer-diagnostics-view"; @@ -195,7 +196,8 @@ export class AnsiDecorationProvider implements vscode.Disposable { // anser won't return both the RGB and the color name at the same time, // so just fake a single foreground control char with the palette number: const spans = anser.ansiToJson(`\x1b[38;5;${paletteColor}m`); - const rgb = spans[1].fg; + const span = unwrapUndefinable(spans[1]); + const rgb = span.fg; if (rgb) { return `rgb(${rgb})`; diff --git a/editors/code/src/nullable.ts b/editors/code/src/nullable.ts new file mode 100644 index 00000000000..e973e162907 --- /dev/null +++ b/editors/code/src/nullable.ts @@ -0,0 +1,19 @@ +export type NotNull = T extends null ? never : T; + +export type Nullable = T | null; + +function isNotNull(input: Nullable): input is NotNull { + return input !== null; +} + +function expectNotNull(input: Nullable, msg: string): NotNull { + if (isNotNull(input)) { + return input; + } + + throw new TypeError(msg); +} + +export function unwrapNullable(input: Nullable): NotNull { + return expectNotNull(input, `unwrapping \`null\``); +} diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts index bdd85243133..c20a5487b77 100644 --- a/editors/code/src/run.ts +++ b/editors/code/src/run.ts @@ -6,6 +6,7 @@ import * as tasks from "./tasks"; import { CtxInit } from "./ctx"; import { makeDebugConfig } from "./debug"; import { Config, RunnableEnvCfg } from "./config"; +import { unwrapUndefinable } from "./undefinable"; const quickPickButtons = [ { iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." }, @@ -68,12 +69,14 @@ export async function selectRunnable( quickPick.onDidHide(() => close()), quickPick.onDidAccept(() => close(quickPick.selectedItems[0])), quickPick.onDidTriggerButton(async (_button) => { - await makeDebugConfig(ctx, quickPick.activeItems[0].runnable); + const runnable = unwrapUndefinable(quickPick.activeItems[0]).runnable; + await makeDebugConfig(ctx, runnable); close(); }), - quickPick.onDidChangeActive((active) => { - if (showButtons && active.length > 0) { - if (active[0].label.startsWith("cargo")) { + quickPick.onDidChangeActive((activeList) => { + if (showButtons && activeList.length > 0) { + const active = unwrapUndefinable(activeList[0]); + if (active.label.startsWith("cargo")) { // save button makes no sense for `cargo test` or `cargo check` quickPick.buttons = []; } else if (quickPick.buttons.length === 0) { diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts index 299d29c27ee..1ad93d280ba 100644 --- a/editors/code/src/snippets.ts +++ b/editors/code/src/snippets.ts @@ -1,10 +1,11 @@ import * as vscode from "vscode"; import { assert } from "./util"; +import { unwrapUndefinable } from "./undefinable"; export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) { if (edit.entries().length === 1) { - const [uri, edits] = edit.entries()[0]; + const [uri, edits] = unwrapUndefinable(edit.entries()[0]); const editor = await editorFromUri(uri); if (editor) await applySnippetTextEdits(editor, edits); return; @@ -68,7 +69,8 @@ export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vs }); if (selections.length > 0) editor.selections = selections; if (selections.length === 1) { - editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenterIfOutsideViewport); + const selection = unwrapUndefinable(selections[0]); + editor.revealRange(selection, vscode.TextEditorRevealType.InCenterIfOutsideViewport); } } diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts index efb889dc797..5199508c822 100644 --- a/editors/code/src/tasks.ts +++ b/editors/code/src/tasks.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import * as toolchain from "./toolchain"; import { Config } from "./config"; import { log } from "./util"; +import { unwrapUndefinable } from "./undefinable"; // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and // our configuration should be compatible with it so use the same key. @@ -120,7 +121,8 @@ export async function buildCargoTask( const fullCommand = [...cargoCommand, ...args]; - exec = new vscode.ProcessExecution(fullCommand[0], fullCommand.slice(1), definition); + const processName = unwrapUndefinable(fullCommand[0]); + exec = new vscode.ProcessExecution(processName, fullCommand.slice(1), definition); } return new vscode.Task( diff --git a/editors/code/src/toolchain.ts b/editors/code/src/toolchain.ts index 917a1d6b099..014e6b66f64 100644 --- a/editors/code/src/toolchain.ts +++ b/editors/code/src/toolchain.ts @@ -4,6 +4,8 @@ import * as path from "path"; import * as readline from "readline"; import * as vscode from "vscode"; import { execute, log, memoizeAsync } from "./util"; +import { unwrapNullable } from "./nullable"; +import { unwrapUndefinable } from "./undefinable"; interface CompilationArtifact { fileName: string; @@ -93,7 +95,8 @@ export class Cargo { throw new Error("Multiple compilation artifacts are not supported."); } - return artifacts[0].fileName; + const artifact = unwrapUndefinable(artifacts[0]); + return artifact.fileName; } private async runCargo( @@ -142,7 +145,9 @@ export async function getRustcId(dir: string): Promise { const data = await execute(`${rustcPath} -V -v`, { cwd: dir }); const rx = /commit-hash:\s(.*)$/m; - return rx.exec(data)![1]; + const result = unwrapNullable(rx.exec(data)); + const first = unwrapUndefinable(result[1]); + return first; } /** Mirrors `toolchain::cargo()` implementation */ @@ -171,7 +176,7 @@ export const getPathForExecutable = memoizeAsync( ); async function lookupInPath(exec: string): Promise { - const paths = process.env.PATH ?? ""; + const paths = process.env["PATH"] ?? ""; const candidates = paths.split(path.delimiter).flatMap((dirInPath) => { const candidate = path.join(dirInPath, exec); diff --git a/editors/code/src/undefinable.ts b/editors/code/src/undefinable.ts new file mode 100644 index 00000000000..813bac5a123 --- /dev/null +++ b/editors/code/src/undefinable.ts @@ -0,0 +1,19 @@ +export type NotUndefined = T extends undefined ? never : T; + +export type Undefinable = T | undefined; + +function isNotUndefined(input: Undefinable): input is NotUndefined { + return input !== undefined; +} + +export function expectNotUndefined(input: Undefinable, msg: string): NotUndefined { + if (isNotUndefined(input)) { + return input; + } + + throw new TypeError(msg); +} + +export function unwrapUndefinable(input: Undefinable): NotUndefined { + return expectNotUndefined(input, `unwrapping \`undefined\``); +} diff --git a/editors/code/tests/unit/settings.test.ts b/editors/code/tests/unit/settings.test.ts index 2cc1b670dc8..f171f5f74f3 100644 --- a/editors/code/tests/unit/settings.test.ts +++ b/editors/code/tests/unit/settings.test.ts @@ -57,7 +57,7 @@ export async function getTests(ctx: Context) { USING_VSCODE_VAR: "${workspaceFolderBasename}", }; const actualEnv = await substituteVariablesInEnv(envJson); - assert.deepStrictEqual(actualEnv.USING_VSCODE_VAR, "code"); + assert.deepStrictEqual(actualEnv["USING_VSCODE_VAR"], "code"); }); }); } diff --git a/editors/code/tsconfig.json b/editors/code/tsconfig.json index 04bd4569377..4b107a5d251 100644 --- a/editors/code/tsconfig.json +++ b/editors/code/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "esModuleInterop": false, "module": "commonjs", + "moduleResolution": "node16", "target": "es2021", "outDir": "out", "lib": ["es2021"], @@ -12,9 +13,7 @@ // These disables some enhancement type checking options // to update typescript version without any code change. "useUnknownInCatchVariables": false, - "exactOptionalPropertyTypes": false, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": false + "exactOptionalPropertyTypes": false }, "exclude": ["node_modules", ".vscode-test"], "include": ["src", "tests"]