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.
+
+
+
#### 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'