From c7aed9dbb961137660e8423811b8e5c08938de46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Mon, 29 Apr 2019 23:26:16 +0900 Subject: [PATCH 1/3] Initial work for debugger --- package-lock.json | 113 ++++++-- package.json | 8 +- src/debugger/ask.ts | 191 ++++++++++++ src/debugger/cargo/build.ts | 102 +++++++ src/debugger/cargo/ext.ts | 21 ++ src/debugger/cargo/index.ts | 47 +++ src/debugger/cargo/metadata.ts | 210 ++++++++++++++ src/debugger/cargo/package.ts | 36 +++ src/debugger/cargo/resolver.ts | 15 + src/debugger/cargo/task_factory.ts | 68 +++++ src/debugger/cargo/task_provider.ts | 57 ++++ src/debugger/cargo/workspace.ts | 41 +++ src/debugger/cargo/workspace_factory.ts | 95 ++++++ src/debugger/index.ts | 345 ++++++++++++++++++++++ src/debugger/rustc/cfg.ts | 22 ++ src/debugger/rustc/factory.ts | 17 ++ src/debugger/rustc/rustc.ts | 39 +++ src/debugger/util/cli.ts | 86 ++++++ src/debugger/util/context.ts | 49 ++++ src/debugger/util/index.ts | 370 ++++++++++++++++++++++++ src/extension.ts | 32 +- tsconfig.json | 9 +- 22 files changed, 1943 insertions(+), 30 deletions(-) create mode 100644 src/debugger/ask.ts create mode 100644 src/debugger/cargo/build.ts create mode 100644 src/debugger/cargo/ext.ts create mode 100644 src/debugger/cargo/index.ts create mode 100644 src/debugger/cargo/metadata.ts create mode 100644 src/debugger/cargo/package.ts create mode 100644 src/debugger/cargo/resolver.ts create mode 100644 src/debugger/cargo/task_factory.ts create mode 100644 src/debugger/cargo/task_provider.ts create mode 100644 src/debugger/cargo/workspace.ts create mode 100644 src/debugger/cargo/workspace_factory.ts create mode 100644 src/debugger/index.ts create mode 100644 src/debugger/rustc/cfg.ts create mode 100644 src/debugger/rustc/factory.ts create mode 100644 src/debugger/rustc/rustc.ts create mode 100644 src/debugger/util/cli.ts create mode 100644 src/debugger/util/context.ts create mode 100644 src/debugger/util/index.ts diff --git a/package-lock.json b/package-lock.json index 422b6fdb..666b361d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,38 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/jsonstream": { + "version": "0.8.30", + "resolved": "https://registry.npmjs.org/@types/jsonstream/-/jsonstream-0.8.30.tgz", + "integrity": "sha512-KqHs2eAapKL7ZKUiKI/giUYPVgkoDXkVGFehk3goo+3Q8qwxVVRC3iwg+hK/THORbcri4RRxTtlm3JoSY1KZLQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, "@types/mocha": { "version": "2.2.43", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.43.tgz", @@ -16,6 +48,12 @@ "integrity": "sha1-YGZR0/iovsCLjLJiFhqrkgn0op0=", "dev": true }, + "@types/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-1.3.1.tgz", + "integrity": "sha512-ZrJDWpvg75LTGX4XwuneY9s6bF3OeZcGTpoGh3zDV9ytzcHMFsRrMIaLBRJZQMBoGyKs6unBQfVdrLZiYfb1zQ==", + "dev": true + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -214,8 +252,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -245,7 +282,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -400,8 +436,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "convert-source-map": { "version": "1.6.0", @@ -741,8 +776,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fstream": { "version": "1.0.11", @@ -772,10 +806,9 @@ } }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "dev": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1122,7 +1155,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -1131,8 +1163,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "is": { "version": "3.3.0", @@ -1231,6 +1262,11 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1283,6 +1319,20 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "jsonparse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.0.0.tgz", + "integrity": "sha1-JiL05mwI4arH7b63YFPJt+EhH3Y=" + }, + "jsonstream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/jsonstream/-/jsonstream-1.0.3.tgz", + "integrity": "sha1-/y1JxPR5tbvN+fnlbIQc+H8O+h0=", + "requires": { + "jsonparse": "~1.0.0", + "through": ">=2.2.7 <3" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -1410,7 +1460,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1519,7 +1568,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -1608,8 +1656,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { "version": "1.0.6", @@ -2022,8 +2069,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.5", @@ -2502,6 +2548,22 @@ "he": "1.1.1", "mkdirp": "0.5.1", "supports-color": "4.4.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "supports-color": { @@ -2554,11 +2616,18 @@ "underscore": "^1.8.3" } }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xtend": { "version": "4.0.1", diff --git a/package.json b/package.json index e3e4e47d..49d75cb5 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,17 @@ "installDevExtension": "npm install && ./node_modules/.bin/vsce package -o ./out/rls-vscode-dev.vsix && code --install-extension ./out/rls-vscode-dev.vsix" }, "dependencies": { - "vscode-languageclient": "^4.3.0" + "glob": "^7.1.3", + "jsonstream": "^1.0.3", + "vscode-languageclient": "^4.3.0", + "which": "^1.3.1" }, "devDependencies": { + "@types/glob": "^7.1.1", + "@types/jsonstream": "^0.8.30", "@types/mocha": "^2.2.43", "@types/node": "~10.1.0", + "@types/which": "^1.3.1", "prettier": "^1.16.4", "tslint": "^5.14.0", "tslint-config-prettier": "^1.18.0", diff --git a/src/debugger/ask.ts b/src/debugger/ask.ts new file mode 100644 index 00000000..08f1d5f7 --- /dev/null +++ b/src/debugger/ask.ts @@ -0,0 +1,191 @@ +import { Factory } from './util'; +import CargoWorkspace from './cargo/workspace'; +import { Context } from './util/context'; +import { QuickPickItem, window } from 'vscode'; +import { Buildable } from './cargo/Build'; +import { relative } from 'path'; + +/** + * Try to get manifest directory of currently opened file. + */ +async function getCurrentManifestDir( + ctx: Context, + cargoWorkspace: Factory, +): Promise { + const editor = window.activeTextEditor; + if (!editor) { + return; + } + + return await cargoWorkspace + .get(ctx) + .then(cargoWs => cargoWs.getManifestDir(editor.document.uri)); +} + +type CrateQuickPickItem = QuickPickItem & { pkgId: string }; + +export async function askCrate( + ctx: Context, + cargoWorkspace: Factory, +): Promise { + const curManifestDir: string | undefined = await getCurrentManifestDir( + ctx, + cargoWorkspace, + ); + + const items: Promise = cargoWorkspace + .get(ctx) + .then(ws => + ws.packages + .filter(pkg => pkg.isMember) + .sort((l, r) => { + // If editor is opened, show the current crate first. + + if (curManifestDir) { + if (l.manifest_dir === curManifestDir) { + return -1; + } + if (r.manifest_dir === curManifestDir) { + return 1; + } + } + return l.name.localeCompare(r.name); + }) + .map( + (pkg): CrateQuickPickItem => { + const cratePath = relative(ctx.ws.uri.fsPath, pkg.manifest_dir); + + return { + label: `${pkg.name} ${pkg.version}`, + description: '', + detail: cratePath, + pkgId: pkg.name, + }; + }, + ), + ); + + const s = await window.showQuickPick(items, { + matchOnDescription: true, + matchOnDetail: false, + + placeHolder: `Select crate`, + ignoreFocusOut: true, + }); + + if (!s) { + throw new Error('Crate name is required'); + } + return s.pkgId; +} + +export async function askBuildFlags( + ctx: Context, + cargoWorkspace: Factory, + crate: string, +): Promise { + const items = cargoWorkspace.get(ctx).then((ws: CargoWorkspace) => { + const pkg = ws.packages.find(pkg => pkg.name === crate); + if (!pkg) { + throw new Error(`Unknown crate ${crate}`); + } + + const items: (QuickPickItem & Buildable)[] = []; + + for (const t of pkg.targets) { + if (t.kind.length !== 1) { + throw new Error(`Unexpected target.kind ${t.kind}`); + } + const { + kind: [kind], + } = t; + if (kind === 'proc-macro') { + continue; + } + + if (kind === 'lib') { + items.push({ + label: `Library`, + + description: 'Run tests in library file', + detail: 'cargo test --lib', + pkg, + get buildFlags(): string[] { + return ['--lib']; + }, + }); + items.push({ + label: `Library (doc)`, + + description: 'Run doc-tests', + detail: 'cargo test --doc', + pkg, + get buildFlags(): string[] { + return ['--doc']; + }, + }); + } else if (kind === 'bin') { + items.push({ + label: `${t.name} (binary)`, + + description: '', + detail: `cargo run --bin `, + + pkg, + get buildFlags(): string[] { + return ['--bin', t.name]; + }, + }); + } else if (kind === 'test') { + items.push({ + label: `${t.name} (test)`, + + description: '', + + pkg, + get buildFlags(): string[] { + return ['--test', t.name]; + }, + }); + } else if (kind === 'example') { + items.push({ + label: `${t.name} (example)`, + + description: '', + + pkg, + get buildFlags(): string[] { + return ['--example', t.name]; + }, + }); + } else { + items.push({ + label: `${t.name} (unknown kind '${kind}')`, + + description: '', + + pkg, + get buildFlags(): string[] { + throw new Error( + `Cannot build an executable from unknown kind '${kind}'`, + ); + }, + }); + } + } + + return items; + }); + + const s = await window.showQuickPick(items, { + matchOnDescription: true, + matchOnDetail: false, + + placeHolder: `Crate: ${crate}`, + ignoreFocusOut: true, + }); + if (!s) { + return []; + } + return s.buildFlags; +} diff --git a/src/debugger/cargo/build.ts b/src/debugger/cargo/build.ts new file mode 100644 index 00000000..85723571 --- /dev/null +++ b/src/debugger/cargo/build.ts @@ -0,0 +1,102 @@ +import { PackageId, PkgTarget } from './metadata'; +import Package from './package'; + +export interface Buildable { + /** + * - `--bin $name` + * - `--bins` + */ + readonly buildFlags: string[]; + readonly pkg: Package; +} + +// tslint:disable-next-line: no-any +export function isBuildable(node: any): node is Buildable { + return !!(node as Buildable).buildFlags && !!(node as Buildable).pkg; +} + +export interface BuildProfile { + readonly debug_assertions: boolean; + /** + * Values + * - `2`: + */ + readonly debuginfo: number; + /** `"0"` */ + readonly opt_level: string; + readonly overflow_checks: boolean; + readonly test: boolean; +} + +export interface CompilerArtifact { + readonly package_id: PackageId; + readonly reason: 'compiler-artifact'; + + readonly features: string[]; + /** + * Built files. + */ + readonly filenames: string[]; + readonly fresh: boolean; + readonly profile: BuildProfile; + + readonly target: PkgTarget; +} + +export interface BuildScriptExecuted { + readonly package_id: PackageId; + readonly reason: 'build-script-executed'; + + readonly cfgs: string[]; + readonly env: string[]; + readonly linked_libs: string[]; + readonly linked_paths: string[]; +} + +export interface CompilerMessage { + readonly package_id: PackageId; + readonly reason: 'compiler-message'; + readonly target: PkgTarget; + + readonly message: Message; +} + +export interface Message { + readonly children: Message[]; + readonly code?: { + code: string; + explanation: string | null; + } | null; + + readonly level: 'note' | string; + readonly message: string; + + readonly rendered: string | null; + readonly spans: Span[]; +} + +export interface Span { + readonly byte_end: number; + readonly byte_start: number; + readonly column_end: number; + readonly column_start: number; + // TODO + readonly expansion: any; + /** Relative */ + readonly file_name: string; + readonly is_primary: boolean; + readonly label: any; + readonly line_end: number; + readonly line_start: number; + readonly suggested_replacement: number; + readonly text: any[]; +} + +/** + * Output from `cargo build --message-format=json` + * + */ +export type BuildOutput = + | CompilerArtifact + | BuildScriptExecuted + | CompilerMessage; diff --git a/src/debugger/cargo/ext.ts b/src/debugger/cargo/ext.ts new file mode 100644 index 00000000..f198a6ff --- /dev/null +++ b/src/debugger/cargo/ext.ts @@ -0,0 +1,21 @@ +import { Disposable, workspace } from 'vscode'; +import CargoTaskProvider from './task_provider'; + +/** + * Extension for cargo. + */ +export default class CargoExt implements Disposable { + private disposable: Disposable; + + public constructor(taskProvider: CargoTaskProvider) { + const disposables: Disposable[] = []; + + disposables.push(workspace.registerTaskProvider('cargo', taskProvider)); + + this.disposable = Disposable.from(...disposables); + } + + public dispose() { + this.disposable.dispose(); + } +} diff --git a/src/debugger/cargo/index.ts b/src/debugger/cargo/index.ts new file mode 100644 index 00000000..a77154ff --- /dev/null +++ b/src/debugger/cargo/index.ts @@ -0,0 +1,47 @@ +import * as JSONStream from 'jsonstream'; + +import { ProcessBuilder, progress } from '../util'; +import { Cli } from '../util/cli'; +import { Context } from '../util/context'; +import { BuildOutput } from './build'; + +export default class Cargo extends Cli { + @progress('Building executable') + public async buildBinary( + ctx: Context, + check: boolean, + flags: string[], + opts: { + // tslint:disable-next-line: no-any + logWith?: (s: string) => any; + // tslint:disable-next-line: no-any + onStdout: (s: BuildOutput) => any; + // tslint:disable-next-line: no-any + onStderr: (s: string) => any; + }, + ): Promise { + const base = check ? ['check'] : ['test', '--no-run']; + + const proc = await new ProcessBuilder( + ctx, + this.executable, + [...base, '--message-format=json', ...flags], + {}, + ) + .logWith(opts.logWith) + .spawn(); + + return new Promise((resolve, reject) => { + proc.stdout + .pipe(JSONStream.parse(undefined)) + // tslint:disable-next-line: no-any + .on('data', (data: any) => opts.onStdout(data as BuildOutput)); + + proc.stderr.on('data', opts.onStderr); + + proc.once('error', reject); + + proc.once('exit', resolve); + }); + } +} diff --git a/src/debugger/cargo/metadata.ts b/src/debugger/cargo/metadata.ts new file mode 100644 index 00000000..1a07d3c6 --- /dev/null +++ b/src/debugger/cargo/metadata.ts @@ -0,0 +1,210 @@ +import { Disposable, Uri, workspace } from 'vscode'; + +import Cargo from '.'; +import { + CachingFactory, + Factory, + ProcessBuilder, + progress, + setContext, +} from '../util'; +import { Context } from '../util/context'; + +/** + * Output of `cargo metadata --format-version 1` + */ +export default interface Metadata { + readonly workspace_root: string | undefined; + readonly target_directory: string; + readonly packages: PackageMetadata[]; + readonly workspace_members: PackageId[]; + readonly resolve: Resolve; +} + +export class MetadataFactory extends CachingFactory { + private readonly disposable: Disposable; + + constructor(private readonly cargo: Factory) { + super([cargo]); + + const disposables: Disposable[] = []; + + const watcher = workspace.createFileSystemWatcher('**/Cargo.lock'); + disposables.push(watcher); + + watcher.onDidCreate(this.invalidateFor, this, disposables); + watcher.onDidChange(this.invalidateFor, this, disposables); + watcher.onDidDelete(this.invalidateFor, this, disposables); + + this.disposable = Disposable.from(...disposables); + } + + public dispose(): void { + this.disposable.dispose(); + } + + public async get(ctx: Context): Promise { + return this.get_uncached(ctx); + } + /** + * Run `cargo metadata` + */ + @progress('Running cargo metadata') + public async get_uncached(ctx: Context): Promise { + const cargo = await this.cargo.get(ctx); + + let stdout; + try { + stdout = await new ProcessBuilder( + ctx, + cargo.executable, + ['metadata', '--format-version', '1'], + {}, + ).exec({ noStderr: true }); + + await setContext('validCargoMetadata', true); + } catch (e) { + await setContext('validCargoMetadata', false); + throw e; + } + + console.log(`'cargo metadata' was successful`); + return JSON.parse(stdout) as Metadata; + } + + private invalidateFor(uri: Uri) { + const ws = workspace.getWorkspaceFolder(uri); + if (ws) { + this.notifyChange(ws); + } + } +} + +export interface Resolve { + /** + * Current package. + */ + readonly root: PackageId; + + readonly nodes: ResolvedNode[]; +} + +export interface ResolvedNode { + readonly id: PackageId; + readonly dependencies: PackageId[]; +} + +/** + * Space separated package id like `swc_macros_common 0.1.0 (path+file:///mnt/c/Users/kdy/Documents/projects/swc/macros/common)`; + */ +export type PackageId = string; + +export function parsePkgId( + pkgId: PackageId, +): { + readonly id: string; + readonly version: string; + readonly path: string; +} { + const ss = pkgId.split(' ', 3); + if (ss.length !== 3) { + throw new Error( + `Cannot parse '${pkgId}' as a cargo package because it's length is ${ + ss.length + } when splited by a space`, + ); + } + + // Remove surrounding paren + const replaced = ss[2] + .substr(1, ss[2].length - 2) + .replace('path+file', 'file') + .replace('registry+http', 'https'); + + try { + const path = Uri.parse(replaced); + return { id: ss[0], version: ss[1], path: path.fsPath }; + } catch (e) { + throw new Error(`failed to parse ${replaced} as a uri: ${e}`); + } +} + +/** + * e.g. `registry+https://github.com/rust-lang/crates.io-index` + */ +export type Source = string | undefined; + +export interface PackageMetadata { + readonly name: string; + /** + * Semver + */ + readonly version: string; + readonly id: PackageId; + readonly license: string | undefined; + readonly license_file: string | undefined; + readonly description: string | undefined; + readonly source: Source; + + readonly dependencies: Dependency[]; + + readonly targets: PkgTarget[]; + + readonly features: Features; + + readonly manifest_path: string; +} + +export interface Features { + /** + * Feature names. + */ + readonly default: string[]; + /** + * e.g. `"nested-values": ["erased-serde"],` + */ + readonly [feature: string]: string[]; +} + +export interface PkgTarget { + /** + * e.g. `["lib"]`, `["example"]`, `["test"]`, `["bench"]`, `["proc-macro"]`, `["custom-build"]` + */ + readonly kind: string[]; + /** + * e.g. `["lib"]`, `["bin"]`, `["proc-macro"]` + */ + readonly crate_types: string[]; + readonly name: string; + /** + * Absolute path to main rust file. + * + * e.g. `/mnt/c/Users/kdy/Documents/projects/swc/macros/common/src/lib.rs` + */ + readonly src_path: string; +} + +export interface Dependency { + readonly name: string; + readonly source: Source; + /** + * Semver match rule. + * + * e.g. `*`, `^1` + */ + readonly req: string; + /** + * - `null` for normal dependencies. + * - `dev` for dev dependencies. + * + * + */ + readonly kind: string | undefined; + readonly optional: boolean; + readonly uses_default_features: boolean; + readonly features: string[]; + /** + * e.g. `null`, `cfg(target_os = \"redox\")`, `cfg(windows)` + */ + readonly target: string | undefined; +} diff --git a/src/debugger/cargo/package.ts b/src/debugger/cargo/package.ts new file mode 100644 index 00000000..c6128a9d --- /dev/null +++ b/src/debugger/cargo/package.ts @@ -0,0 +1,36 @@ +import { Dependency } from './metadata'; + +export const enum LibraryType { + None, + Normal, + ProcMacro, +} + +/** + * Crate + */ +export default interface Package { + readonly name: string; + /** + * Semver + */ + readonly version: string; + readonly manifest_path: string; + + readonly targetDir: string; + + readonly targets: TaskTarget[]; + + readonly dependencies: Dependency[]; + + readonly libType: LibraryType; + readonly isMember: boolean; + + readonly manifest_dir: string; +} + +export interface TaskTarget { + readonly name: string; + readonly kind: string[]; + readonly src_path: string; +} diff --git a/src/debugger/cargo/resolver.ts b/src/debugger/cargo/resolver.ts new file mode 100644 index 00000000..e574a264 --- /dev/null +++ b/src/debugger/cargo/resolver.ts @@ -0,0 +1,15 @@ +import Cargo from '.'; +import { Factory, progress } from '../util'; +import { Context } from '../util/context'; + +export default class CargoResolver extends Factory { + constructor() { + super([]); + } + + @progress('Resolving cargo') + public async get(_ctx: Context): Promise { + // TODO + return new Cargo('cargo'); + } +} diff --git a/src/debugger/cargo/task_factory.ts b/src/debugger/cargo/task_factory.ts new file mode 100644 index 00000000..bb4a4ed6 --- /dev/null +++ b/src/debugger/cargo/task_factory.ts @@ -0,0 +1,68 @@ +import { dirname } from 'path'; +import { ProcessExecution, Task, Uri, workspace } from 'vscode'; + +import Cargo from '.'; +import { CachingFactory, Factory } from '../util'; +import { Context } from '../util/context'; +import Package from './package'; +import CargoWorkspace from './workspace'; + +export default class CargoTaskFactory extends CachingFactory { + constructor( + private readonly cargo: Factory, + private readonly cargoWorkspace: Factory, + ) { + super([cargo, cargoWorkspace]); + } + + public async get_uncached(ctx: Context): Promise { + const cargo = await this.cargo.get(ctx); + function makeTask(crate: Package, cmd: string[]): Task { + const dir = dirname(crate.manifest_path); + + return new Task( + { + type: 'cargo', + crate: crate.name, + cmd, + }, + ctx.ws, + `${cmd.join(' ')} (${crate.name})`, + `Cargo`, + new ProcessExecution(cargo.executable, cmd, { + cwd: dir, + }), + ['$rustc'], + ); + } + + const cargoWorkspace = await this.cargoWorkspace.get(ctx); + + const tasks: Task[] = []; + + for (const member of cargoWorkspace.members) { + if ( + workspace.getWorkspaceFolder(Uri.file(member.manifest_path)) !== ctx.ws + ) { + console.log('Not mine', member.manifest_path, ctx.ws.uri.fsPath); + continue; + } + tasks.push(makeTask(member, ['check'])); + + for (const tt of member.targets) { + const kind = tt.kind[0]; + console.log('Task target kind: ', kind); + + if (kind === 'bin') { + tasks.push(makeTask(member, ['install', '--bin', tt.name])); + } + + if (kind === 'test') { + tasks.push(makeTask(member, ['test', '--test', tt.name])); + } + } + } + + return tasks; + } +} diff --git a/src/debugger/cargo/task_provider.ts b/src/debugger/cargo/task_provider.ts new file mode 100644 index 00000000..9b161114 --- /dev/null +++ b/src/debugger/cargo/task_provider.ts @@ -0,0 +1,57 @@ +import { CancellationToken, Disposable, Task, TaskProvider, workspace } from 'vscode'; + +import Cargo from '.'; +import { Factory } from '../util'; +import { Context } from '../util/context'; +import CargoTaskFactory from './task_factory'; +import CargoWorkspace from './workspace'; + +export default class CargoTaskProvider implements TaskProvider, Disposable { + private readonly tasksFactory: CargoTaskFactory = new CargoTaskFactory( + this.cargo, + this.cargoWorkspace, + ); + + constructor( + readonly cargo: Factory, + readonly cargoWorkspace: Factory, + ) { } + + public async resolveTask( + task: Task, + _token?: CancellationToken | undefined, + ): Promise { + console.log('resolveTask', task); + return; + } + + public async provideTasks( + _token?: CancellationToken | undefined, + ): Promise { + if (!workspace.workspaceFolders) { + return; + } + + const promises: Array> = []; + const tasks: Task[] = []; + + for (const ws of workspace.workspaceFolders) { + const promise = this.tasksFactory + .get(Context.root(ws, 'Resolving cargo tasks')) + .then( + (ts): void => { + tasks.push(...ts); + }, + ); + promises.push(promise); + } + + await Promise.all(promises); + + return tasks; + } + + public dispose() { + this.tasksFactory.dispose(); + } +} diff --git a/src/debugger/cargo/workspace.ts b/src/debugger/cargo/workspace.ts new file mode 100644 index 00000000..effe624e --- /dev/null +++ b/src/debugger/cargo/workspace.ts @@ -0,0 +1,41 @@ +import { Uri } from 'vscode'; +import { isDescendant } from '../util'; +import Package from './package'; + +export default class CargoWorkspace { + constructor( + readonly wsRoot: string | undefined, + readonly targetDir: string, + readonly members: Package[], + readonly packages: Package[], + ) { + if (members === undefined) { + throw new Error('Assertion failed: CargoWorkspace.members !== undefined'); + } + } + + public getManifestDir(uri: Uri): string | undefined { + let candidate: string = ''; + + for (const m of this.members) { + if (candidate.length >= m.manifest_dir.length) { + continue; + } + + const dir = m.manifest_dir; + if (!isDescendant(dir, uri.fsPath)) { + continue; + } + + candidate = m.manifest_dir; + } + + // + if (candidate.length === 0) { + return; + } + + return candidate; + } +} + diff --git a/src/debugger/cargo/workspace_factory.ts b/src/debugger/cargo/workspace_factory.ts new file mode 100644 index 00000000..b513a4b9 --- /dev/null +++ b/src/debugger/cargo/workspace_factory.ts @@ -0,0 +1,95 @@ +import { dirname } from 'path'; + +import { CachingFactory, Factory, profile, progress } from '../util'; +import { Context } from '../util/context'; +import Metadata, { PackageMetadata, parsePkgId } from './metadata'; +import Package, { LibraryType } from './package'; +import CargoWorkspace from './workspace'; + +export class CargoWorkspaceFactory extends CachingFactory { + constructor(private readonly metadata: Factory) { + super([metadata]); + } + + /** + * Run `cargo metadata` + */ + @profile('CargoWorkspaceFactory.get_uncached') + @progress('Parsing cargo workspace') + public async get_uncached(ctx: Context): Promise { + const data = await this.metadata.get(ctx); + + const packages = data.packages.map(p => + intoPackage(data.target_directory, p), + ); + + const members = data.workspace_members + .sort() + .map(parsePkgId) + .map( + ({ id }): Package => { + for (const p of packages) { + if (p.name === id) { + p.isMember = true; + return p; + } + } + throw new Error( + `'cargo metadata' says that an unknown package (${id}) is a member of the current workspace`, + ); + }, + ); + + return new CargoWorkspace( + data.workspace_root, + data.target_directory, + members, + packages.sort((l, r) => { + if (l.isMember === r.isMember) { + return l.name.localeCompare(r.name); + } + + return r.isMember ? 1 : 0 - (l.isMember ? 1 : 0); + }), + ); + } +} + +function intoPackage( + targetDir: string, + p: PackageMetadata, +): PackageMetadata & { + manifest_dir: string; + isMember: boolean; + libType: LibraryType; + targetDir: string; +} { + let libType = LibraryType.None; + for (const t of p.targets) { + if (t.kind.length !== 1) { + throw new Error( + `Unexpected target.kind: ${t.kind} found from crate ${p.name}`, + ); + } + + const { + kind: [kind], + } = t; + + if (kind === 'lib') { + libType = LibraryType.Normal; + break; + } else if (kind === 'proc-macro') { + libType = LibraryType.ProcMacro; + break; + } + } + + return { + ...p, + manifest_dir: dirname(p.manifest_path), + isMember: false, + libType, + targetDir, + }; +} diff --git a/src/debugger/index.ts b/src/debugger/index.ts new file mode 100644 index 00000000..192de55c --- /dev/null +++ b/src/debugger/index.ts @@ -0,0 +1,345 @@ +import * as os from 'os'; +import { + CancellationToken, + debug, + DebugConfiguration, + DebugConfigurationProvider, + Disposable, + WorkspaceFolder, +} from 'vscode'; +import * as which from 'which'; +import { askBuildFlags, askCrate } from './ask'; +import Cargo from './cargo'; +import { BuildOutput } from './cargo/Build'; +import CargoWorkspace from './cargo/Workspace'; +import Rustc from './rustc/rustc'; +import { Factory, } from './util'; +import { trueCasePath } from './util/cli'; +import { Context } from './util/context'; + +type DebuggerType = 'msvc' | 'gdb' | 'lldb'; + +/** + * types for vscode cpp tools extension. + */ +type CppDebugType = 'cppdbg' | 'cppvsdbg'; + +interface BaseCfg extends DebugConfiguration { + readonly type: CppDebugType; + + readonly program: string; + readonly args: string[]; + readonly cwd: string; + readonly env: { [key: string]: string }; + + readonly sourceFileMap: { [key: string]: string }; + readonly visualizerFile?: string; + + readonly externalConsole: boolean; +} + +interface MsvcCfg extends BaseCfg { + readonly type: 'cppvsdbg'; + readonly symbolSearchPath?: string; + readonly dumpPath?: string; +} + +interface MiDebuggerCfg extends BaseCfg { + readonly type: 'cppdbg'; + readonly setupCommands: SetupCommand[]; + customLaunchSetupCommands?: SetupCommand[]; + launchCompleteCommand?: 'exec-run' | 'exec-continue' | 'None'; + showDisplayString?: boolean; + additionalSOLibSearchPath?: string; + readonly MIMode: 'gdb' | 'lldb'; + miDebuggerPath?: string; +} +interface SetupCommand { + readonly description: string; + readonly text: string; + readonly ignoreFailures: boolean; +} + +type WrappedConfig = MsvcCfg | MiDebuggerCfg; +/** Wrapper class for `default behavior`s */ +export interface RustDebugConfig extends DebugConfiguration { + /** + * true by default + */ + readonly pretty: boolean; + readonly noDebug: boolean; + readonly crate: string; + readonly buildFlags: string[]; + + readonly sourceFileMap: { [from: string]: string }; + + readonly mode: DebuggerType; + readonly MIDebuggerPath?: string; +} + +async function parseRustDebugConfig( + ctx: Context, + cargoWorkspace: Factory, + rustc: Rustc, + raw: DebugConfiguration, +): Promise { + const pretty = raw.pretty !== false; + delete raw.pretty; + + const noDebug = !!raw.noDebug; + + const crate: string = await (async () => { + if (raw.crate) { + return raw.crate; + } + + return askCrate(ctx, cargoWorkspace); + })(); + delete raw.crate; + + const buildFlags: string[] = await (async () => { + if (raw.buildFlags) { + return raw.buildFlags; + } + return askBuildFlags(ctx, cargoWorkspace, crate); + })(); + delete raw.buildFlags; + + const cwd: string = await trueCasePath(raw.cwd || ctx.ws.uri.fsPath); + const env: { [key: string]: string } = raw.env || {}; + + const sourceFileMap = await (async () => { + const map: { [from: string]: string } = raw.sourceMap || {}; + + if (os.platform() === 'win32') { + // 'C:\\projects\\rust' is hardcoded in rust lang's windows builder script. + map['C:\\projects\\rust'] = await rustc.rustSrcPath; + + // TODO: some general way + map['/c/'] = 'C:\\'; + } + + return map; + })(); + + const mode = await (async () => { + // Check for msvc + if (os.platform() === 'win32') { + const cfgs = await rustc.configs; + for (const cfg of cfgs) { + if (cfg.key === 'target_env' && cfg.value === 'msvc') { + return 'msvc'; + } + } + } + + switch (raw.MIMode) { + case 'gdb': + return 'gdb'; + case 'lldb': + return 'lldb'; + case undefined: + return os.platform() === 'darwin' ? 'lldb' : 'gdb'; + + default: + throw new Error(`Unknown MIMode: ${raw.MIMode}`); + } + })(); + + const miDebuggerPath = await (async (): Promise => { + if (!!raw.MIDebuggerPath || noDebug) { + return raw.MIDebuggerPath; + } + + // mi engine is not used in msvc mode. + if (mode === 'msvc') { + return; + } + + return new Promise((resolve, reject) => { + which(mode, (err, resolvedPath: string | undefined) => { + if (!!err) { + return reject(err); + } + console.log(`Resolved ${mode} as ${resolvedPath} `); + resolve(resolvedPath); + }); + }); + })(); + delete raw.MIDebuggerPath; + + return { + ...raw, + noDebug, + pretty, + crate, + cwd, + buildFlags, + sourceFileMap, + mode, + MIDebuggerPath: miDebuggerPath, + }; +} + +export default class RustConfigProvider implements DebugConfigurationProvider, Disposable { + constructor( + private readonly rustc: Factory, + private readonly cargo: Factory, + private readonly cargoWorkspace: Factory, + ) { } + + public async resolveDebugConfiguration( + ws: WorkspaceFolder | undefined, + debugCfg: DebugConfiguration, + token?: CancellationToken, + ): Promise { + if (!ws) { + throw new Error( + 'Not supported yet: rust debugger for files without vscode workspace', + ); + } + const msg = debugCfg.noDebug ? 'Launching' : 'Launching debugger'; + return Context.root(ws, msg).runWith( + async (ctx): Promise => { + const cargo = await this.cargo.get(ctx); + const rustc = await this.rustc.get(ctx); + + const cfg = await ctx.subTask('Parsing configuration', async ctx => + parseRustDebugConfig(ctx, this.cargoWorkspace, rustc, debugCfg), + ); + + const { crate, sourceFileMap, env } = cfg; + + const extras: string[] = ['-p', crate]; + + const mode = await cfg.mode; + const type = mode === 'gdb' || mode === 'lldb' ? 'cppdbg' : 'cppvsdbg'; + + const executables: string[] = []; + try { + await cargo.buildBinary( + ctx, + false, + [...cfg.buildFlags, '-p', crate], + { + onStdout(d: BuildOutput) { + if (d.reason !== 'compiler-artifact') { + return; + } + if (d.target.kind[0] === 'custom-build') { + return; + } + if ( + d.target.crate_types[0] === 'bin' || + (d.target.kind[0] === 'lib' && d.profile.test) + ) { + executables.push(...d.filenames); + return; + } + }, + onStderr(s: string) { + debug.activeDebugConsole.append(s); + }, + logWith(s: string) { + debug.activeDebugConsole.appendLine(s); + }, + }, + ); + } catch (e) { + throw new Error(`Failed to build executable: ${e}`); + } + if (executables.length === 0) { + throw new Error(`cargo build did not produce any executable file. `); + } + + // Print executable names to debug console. + debug.activeDebugConsole.appendLine('Built executables:'); + for (const e of executables) { + debug.activeDebugConsole.appendLine(`\t${e}`); + } + + { + // Add environment variables related to cargo. + const cargoWs = await this.cargoWorkspace.get(ctx); + + // TODO: Use cargo pacakge id instead of name. + const pkg = cargoWs.packages.find(pkg => pkg.name === crate); + if (!pkg) { + throw new Error(`Cannot build unknown package ${crate}`); + } + + env.CARGO = cargo.executable; + env.CARGO_TARGET_DIR = pkg.targetDir; + env.CARGO_MANIFEST_DIR = pkg.manifest_dir; + env.CARGO_PKG_NAME = pkg.name; + } + + if (cfg.noDebug) { + // TODO + } + + // Redirect to cpptools. + + if (executables.length !== 1) { + throw new Error(`cargo build produced too many executable files. Debugging multiple files is not supported yet and\ + built executables are printed on the debug console.`); + } + const program = executables[0]; + + const base: BaseCfg = { + ...cfg, + + type, + request: cfg.request, + program, + cwd: cfg.cwd, + args: cfg.args || [], + env, + sourceFileMap, + externalConsole: cfg.externalConsole, + }; + const resolved: WrappedConfig = + mode === 'msvc' + ? { + ...base, + type: 'cppvsdbg', + } + : { + ...base, + type: 'cppdbg', + MIMode: mode, + setupCommands: cfg.setupCommands || [], + }; + + // Enable pretty printing + if (cfg.pretty && resolved.type === 'cppdbg') { + // `-enable-pretty-printing` + + // But if user did it already, skip it. + let has = false; + for (const sc of resolved.setupCommands) { + if (sc && sc.text === '-enable-pretty-printing') { + has = true; + break; + } + } + if (!has) { + resolved.setupCommands.push({ + description: 'Enable pretty-printing for gdb', + text: '-enable-pretty-printing', + ignoreFailures: true, + }); + } + } + + console.log('Resolved', debugCfg, 'as', resolved); + + return resolved; + }, + ); + } + + public dispose() { + this.cargoWorkspace.dispose(); + } +} diff --git a/src/debugger/rustc/cfg.ts b/src/debugger/rustc/cfg.ts new file mode 100644 index 00000000..7a60edd7 --- /dev/null +++ b/src/debugger/rustc/cfg.ts @@ -0,0 +1,22 @@ +/** + * `#[cfg(debug_assertions)]`, `#[cfg(target_env="gnu")]` + */ +export default class RustCfg { + private constructor(readonly key: string, readonly value: string | true) {} + + /** + * Parse output of `rustc --print=cfg`. + * + * @param line + * - `debug_assertions` + * - `target_arch="x86_64"` + */ + public static parse(line: string): RustCfg { + const [key, valueStr] = line.split('=', 2); + // `"xxx"` -> xxx + const value = + valueStr === undefined ? true : valueStr.substr(1, valueStr.length - 2); + + return new RustCfg(key, value); + } +} diff --git a/src/debugger/rustc/factory.ts b/src/debugger/rustc/factory.ts new file mode 100644 index 00000000..1dfca205 --- /dev/null +++ b/src/debugger/rustc/factory.ts @@ -0,0 +1,17 @@ +import { join } from 'path'; + +import { Factory } from '../util'; +import { Context } from '../util/context'; +import Rustc from './rustc'; + + +export class RustcResolver extends Factory { + constructor() { + super([]); + } + + public async get(ctx: Context): Promise { + // TODO + return new Rustc(ctx, 'rustc'); + } +} diff --git a/src/debugger/rustc/rustc.ts b/src/debugger/rustc/rustc.ts new file mode 100644 index 00000000..9eba9526 --- /dev/null +++ b/src/debugger/rustc/rustc.ts @@ -0,0 +1,39 @@ +import { join } from 'path'; + +import { ProcessBuilder } from '../util'; +import { Cli } from '../util/cli'; +import { Context } from '../util/context'; +import RustCfg from './cfg'; + +export default class Rustc extends Cli { + constructor(private readonly ctx: Context, executable: string) { + super(executable); + } + + get sysroot(): Promise { + return new ProcessBuilder( + this.ctx, + this.executable, + ['--print=sysroot'], + {}, + ) + .exec({ noStderr: true }) + .then(v => v.replace('\r', '').replace('\n', '')); + } + + get rustSrcPath(): Promise { + return this.sysroot.then(v => join(v, 'lib', 'rustlib', 'src', 'rust')); + } + + get configs(): Promise { + return new ProcessBuilder(this.ctx, this.executable, ['--print=cfg'], {}) + .exec({ noStderr: true }) + .then(v => + v + .replace('\r', '') + .split('\n') + .map(RustCfg.parse), + ); + } +} + diff --git a/src/debugger/util/cli.ts b/src/debugger/util/cli.ts new file mode 100644 index 00000000..9b184174 --- /dev/null +++ b/src/debugger/util/cli.ts @@ -0,0 +1,86 @@ +import * as glob from 'glob'; +import { platform } from 'os'; +import * as path from 'path'; + +export class Cli { + /** + * + * @param executable Resolved path to executable. + */ + constructor(readonly executable: string) {} +} + +/* +https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows + +SYNOPSIS + trueCasePathSync() +DESCRIPTION + Given a possibly case-variant version of an existing filesystem path, returns + the case-exact, normalized version as stored in the filesystem. + Note: If the input path is a globbing *pattern* as defined by the 'glob' npm + package (see prerequisites below), only the 1st match, if any, + is returned. + Only a literal input path guarantees an unambiguous result. + If no matching path exists, undefined is returned. + On case-SENSITIVE filesystems, a match will also be found, but if case + variations of a given path exist, it is undefined which match is returned. +PLATFORMS + Windows, OSX, and Linux (though note the limitations with case-insensitive + filesystems). +LIMITATIONS + - Paths starting with './' are acceptable, but paths starting with '../' + are not - when in doubt, resolve with fs.realPathSync() first. + An initial '.' and *interior* '..' instances are normalized, but a relative + input path still results in a relative output path. If you want to ensure + an absolute output path, apply fs.realPathSync() to the result. + - On Windows, no attempt is made to case-correct the drive letter or UNC-share + component of the path. + - Unicode support: + - Be sure to use UTF8 source-code files (with a BOM on Windows) + - On OSX, the input path is automatically converted to NFD Unicode form + to match how the filesystem stores names, but note that the result will + invariably be NFD too (which makes no difference for ASCII-characters-only + names). +PREREQUISITES + npm install glob # see https://www.npmjs.com/search?q=glob +EXAMPLES + trueCasePathSync('/users/guest') // OSX: -> '/Users/Guest' + trueCasePathSync('c:\\users\\all users') // Windows: -> 'c:\Users\All Users' +*/ +export async function trueCasePath(fsPath: string): Promise { + // Normalize the path so as to resolve . and .. components. + // !! As of Node v4.1.1, a path starting with ../ is NOT resolved relative + // !! to the current dir, and glob.sync() below then fails. + // !! When in doubt, resolve with fs.realPathSync() *beforehand*. + let fsPathNormalized = path.normalize(fsPath); + + // OSX: HFS+ stores filenames in NFD (decomposed normal form) Unicode format, + // so we must ensure that the input path is in that format first. + if (process.platform === 'darwin') { + fsPathNormalized = fsPathNormalized.normalize('NFD'); + } + + // !! Windows: Curiously, the drive component mustn't be part of a glob, + // !! otherwise glob.sync() will invariably match nothing. + // !! Thus, we remove the drive component and instead pass it in as the 'cwd' + // !! (working dir.) property below. + let pathRoot = path.parse(fsPathNormalized).root; + if (platform() === 'win32') { + pathRoot = pathRoot.toUpperCase(); + } + + const noDrivePath = fsPathNormalized.slice(Math.max(pathRoot.length - 1, 0)); + + // Perform case-insensitive globbing (on Windows, relative to the drive / + // network share) and return the 1st match, if any. + // Fortunately, glob() with nocase case-corrects the input even if it is + // a *literal* path. + const g = new glob.Glob(noDrivePath, { nocase: true, cwd: pathRoot }); + return new Promise(resolve => { + g.once('match', (s: string) => { + g.abort(); + resolve(s); + }); + }); +} diff --git a/src/debugger/util/context.ts b/src/debugger/util/context.ts new file mode 100644 index 00000000..fe153a59 --- /dev/null +++ b/src/debugger/util/context.ts @@ -0,0 +1,49 @@ +import { ProgressLocation, window, WorkspaceFolder } from 'vscode'; + +export class Context { + private constructor( + private readonly parent: Context | undefined, + readonly ws: WorkspaceFolder, + readonly taskName: string, + ) {} + + public static root(ws: WorkspaceFolder, taskName: string): Context { + return new Context(undefined, ws, taskName); + } + + public runWith(op: (c: Context) => Promise): Promise { + return this.withProgress(op(this)); + } + + public subTask( + name: string, + op: (ctx: Context) => Promise, + ): Promise { + const sub = new Context(this, this.ws, name); + const task = op(sub); + + return sub.withProgress(task); + } + + private get taskStack(): string[] { + if (!this.parent) { + return [this.taskName]; + } else { + return [...this.parent.taskStack, this.taskName]; + } + } + + private async withProgress(task: Promise): Promise { + return window.withProgress( + { + location: ProgressLocation.Window, + }, + async progress => { + progress.report({ message: this.taskStack.join(' - ') }); + const res = await task; + + return res; + }, + ); + } +} diff --git a/src/debugger/util/index.ts b/src/debugger/util/index.ts new file mode 100644 index 00000000..335d4c75 --- /dev/null +++ b/src/debugger/util/index.ts @@ -0,0 +1,370 @@ +import { ChildProcess, execFile, spawn } from 'child_process'; +import * as fs from 'fs'; +import { relative } from 'path'; +import { + commands, + Disposable, + DocumentLink, + Event, + EventEmitter, + SymbolInformation, + Uri, + WorkspaceFolder, +} from 'vscode'; +import { trueCasePath } from './cli'; +import { Context } from './context'; + +export function isDescendant(parent: string, descendant: string): boolean { + return !relative(parent, descendant).startsWith('..'); +} + +export async function rename(oldPath: string, newPath: string): Promise { + return new Promise((resolve, reject) => { + fs.rename(oldPath, newPath, err => { + if (!!err) { + return reject(err); + } + + return resolve(); + }); + }); +} + +export async function exists(path: string): Promise { + return new Promise(resolve => { + fs.stat(path, err => { + if (!!err) { + return resolve(false); + } + + return resolve(true); + }); + }); +} + +export async function readFile(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (!!err) { + return reject(err); + } + return resolve(data); + }); + }); +} + +// tslint:disable-next-line: no-any +export async function setContext(key: string, value: any): Promise { + await commands.executeCommand('setContext', key, value); +} + +export async function executeDocumentSymbolProvider( + uri: Uri, +): Promise { + try { + const res = await commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + uri, + ); + console.log('executeDocumentSymbolProvider was successful'); + if (!res) { + return []; + } + return res; + } catch (e) { + throw new Error(`Cannot fetch symbols of ${uri} : ${e}`); + } +} + +export async function executeLinkProvider(uri: Uri): Promise { + try { + const res = await commands.executeCommand( + 'vscode.executeLinkProvider', + uri, + ); + console.log('executeLinkProvider was successful'); + if (!res) { + return []; + } + return res; + } catch (e) { + throw e; + // throw new Error(`Cannot fetch symbols of ${uri} : ${e}`) + } +} + +function decorate( + // tslint:disable-next-line: ban-types + decorator: (fn: Function, key: string) => Function, + // tslint:disable-next-line: ban-types +): Function { + // tslint:disable-next-line: no-any + return (_target: any, key: string, descriptor: any) => { + let fnKey: string | null = null; + // tslint:disable-next-line: ban-types + let fn: Function | null = null; + + if (typeof descriptor.value === 'function') { + fnKey = 'value'; + fn = descriptor.value; + } else if (typeof descriptor.get === 'function') { + fnKey = 'get'; + fn = descriptor.get; + } + + if (!fn || !fnKey) { + throw new Error('not supported'); + } + + const decorated = decorator(fn, key); + if ( + !decorated || + typeof decorated !== 'function' || + decorated.toString() === 'null' + ) { + throw new Error( + `util.decorate: ${decorator} returned invalid value for input (${fn}, ${key})`, + ); + } + descriptor[fnKey] = decorated; + if (!descriptor[fnKey]) { + throw new Error(`util.decorate: failed to set value`); + } + }; +} + +// tslint:disable-next-line: ban-types +export function profile(name: string): Function { + // tslint:disable-next-line: ban-types + return decorate((fn: Function, key: string): Function => { + // tslint:disable-next-line: no-any + return function timeTracked(this: any, ...args: any[]): any { + const start = clock(); + // tslint:disable-next-line: no-any + const res: any = fn.apply(this, args); + + if (res instanceof Promise) { + return res.then(result => { + const ms = clock(start); + // console.log(`[perf] ${name}: `, ms, 'ms'); + return result; + }); + } else { + const ms = clock(start); + // console.log(`[perf] ${name}: `, ms, 'ms'); + return res; + } + }; + }); +} + +const nop = () => { }; + +// tslint:disable-next-line: ban-types +export function progress(name: string): Function { + // tslint:disable-next-line: ban-types + return decorate((fn: Function, _key: string): Function => { + return function withProgress( + this: any, + ctx: Context, + ...args: any[] + ): Promise { + return ctx.subTask(name, ctx => fn.apply(this, [ctx, ...args])); + }; + }); +} + +export function clock(start: [number, number]): number; +export function clock(): [number, number]; +export function clock( + start?: [number, number] | undefined, +): number | [number, number] { + if (!start) { + return process.hrtime(); + } + const end = process.hrtime(start); + return Math.round(end[0] * 1000 + end[1] / 1000000); +} + +export abstract class Factory { + public get onChange(): Event { + return this._onChange.event; + } + private readonly _factorydisposable: Disposable; + /** + * + * @param deps Dependencies. + * @param _onChange + */ + protected constructor( + // tslint:disable-next-line: no-any + deps: Array>, + private readonly _onChange: EventEmitter< + WorkspaceFolder + > = new EventEmitter(), + ) { + const disposables: Disposable[] = []; + for (const dep of deps) { + dep.onChange(this.notifyChange, this, disposables); + } + + this._factorydisposable = Disposable.from(...disposables); + } + + public abstract get(ctx: Context): Promise; + public dispose(): void { + this._onChange.dispose(); + this._factorydisposable.dispose(); + } + + protected notifyChange(ws: WorkspaceFolder) { + this._onChange.fire(ws); + } +} + +export abstract class CachingFactory extends Factory { + private _cached: WeakMap>; + + protected constructor(deps: Array>) { + super(deps); + this._cached = new WeakMap(); + } + + public async get(ctx: Context): Promise { + let cached = this._cached.get(ctx.ws); + if (cached !== undefined) { + return cached; + } + cached = this.get_uncached(ctx); + this._cached.set(ctx.ws, cached); + return cached; + } + + protected notifyChange(ws: WorkspaceFolder): void { + super.notifyChange(ws); + this._cached.delete(ws); + } + + protected abstract get_uncached(ctx: Context): Promise; +} + +export interface ProcessOptions { + readonly env?: Map; + /** + * Defaults to 10 seconds. + */ + readonly timeout?: number; +} + +export interface ExecOpts { + readonly noStderr: boolean; +} + +export class ProcessBuilder { + private logger?: (cmd: string) => void; + + constructor( + private readonly ctx: Context, + private readonly executable: string, + private readonly args: string[], + private readonly opts: ProcessOptions, + ) { } + + public logWith(f: undefined | ((cmd: string) => void)): ProcessBuilder { + this.logger = f; + return this; + } + + private get timeout(): number { + if (this.opts.timeout !== undefined) { + return this.opts.timeout; + } + + return 10000; + } + + public async spawn(): Promise { + if (this.logger) { + this.logger(`${this.command}`); + } + + const cwd = await trueCasePath(this.ctx.ws.uri.fsPath); + + const p = spawn(this.executable, this.args, { + cwd, + env: this.opts.env, + }); + + p.stderr.setEncoding('utf8'); + + // TODO: Timeout + + p.stdin.end(); + return p; + } + + public exec(opts: { noStderr: true } & ExecOpts): Promise; + public exec(opts: ExecOpts): Promise<{ stdout: string; stderr: string }>; + /** + * @returns Returned promise will be resolved when child process is terminated. + */ + public async exec( + opts: ExecOpts, + ): Promise<{ stdout: string; stderr: string } | string> { + if (this.logger) { + await this.logger(`${this.command}`); + } + + const cwd = await trueCasePath(this.ctx.ws.uri.fsPath); + + const { stdout, stderr } = await new Promise<{ + stdout: string; + stderr: string; + }>((resolve, reject) => { + execFile( + this.executable, + this.args, + { + encoding: 'utf8', + timeout: this.timeout, + env: this.opts.env, + cwd, + }, + (err, stdout: string, stderr: string): void => { + if (!!err) { + console.error( + `${ + this.command + } failed: ${err}\nStdout: ${stdout}\nStdErr: ${stderr}`, + ); + return reject(err); + } + + resolve({ stdout, stderr }); + }, + ); + }); + + if (opts.noStderr) { + if (stderr) { + console.error( + `${ + this.command + } printed something on stderr.\nStdout: ${stdout}\nStdErr: ${stderr}`, + ); + throw new Error( + `${ + this.command + } printed something on stderr.\nStdout: ${stdout}\nStderr: ${stderr}`, + ); + } + return stdout; + } + + return { stderr, stdout }; + } + + private get command(): string { + return `${this.executable} ${this.args.join(' ')}`; + } +} diff --git a/src/extension.ts b/src/extension.ts index 31365edf..e1229e73 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,6 +13,7 @@ import { workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent, + debug, } from 'vscode'; import { LanguageClient, @@ -22,6 +23,13 @@ import { } from 'vscode-languageclient'; import { RLSConfiguration } from './configuration'; +import RustConfigProvider from './debugger'; +import CargoExt from './debugger/cargo/ext'; +import { MetadataFactory } from './debugger/cargo/metadata'; +import CargoResolver from './debugger/cargo/resolver'; +import CargoTaskProvider from './debugger/cargo/task_provider'; +import { CargoWorkspaceFactory } from './debugger/cargo/workspace_factory'; +import { RustcResolver } from './debugger/rustc/factory'; import { SignatureHelpProvider } from './providers/signatureHelpProvider'; import { checkForRls, ensureToolchain, rustupUpdate } from './rustup'; import { startSpinner, stopSpinner } from './spinner'; @@ -42,6 +50,22 @@ interface ProgressParams { } export async function activate(context: ExtensionContext) { + function add(t: T): T { + context.subscriptions.push(t); + return t + } + + const rustc = add(new RustcResolver()); + const cargo = add(new CargoResolver()); + const cargoMetadata = add(new MetadataFactory(cargo)); + const cargoWorkspace = add(new CargoWorkspaceFactory(cargoMetadata)); + + add(new CargoExt(new CargoTaskProvider(cargo, cargoWorkspace))); + + const debugConfigProvider = add(new RustConfigProvider(rustc, cargo, cargoWorkspace)); + debug.registerDebugConfigurationProvider('rust', debugConfigProvider); + + context.subscriptions.push(configureLanguage()); workspace.onDidOpenTextDocument(doc => didOpenTextDocument(doc, context)); @@ -348,10 +372,10 @@ class ClientWorkspace { this.config.rustupDisabled ? wslWrapper.execFile('rustc', ['--print', 'sysroot'], { env }) : wslWrapper.execFile( - this.config.rustupPath, - ['run', this.config.channel, 'rustc', '--print', 'sysroot'], - { env }, - ); + this.config.rustupPath, + ['run', this.config.channel, 'rustc', '--print', 'sysroot'], + { env }, + ); const { stdout } = await rustcPrintSysroot(); return stdout diff --git a/tsconfig.json b/tsconfig.json index cb1b7176..759fe3d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,17 +3,20 @@ "module": "commonjs", "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": [ + "es6" + ], "sourceMap": true, "rootDir": "src", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true }, "exclude": [ "node_modules", ".vscode-test" ] -} +} \ No newline at end of file From e623b99a1d1e99e804314fe52dd24884bb95b667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Mon, 29 Apr 2019 23:30:41 +0900 Subject: [PATCH 2/3] Update package.json --- package.json | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) diff --git a/package.json b/package.json index 49d75cb5..7b100f5d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ ], "preview": true, "activationEvents": [ + "onCommand:workbench.action.tasks.runTask", "onLanguage:rust", + "onDebug", "workspaceContains:Cargo.toml" ], "main": "./out/extension.js", @@ -70,6 +72,266 @@ "vscode": "^1.1.30" }, "contributes": { + "breakpoints": [ + { + "language": "rust" + } + ], + "debuggers": [ + { + "type": "rust", + "label": "rust: Launch (GDB / LLDB / VSDebug)", + "configurationAttributes": { + "launch": { + "required": [ + "crate" + ], + "properties": { + "crate": { + "type": "string", + "decsription": "Crate name.", + "default": "" + }, + "targets": { + "type": "string", + "decsription": "e.g. ", + "default": "" + }, + "cwd": { + "type": "string", + "description": "The working directory of the target", + "default": "." + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "JSON array of command line arguments to pass to the program when it is launched. Example [\"arg1\", \"arg2\"].", + "default": [] + }, + "env": { + "type": "object", + "description": "Environment variables to add to the environment for the program. Example: { \"RUST_LOG\": \"debug\" }.", + "properties": { + "name": "string", + "value": "string" + }, + "default": {} + }, + "pretty": { + "type": "boolean", + "description": "Enable pretty priting. Enabled by default.", + "default": true + }, + "setupCommands": { + "type": "array", + "description": "One or more GDB/LLDB commands to execute in order to setup the underlying debugger. Example: \"setupCommands\": [ { \"text\": \"-enable-pretty-printing\", \"description\": \"Enable GDB pretty printing\", \"ignoreFailures\": true }].\n\nNote that recommended way to enable pretty printing is specifying \"pretty: true,\" instead of using this", + "items": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The debugger command to execute.", + "default": "" + }, + "description": { + "type": "string", + "description": "Optional description for the command.", + "default": "" + }, + "ignoreFailures": { + "type": "boolean", + "description": "If true, failures from the command should be ignored. Default value is false.", + "default": "false" + } + } + }, + "default": [] + }, + "launchCompleteCommand": { + "enum": [ + "exec-run", + "exec-continue", + "None" + ], + "description": "The command to execute after the debugger is fully setup in order to cause the target process to run. Allowed values are \"exec-run\", \"exec-continue\", \"None\". The default value is \"exec-run\".", + "default": "exec-run" + }, + "visualizerFile": { + "type": "string", + "description": ".natvis file to be used when debugging this process. This option is not compatible with GDB pretty printing. Please also see \"showDisplayString\" if using this setting.", + "default": "" + }, + "showDisplayString": { + "type": "boolean", + "description": "When a visualizerFile is specified, showDisplayString will enable the display string. Turning this option on can cause slower performance during debugging.", + "default": "true" + }, + "MIMode": { + "type": "string", + "description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".\n\n This is ignored if configured rustup toolchain is msvc.", + "default": "" + }, + "miDebuggerPath": { + "type": "string", + "description": "The path to the mi debugger (such as gdb). When unspecified, it will search path first for the debugger.", + "default": "" + }, + "miDebuggerServerAddress": { + "type": "string", + "description": "Network address of the MI Debugger Server to connect to (example: localhost:1234).", + "default": "serveraddress:port" + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": true + }, + "debugServerPath": { + "type": "string", + "description": "Optional full path to debug server to launch. Defaults to null.", + "default": "" + }, + "debugServerArgs": { + "type": "string", + "description": "Optional debug server args. Defaults to null.", + "default": "" + }, + "serverStarted": { + "type": "string", + "description": "Optional server-started pattern to look for in the debug server output. Defaults to null.", + "default": "" + }, + "filterStdout": { + "type": "boolean", + "description": "Search stdout stream for server-started pattern and log stdout to debug output. Defaults to true.", + "default": "true" + }, + "filterStderr": { + "type": "boolean", + "description": "Search stderr stream for server-started pattern and log stderr to debug output. Defaults to false.", + "default": "false" + }, + "serverLaunchTimeout": { + "type": "integer", + "description": "Optional time, in milliseconds, for the debugger to wait for the debugServer to start up. Default is 10000.", + "default": "10000" + }, + "coreDumpPath": { + "type": "string", + "description": "Optional full path to a core dump file for the specified program. Defaults to null.", + "default": "" + }, + "externalConsole": { + "type": "boolean", + "description": "If true, a console is launched for the debuggee. If false, no console is launched. Note this option is ignored in some cases for technical reasons.", + "default": "false" + }, + "sourceFileMap": { + "type": "object", + "description": "Optional source file mappings passed to the debug engine. Example: '{ \"/original/source/path\":\"/current/source/path\" }'", + "default": { + "": "" + } + }, + "logging": { + "type": "object", + "description": "Optional flags to determine what types of messages should be logged to the Debug Console.", + "default": {}, + "properties": { + "exceptions": { + "type": "boolean", + "description": "Optional flag to determine whether exception messages should be logged to the Debug Console. Defaults to true.", + "default": true + }, + "moduleLoad": { + "type": "boolean", + "description": "Optional flag to determine whether module load events should be logged to the Debug Console. Defaults to true.", + "default": true + }, + "programOutput": { + "type": "boolean", + "description": "Optional flag to determine whether program output should be logged to the Debug Console. Defaults to true.", + "default": true + }, + "engineLogging": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic engine logs should be logged to the Debug Console. Defaults to false.", + "default": false + }, + "trace": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic adapter command tracing should be logged to the Debug Console. Defaults to false.", + "default": false + }, + "traceResponse": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic adapter command and response tracing should be logged to the Debug Console. Defaults to false.", + "default": false + } + } + }, + "pipeTransport": { + "description": "When present, this tells the debugger to connect to a remote computer using another executable as a pipe that will relay standard input/output between VS Code and the MI-enabled debugger backend executable (such as gdb).", + "type": "object", + "default": { + "pipeCwd": "${workspaceRoot}", + "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", + "pipeArgs": [], + "debuggerPath": "enter the path for the debugger on the target machine, for example /usr/bin/gdb" + }, + "properties": { + "pipeCwd": { + "type": "string", + "description": "The fully qualified path to the working directory for the pipe program.", + "default": "/usr/bin" + }, + "pipeProgram": { + "type": "string", + "description": "The fully qualified pipe command to execute.", + "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" + }, + "pipeArgs": { + "type": "array", + "description": "Command line arguments passed to the pipe program to configure the connection.", + "items": { + "type": "string" + }, + "default": [] + }, + "debuggerPath": { + "type": "string", + "description": "The full path to the debugger on the target machine, for example /usr/bin/gdb.", + "default": "The full path to the debugger on the target machine, for example /usr/bin/gdb." + }, + "pipeEnv": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables passed to the pipe program.", + "default": {} + } + } + } + } + } + }, + "configurationSnippets": [ + { + "label": "rust: Launch (GDB / LLDB / VSDebug)", + "description": "A new configuration for launching a mock debug program", + "body": { + "type": "rust", + "request": "launch", + "name": "${2:Launch Program}", + "program": "^\"\\${workspaceFolder}/${1:Program}\"" + } + } + ] + } + ], "languages": [ { "id": "rust", From d5ccda4a48c517a039590398ce92966281a490b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Tue, 30 Apr 2019 15:01:34 +0900 Subject: [PATCH 3/3] Remove unused import --- src/debugger/rustc/factory.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/debugger/rustc/factory.ts b/src/debugger/rustc/factory.ts index 1dfca205..2216601c 100644 --- a/src/debugger/rustc/factory.ts +++ b/src/debugger/rustc/factory.ts @@ -1,5 +1,3 @@ -import { join } from 'path'; - import { Factory } from '../util'; import { Context } from '../util/context'; import Rustc from './rustc';