From 09a760e52e20dcd79d902b05065934615cc4d56b Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 16:05:42 +0300 Subject: [PATCH 1/6] vscode: add syntax tree inspection hovers and highlights --- editors/code/src/commands/syntax_tree.ts | 169 ++++++++++++++++------- editors/code/src/util.ts | 4 +- 2 files changed, 118 insertions(+), 55 deletions(-) diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index 2e08e8f115..21ecf2661e 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -1,8 +1,10 @@ import * as vscode from 'vscode'; import * as ra from '../rust-analyzer-api'; -import { Ctx, Cmd } from '../ctx'; -import { isRustDocument } from '../util'; +import { Ctx, Cmd, Disposable } from '../ctx'; +import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util'; + +const AST_FILE_SCHEME = "rust-analyzer"; // Opens the virtual file that will show the syntax tree // @@ -10,35 +12,13 @@ import { isRustDocument } from '../util'; export function syntaxTree(ctx: Ctx): Cmd { const tdcp = new TextDocumentContentProvider(ctx); - ctx.pushCleanup( - vscode.workspace.registerTextDocumentContentProvider( - 'rust-analyzer', - tdcp, - ), - ); - - vscode.workspace.onDidChangeTextDocument( - (event: vscode.TextDocumentChangeEvent) => { - const doc = event.document; - if (!isRustDocument(doc)) return; - afterLs(() => tdcp.eventEmitter.fire(tdcp.uri)); - }, - null, - ctx.subscriptions, - ); - - vscode.window.onDidChangeActiveTextEditor( - (editor: vscode.TextEditor | undefined) => { - if (!editor || !isRustDocument(editor.document)) return; - tdcp.eventEmitter.fire(tdcp.uri); - }, - null, - ctx.subscriptions, - ); + ctx.pushCleanup(new AstInspector); + ctx.pushCleanup(tdcp); + ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp)); return async () => { const editor = vscode.window.activeTextEditor; - const rangeEnabled = !!(editor && !editor.selection.isEmpty); + const rangeEnabled = !!editor && !editor.selection.isEmpty; const uri = rangeEnabled ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) @@ -48,45 +28,128 @@ export function syntaxTree(ctx: Ctx): Cmd { tdcp.eventEmitter.fire(uri); - return vscode.window.showTextDocument( - document, - vscode.ViewColumn.Two, - true, - ); + void await vscode.window.showTextDocument(document, { + viewColumn: vscode.ViewColumn.Two, + preserveFocus: true + }); }; } -// We need to order this after LS updates, but there's no API for that. -// Hence, good old setTimeout. -function afterLs(f: () => void) { - setTimeout(f, 10); -} - - -class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { - uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); - eventEmitter = new vscode.EventEmitter(); +class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable { + readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); + readonly eventEmitter = new vscode.EventEmitter(); + private readonly disposables: Disposable[] = []; constructor(private readonly ctx: Ctx) { + vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables); + vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables); + } + dispose() { + this.disposables.forEach(d => d.dispose()); } - provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult { - const editor = vscode.window.activeTextEditor; - const client = this.ctx.client; - if (!editor || !client) return ''; + private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { + if (isRustDocument(event.document)) { + // We need to order this after language server updates, but there's no API for that. + // Hence, good old sleep(). + void sleep(10).then(() => this.eventEmitter.fire(this.uri)); + } + } + private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { + if (editor && isRustEditor(editor)) { + this.eventEmitter.fire(this.uri); + } + } + + provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult { + const rustEditor = this.ctx.activeRustEditor; + if (!rustEditor) return ''; // When the range based query is enabled we take the range of the selection - const range = uri.query === 'range=true' && !editor.selection.isEmpty - ? client.code2ProtocolConverter.asRange(editor.selection) + const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty + ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection) : null; - return client.sendRequest(ra.syntaxTree, { - textDocument: { uri: editor.document.uri.toString() }, - range, - }); + const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, }; + return this.ctx.client.sendRequest(ra.syntaxTree, params, ct); } get onDidChange(): vscode.Event { return this.eventEmitter.event; } } + + +// FIXME: consider implementing this via the Tree View API? +// https://code.visualstudio.com/api/extension-guides/tree-view +class AstInspector implements vscode.HoverProvider, Disposable { + private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({ + fontStyle: "normal", + border: "#ffffff 1px solid", + }); + private rustEditor: undefined | RustEditor; + private readonly disposables: Disposable[] = []; + + constructor() { + this.disposables.push(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this)); + vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables); + vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); + } + dispose() { + this.setRustEditor(undefined); + this.disposables.forEach(d => d.dispose()); + } + + private onDidCloseTextDocument(doc: vscode.TextDocument) { + if (!!this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { + this.setRustEditor(undefined); + } + } + + private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) { + if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) { + this.setRustEditor(undefined); + return; + } + this.setRustEditor(editors.find(isRustEditor)); + } + + private setRustEditor(newRustEditor: undefined | RustEditor) { + if (newRustEditor !== this.rustEditor) { + this.rustEditor?.setDecorations(AstInspector.astDecorationType, []); + } + this.rustEditor = newRustEditor; + } + + provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult { + if (!this.rustEditor) return; + + const astTextLine = doc.lineAt(hoverPosition.line); + + const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text); + if (!rustTextRange) return; + + this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]); + + const rustSourceCode = this.rustEditor.document.getText(rustTextRange); + const astTextRange = this.findAstRange(astTextLine); + + return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astTextRange); + } + + private findAstRange(astLine: vscode.TextLine) { + const lineOffset = astLine.range.start; + const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex); + const end = lineOffset.translate(undefined, astLine.text.trimEnd().length); + return new vscode.Range(begin, end); + } + + private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range { + const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine); + if (!parsedRange) return; + + const [, begin, end] = parsedRange.map(off => doc.positionAt(+off)); + + return new vscode.Range(begin, end); + } +} diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 978a31751e..6f91f81d63 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -65,12 +65,12 @@ export async function sendRequestWithRetry( throw 'unreachable'; } -function sleep(ms: number) { +export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } export type RustDocument = vscode.TextDocument & { languageId: "rust" }; -export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; +export type RustEditor = vscode.TextEditor & { document: RustDocument }; export function isRustDocument(document: vscode.TextDocument): document is RustDocument { return document.languageId === 'rust' From 4fbca1c64df789c1fa46d083d6555b0d0b3107c0 Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 16:57:03 +0300 Subject: [PATCH 2/6] vscode: use ctx.subscriptions instead of local .disposables --- editors/code/src/commands/syntax_tree.ts | 27 +++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index 21ecf2661e..eba5111930 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -12,8 +12,8 @@ const AST_FILE_SCHEME = "rust-analyzer"; export function syntaxTree(ctx: Ctx): Cmd { const tdcp = new TextDocumentContentProvider(ctx); - ctx.pushCleanup(new AstInspector); - ctx.pushCleanup(tdcp); + void new AstInspector(ctx); + ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp)); return async () => { @@ -35,17 +35,14 @@ export function syntaxTree(ctx: Ctx): Cmd { }; } -class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable { +class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); readonly eventEmitter = new vscode.EventEmitter(); - private readonly disposables: Disposable[] = []; + constructor(private readonly ctx: Ctx) { - vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables); - vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables); - } - dispose() { - this.disposables.forEach(d => d.dispose()); + vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); + vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions); } private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { @@ -88,16 +85,16 @@ class AstInspector implements vscode.HoverProvider, Disposable { border: "#ffffff 1px solid", }); private rustEditor: undefined | RustEditor; - private readonly disposables: Disposable[] = []; - constructor() { - this.disposables.push(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this)); - vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables); - vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); + constructor(ctx: Ctx) { + ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this)); + vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions); + vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions); + + ctx.pushCleanup(this); } dispose() { this.setRustEditor(undefined); - this.disposables.forEach(d => d.dispose()); } private onDidCloseTextDocument(doc: vscode.TextDocument) { From e86bfc0995a0c52eeed2703d33a9089032b68c77 Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 18:26:53 +0300 Subject: [PATCH 3/6] vscode: add docs about syntax tree --- docs/user/features.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/user/features.md b/docs/user/features.md index 56d2969fd4..8aeec2e81e 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -81,6 +81,12 @@ Join selected lines into one, smartly fixing up whitespace and trailing commas. Shows the parse tree of the current file. It exists mostly for debugging rust-analyzer itself. +You can hover over syntax nodes in the opened text file to see the appropriate +rust code that it refers to and the rust editor will also highlight the proper +text range. + +demo + #### Expand Macro Recursively Shows the full macro expansion of the macro at current cursor. From 611adc83dae6a2e50ac4c4fde8ef814ca1c56273 Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 19:00:23 +0300 Subject: [PATCH 4/6] Simplify --- crates/ra_hir_ty/src/display.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/crates/ra_hir_ty/src/display.rs b/crates/ra_hir_ty/src/display.rs index c3d92a268f..13ecd537ad 100644 --- a/crates/ra_hir_ty/src/display.rs +++ b/crates/ra_hir_ty/src/display.rs @@ -190,8 +190,6 @@ impl HirDisplay for ApplicationTy { }; write!(f, "{}", name)?; if self.parameters.len() > 0 { - write!(f, "<")?; - let mut non_default_parameters = Vec::with_capacity(self.parameters.len()); let parameters_to_write = if f.omit_verbose_types() { match self @@ -200,8 +198,8 @@ impl HirDisplay for ApplicationTy { .map(|generic_def_id| f.db.generic_defaults(generic_def_id)) .filter(|defaults| !defaults.is_empty()) { - Option::None => self.parameters.0.as_ref(), - Option::Some(default_parameters) => { + None => self.parameters.0.as_ref(), + Some(default_parameters) => { for (i, parameter) in self.parameters.iter().enumerate() { match (parameter, default_parameters.get(i)) { (&Ty::Unknown, _) | (_, None) => { @@ -221,7 +219,7 @@ impl HirDisplay for ApplicationTy { } else { self.parameters.0.as_ref() }; - + write!(f, "<")?; f.write_joined(parameters_to_write, ", ")?; write!(f, ">")?; } @@ -231,9 +229,9 @@ impl HirDisplay for ApplicationTy { AssocContainerId::TraitId(it) => it, _ => panic!("not an associated type"), }; - let trait_name = f.db.trait_data(trait_).name.clone(); - let name = f.db.type_alias_data(type_alias).name.clone(); - write!(f, "{}::{}", trait_name, name)?; + let trait_ = f.db.trait_data(trait_); + let type_alias = f.db.type_alias_data(type_alias); + write!(f, "{}::{}", trait_.name, type_alias.name)?; if self.parameters.len() > 0 { write!(f, "<")?; f.write_joined(&*self.parameters.0, ", ")?; @@ -266,8 +264,8 @@ impl HirDisplay for ProjectionTy { return write!(f, "{}", TYPE_HINT_TRUNCATION); } - let trait_name = f.db.trait_data(self.trait_(f.db)).name.clone(); - write!(f, "<{} as {}", self.parameters[0].display(f.db), trait_name,)?; + let trait_ = f.db.trait_data(self.trait_(f.db)); + write!(f, "<{} as {}", self.parameters[0].display(f.db), trait_.name)?; if self.parameters.len() > 1 { write!(f, "<")?; f.write_joined(&self.parameters[1..], ", ")?; @@ -312,7 +310,7 @@ impl HirDisplay for Ty { Ty::Opaque(_) => write!(f, "impl ")?, _ => unreachable!(), }; - write_bounds_like_dyn_trait(&predicates, f)?; + write_bounds_like_dyn_trait(predicates, f)?; } Ty::Unknown => write!(f, "{{unknown}}")?, Ty::Infer(..) => write!(f, "_")?, @@ -345,7 +343,7 @@ fn write_bounds_like_dyn_trait( // We assume that the self type is $0 (i.e. the // existential) here, which is the only thing that's // possible in actual Rust, and hence don't print it - write!(f, "{}", f.db.trait_data(trait_ref.trait_).name.clone())?; + write!(f, "{}", f.db.trait_data(trait_ref.trait_).name)?; if trait_ref.substs.len() > 1 { write!(f, "<")?; f.write_joined(&trait_ref.substs[1..], ", ")?; @@ -362,9 +360,8 @@ fn write_bounds_like_dyn_trait( write!(f, "<")?; angle_open = true; } - let name = - f.db.type_alias_data(projection_pred.projection_ty.associated_ty).name.clone(); - write!(f, "{} = ", name)?; + let type_alias = f.db.type_alias_data(projection_pred.projection_ty.associated_ty); + write!(f, "{} = ", type_alias.name)?; projection_pred.ty.hir_fmt(f)?; } GenericPredicate::Error => { @@ -398,7 +395,7 @@ impl TraitRef { } else { write!(f, ": ")?; } - write!(f, "{}", f.db.trait_data(self.trait_).name.clone())?; + write!(f, "{}", f.db.trait_data(self.trait_).name)?; if self.substs.len() > 1 { write!(f, "<")?; f.write_joined(&self.substs[1..], ", ")?; From 3b09768ebcd7ea6523c58c92e32198ae5b18e11c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Tue, 31 Mar 2020 19:06:07 +0300 Subject: [PATCH 5/6] vscode: apply review nits --- editors/code/src/commands/syntax_tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index eba5111930..91d97f9c4f 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -98,7 +98,7 @@ class AstInspector implements vscode.HoverProvider, Disposable { } private onDidCloseTextDocument(doc: vscode.TextDocument) { - if (!!this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { + if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { this.setRustEditor(undefined); } } From f3612b7024e5828bdd37fb6d39c1b4ebe989e819 Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 20:28:10 +0300 Subject: [PATCH 6/6] vscode: scroll to the syntax node in rust editor when highlighting --- editors/code/src/commands/syntax_tree.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index eba5111930..e443c5e542 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -127,6 +127,7 @@ class AstInspector implements vscode.HoverProvider, Disposable { if (!rustTextRange) return; this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]); + this.rustEditor.revealRange(rustTextRange); const rustSourceCode = this.rustEditor.document.getText(rustTextRange); const astTextRange = this.findAstRange(astTextLine); @@ -145,7 +146,7 @@ class AstInspector implements vscode.HoverProvider, Disposable { const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine); if (!parsedRange) return; - const [, begin, end] = parsedRange.map(off => doc.positionAt(+off)); + const [begin, end] = parsedRange.slice(1).map(off => doc.positionAt(+off)); return new vscode.Range(begin, end); }