diff --git a/crates/ra_hir_ty/src/display.rs b/crates/ra_hir_ty/src/display.rs index c3d92a268fd..13ecd537ad3 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..], ", ")?; diff --git a/docs/user/features.md b/docs/user/features.md index 56d2969fd43..8aeec2e81ef 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. diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index 2e08e8f115b..996c7a71697 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, - ), - ); + void new AstInspector(ctx); - 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(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,126 @@ 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(); + readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); + readonly eventEmitter = new vscode.EventEmitter(); + constructor(private readonly ctx: Ctx) { + vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); + vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions); } - 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; + + 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); + } + + 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]); + this.rustEditor.revealRange(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.slice(1).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 978a31751ec..6f91f81d63e 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'