11445: Upstream inlay hints r=lnicola a=lnicola

Closes https://github.com/rust-analyzer/rust-analyzer/issues/2797
Closes https://github.com/rust-analyzer/rust-analyzer/issues/3394 (since now resolve the hints for the range given only, not for the whole document. We don't actually resolve anything due to [hard requirement](https://github.com/rust-analyzer/rust-analyzer/pull/11445#issuecomment-1035227434) on label being immutable. Any further heavy actions could go to the `resolve` method that's now available via the official Code API for hints)

Based on `@SomeoneToIgnore's` branch, with a couple of updates:

 - I squashed, more or less successfully, the commits on that branch
 - downloading the `.d.ts` no longer works, but you can get it manually from https://raw.githubusercontent.com/microsoft/vscode/release/1.64/src/vscode-dts/vscode.proposed.inlayHints.d.ts
 - you might need to pass `--enable-proposed-api matklad.rust-analyzer`
 - if I'm reading the definition right, `InlayHintKind` needs to be serialized as a number, not string
 - this doesn't work anyway -- the client-side gets the hints, but they don't display

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Laurențiu Nicola <lnicola@dend.ro>
This commit is contained in:
bors[bot] 2022-03-07 16:49:12 +00:00 committed by GitHub
commit 49646b71d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 552 additions and 640 deletions

View file

@ -5,7 +5,7 @@ use itertools::Itertools;
use stdx::to_lower_snake_case; use stdx::to_lower_snake_case;
use syntax::{ use syntax::{
ast::{self, AstNode, HasArgList, HasName, UnaryOp}, ast::{self, AstNode, HasArgList, HasName, UnaryOp},
match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, TextRange, T, match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, SyntaxNode, TextRange, T,
}; };
use crate::FileId; use crate::FileId;
@ -58,6 +58,7 @@ pub struct InlayHint {
pub(crate) fn inlay_hints( pub(crate) fn inlay_hints(
db: &RootDatabase, db: &RootDatabase,
file_id: FileId, file_id: FileId,
range_limit: Option<FileRange>,
config: &InlayHintsConfig, config: &InlayHintsConfig,
) -> Vec<InlayHint> { ) -> Vec<InlayHint> {
let _p = profile::span("inlay_hints"); let _p = profile::span("inlay_hints");
@ -65,25 +66,50 @@ pub(crate) fn inlay_hints(
let file = sema.parse(file_id); let file = sema.parse(file_id);
let file = file.syntax(); let file = file.syntax();
let mut res = Vec::new(); let mut hints = Vec::new();
for node in file.descendants() { if let Some(range_limit) = range_limit {
if let Some(expr) = ast::Expr::cast(node.clone()) { let range_limit = range_limit.range;
get_chaining_hints(&mut res, &sema, config, &expr); match file.covering_element(range_limit) {
match expr { NodeOrToken::Token(_) => return hints,
ast::Expr::CallExpr(it) => { NodeOrToken::Node(n) => {
get_param_name_hints(&mut res, &sema, config, ast::Expr::from(it)); for node in n
.descendants()
.filter(|descendant| range_limit.contains_range(descendant.text_range()))
{
get_hints(&mut hints, &sema, config, node);
} }
ast::Expr::MethodCallExpr(it) => {
get_param_name_hints(&mut res, &sema, config, ast::Expr::from(it));
}
_ => (),
} }
} else if let Some(it) = ast::IdentPat::cast(node.clone()) { }
get_bind_pat_hints(&mut res, &sema, config, &it); } else {
for node in file.descendants() {
get_hints(&mut hints, &sema, config, node);
} }
} }
res
hints
}
fn get_hints(
hints: &mut Vec<InlayHint>,
sema: &Semantics<RootDatabase>,
config: &InlayHintsConfig,
node: SyntaxNode,
) {
if let Some(expr) = ast::Expr::cast(node.clone()) {
get_chaining_hints(hints, sema, config, &expr);
match expr {
ast::Expr::CallExpr(it) => {
get_param_name_hints(hints, sema, config, ast::Expr::from(it));
}
ast::Expr::MethodCallExpr(it) => {
get_param_name_hints(hints, sema, config, ast::Expr::from(it));
}
_ => (),
}
} else if let Some(it) = ast::IdentPat::cast(node) {
get_bind_pat_hints(hints, sema, config, &it);
}
} }
fn get_chaining_hints( fn get_chaining_hints(
@ -541,6 +567,8 @@ fn get_callable(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use expect_test::{expect, Expect}; use expect_test::{expect, Expect};
use ide_db::base_db::FileRange;
use syntax::{TextRange, TextSize};
use test_utils::extract_annotations; use test_utils::extract_annotations;
use crate::{fixture, inlay_hints::InlayHintsConfig}; use crate::{fixture, inlay_hints::InlayHintsConfig};
@ -604,7 +632,7 @@ mod tests {
fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) { fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) {
let (analysis, file_id) = fixture::file(ra_fixture); let (analysis, file_id) = fixture::file(ra_fixture);
let expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
let inlay_hints = analysis.inlay_hints(&config, file_id).unwrap(); let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
let actual = let actual =
inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::<Vec<_>>(); inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::<Vec<_>>();
assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual); assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual);
@ -613,7 +641,7 @@ mod tests {
#[track_caller] #[track_caller]
fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) { fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
let (analysis, file_id) = fixture::file(ra_fixture); let (analysis, file_id) = fixture::file(ra_fixture);
let inlay_hints = analysis.inlay_hints(&config, file_id).unwrap(); let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
expect.assert_debug_eq(&inlay_hints) expect.assert_debug_eq(&inlay_hints)
} }
@ -1045,6 +1073,55 @@ fn main() {
) )
} }
#[test]
fn check_hint_range_limit() {
let fixture = r#"
//- minicore: fn, sized
fn foo() -> impl Fn() { loop {} }
fn foo1() -> impl Fn(f64) { loop {} }
fn foo2() -> impl Fn(f64, f64) { loop {} }
fn foo3() -> impl Fn(f64, f64) -> u32 { loop {} }
fn foo4() -> &'static dyn Fn(f64, f64) -> u32 { loop {} }
fn foo5() -> &'static dyn Fn(&'static dyn Fn(f64, f64) -> u32, f64) -> u32 { loop {} }
fn foo6() -> impl Fn(f64, f64) -> u32 + Sized { loop {} }
fn foo7() -> *const (impl Fn(f64, f64) -> u32 + Sized) { loop {} }
fn main() {
let foo = foo();
let foo = foo1();
let foo = foo2();
let foo = foo3();
// ^^^ impl Fn(f64, f64) -> u32
let foo = foo4();
// ^^^ &dyn Fn(f64, f64) -> u32
let foo = foo5();
let foo = foo6();
let foo = foo7();
}
"#;
let (analysis, file_id) = fixture::file(fixture);
let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
let inlay_hints = analysis
.inlay_hints(
&InlayHintsConfig {
parameter_hints: false,
type_hints: true,
chaining_hints: false,
hide_named_constructor_hints: false,
max_length: None,
},
file_id,
Some(FileRange {
file_id,
range: TextRange::new(TextSize::from(500), TextSize::from(600)),
}),
)
.unwrap();
let actual =
inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::<Vec<_>>();
assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual);
}
#[test] #[test]
fn fn_hints_ptr_rpit_fn_parentheses() { fn fn_hints_ptr_rpit_fn_parentheses() {
check_types( check_types(

View file

@ -358,8 +358,9 @@ impl Analysis {
&self, &self,
config: &InlayHintsConfig, config: &InlayHintsConfig,
file_id: FileId, file_id: FileId,
range: Option<FileRange>,
) -> Cancellable<Vec<InlayHint>> { ) -> Cancellable<Vec<InlayHint>> {
self.with_db(|db| inlay_hints::inlay_hints(db, file_id, config)) self.with_db(|db| inlay_hints::inlay_hints(db, file_id, range, config))
} }
/// Returns the set of folding ranges. /// Returns the set of folding ranges.

View file

@ -112,6 +112,7 @@ impl StaticIndex<'_> {
max_length: Some(25), max_length: Some(25),
}, },
file_id, file_id,
None,
) )
.unwrap(); .unwrap();
// hovers // hovers

View file

@ -115,6 +115,7 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities {
experimental: Some(json!({ experimental: Some(json!({
"externalDocs": true, "externalDocs": true,
"hoverRange": true, "hoverRange": true,
"inlayHints": true,
"joinLines": true, "joinLines": true,
"matchingBrace": true, "matchingBrace": true,
"moveItem": true, "moveItem": true,

View file

@ -1318,11 +1318,22 @@ pub(crate) fn handle_inlay_hints(
params: InlayHintsParams, params: InlayHintsParams,
) -> Result<Vec<InlayHint>> { ) -> Result<Vec<InlayHint>> {
let _p = profile::span("handle_inlay_hints"); let _p = profile::span("handle_inlay_hints");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?; let document_uri = &params.text_document.uri;
let file_id = from_proto::file_id(&snap, document_uri)?;
let line_index = snap.file_line_index(file_id)?; let line_index = snap.file_line_index(file_id)?;
let range = params
.range
.map(|range| {
from_proto::file_range(
&snap,
TextDocumentIdentifier::new(document_uri.to_owned()),
range,
)
})
.transpose()?;
Ok(snap Ok(snap
.analysis .analysis
.inlay_hints(&snap.config.inlay_hints(), file_id)? .inlay_hints(&snap.config.inlay_hints(), file_id, range)?
.into_iter() .into_iter()
.map(|it| to_proto::inlay_hint(&line_index, it)) .map(|it| to_proto::inlay_hint(&line_index, it))
.collect()) .collect())

View file

@ -233,27 +233,34 @@ pub enum InlayHints {}
impl Request for InlayHints { impl Request for InlayHints {
type Params = InlayHintsParams; type Params = InlayHintsParams;
type Result = Vec<InlayHint>; type Result = Vec<InlayHint>;
const METHOD: &'static str = "rust-analyzer/inlayHints"; const METHOD: &'static str = "experimental/inlayHints";
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InlayHintsParams { pub struct InlayHintsParams {
pub text_document: TextDocumentIdentifier, pub text_document: TextDocumentIdentifier,
pub range: Option<lsp_types::Range>,
} }
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] #[derive(Eq, PartialEq, Debug, Copy, Clone, Serialize, Deserialize)]
pub enum InlayKind { #[serde(transparent)]
TypeHint, pub struct InlayHintKind(u8);
ParameterHint,
ChainingHint, impl InlayHintKind {
pub const TYPE: InlayHintKind = InlayHintKind(1);
pub const PARAMETER: InlayHintKind = InlayHintKind(2);
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InlayHint { pub struct InlayHint {
pub range: Range,
pub kind: InlayKind,
pub label: String, pub label: String,
pub position: Position,
pub kind: Option<InlayHintKind>,
pub tooltip: Option<String>,
pub padding_left: Option<bool>,
pub padding_right: Option<bool>,
} }
pub enum Ssr {} pub enum Ssr {}

View file

@ -416,12 +416,18 @@ pub(crate) fn signature_help(
pub(crate) fn inlay_hint(line_index: &LineIndex, inlay_hint: InlayHint) -> lsp_ext::InlayHint { pub(crate) fn inlay_hint(line_index: &LineIndex, inlay_hint: InlayHint) -> lsp_ext::InlayHint {
lsp_ext::InlayHint { lsp_ext::InlayHint {
label: inlay_hint.label.to_string(), label: inlay_hint.label.to_string(),
range: range(line_index, inlay_hint.range), position: match inlay_hint.kind {
kind: match inlay_hint.kind { InlayKind::ParameterHint => position(line_index, inlay_hint.range.start()),
InlayKind::ParameterHint => lsp_ext::InlayKind::ParameterHint, _ => position(line_index, inlay_hint.range.end()),
InlayKind::TypeHint => lsp_ext::InlayKind::TypeHint,
InlayKind::ChainingHint => lsp_ext::InlayKind::ChainingHint,
}, },
kind: match inlay_hint.kind {
InlayKind::ParameterHint => Some(lsp_ext::InlayHintKind::PARAMETER),
InlayKind::TypeHint => Some(lsp_ext::InlayHintKind::TYPE),
InlayKind::ChainingHint => None,
},
tooltip: None,
padding_left: Some(true),
padding_right: Some(true),
} }
} }

View file

@ -1,5 +1,5 @@
<!--- <!---
lsp_ext.rs hash: 5b53b92c9f9d6650 lsp_ext.rs hash: e32fdde032ff6ebc
If you need to change the above hash to make the test pass, please check if you If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: need to adjust this doc as well and ping this issue:
@ -562,11 +562,11 @@ Expands macro call at a given position.
## Inlay Hints ## Inlay Hints
**Method:** `rust-analyzer/inlayHints` **Method:** `experimental/inlayHints`
This request is sent from client to server to render "inlay hints" -- virtual text inserted into editor to show things like inferred types. This request is sent from client to server to render "inlay hints" -- virtual text inserted into editor to show things like inferred types.
Generally, the client should re-query inlay hints after every modification. Generally, the client should re-query inlay hints after every modification.
Note that we plan to move this request to `experimental/inlayHints`, as it is not really Rust-specific, but the current API is not necessary the right one. Until it gets upstreamed, this follows the VS Code API.
Upstream issues: https://github.com/microsoft/language-server-protocol/issues/956 , https://github.com/rust-analyzer/rust-analyzer/issues/2797 Upstream issues: https://github.com/microsoft/language-server-protocol/issues/956 , https://github.com/rust-analyzer/rust-analyzer/issues/2797
**Request:** **Request:**
@ -581,9 +581,12 @@ interface InlayHintsParams {
```typescript ```typescript
interface InlayHint { interface InlayHint {
kind: "TypeHint" | "ParameterHint" | "ChainingHint", position: Position;
range: Range, label: string | InlayHintLabelPart[];
label: string, tooltip?: string | MarkdownString | undefined;
kind?: InlayHintKind;
paddingLeft?: boolean;
paddingRight?: boolean;
} }
``` ```

View file

@ -3,3 +3,5 @@ node_modules
server server
.vscode-test/ .vscode-test/
*.vsix *.vsix
bundle
vscode.proposed.d.ts

File diff suppressed because it is too large Load diff

View file

@ -21,9 +21,9 @@
"Programming Languages" "Programming Languages"
], ],
"engines": { "engines": {
"vscode": "^1.63.0" "vscode": "^1.65.0"
}, },
"enableProposedApi": true, "enabledApiProposals": [],
"scripts": { "scripts": {
"vscode:prepublish": "npm run build-base -- --minify", "vscode:prepublish": "npm run build-base -- --minify",
"package": "vsce package -o rust-analyzer.vsix", "package": "vsce package -o rust-analyzer.vsix",
@ -36,18 +36,18 @@
"test": "node ./out/tests/runTests.js" "test": "node ./out/tests/runTests.js"
}, },
"dependencies": { "dependencies": {
"vscode-languageclient": "8.0.0-next.8", "vscode-languageclient": "8.0.0-next.12",
"d3": "^7.3.0", "d3": "^7.3.0",
"d3-graphviz": "^4.0.0" "d3-graphviz": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "~14.17.5", "@types/node": "~14.17.5",
"@types/vscode": "~1.63.0", "@types/vscode": "~1.65.0",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
"@vscode/test-electron": "^2.1.1", "@vscode/test-electron": "^2.1.1",
"esbuild": "^0.14.12", "esbuild": "^0.14.12",
"eslint": "^8.7.0", "eslint": "^8.10.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"typescript-formatter": "^7.2.2", "typescript-formatter": "^7.2.2",
@ -298,11 +298,6 @@
"default": true, "default": true,
"description": "Whether to show inlay hints." "description": "Whether to show inlay hints."
}, },
"rust-analyzer.inlayHints.smallerHints": {
"type": "boolean",
"default": true,
"description": "Whether inlay hints font size should be smaller than editor's font size."
},
"rust-analyzer.server.path": { "rust-analyzer.server.path": {
"type": [ "type": [
"null", "null",
@ -1083,78 +1078,6 @@
} }
], ],
"colors": [ "colors": [
{
"id": "rust_analyzer.inlayHints.foreground",
"description": "Foreground color of inlay hints (is overriden by more specific rust_analyzer.inlayHints.foreground.* configurations)",
"defaults": {
"dark": "#A0A0A0F0",
"light": "#747474",
"highContrast": "#BEBEBE"
}
},
{
"id": "rust_analyzer.inlayHints.background",
"description": "Background color of inlay hints (is overriden by more specific rust_analyzer.inlayHints.background.* configurations)",
"defaults": {
"dark": "#11223300",
"light": "#11223300",
"highContrast": "#11223300"
}
},
{
"id": "rust_analyzer.inlayHints.foreground.typeHints",
"description": "Foreground color of inlay type hints for variables (overrides rust_analyzer.inlayHints.foreground)",
"defaults": {
"dark": "rust_analyzer.inlayHints.foreground",
"light": "rust_analyzer.inlayHints.foreground",
"highContrast": "rust_analyzer.inlayHints.foreground"
}
},
{
"id": "rust_analyzer.inlayHints.foreground.chainingHints",
"description": "Foreground color of inlay type hints for method chains (overrides rust_analyzer.inlayHints.foreground)",
"defaults": {
"dark": "rust_analyzer.inlayHints.foreground",
"light": "rust_analyzer.inlayHints.foreground",
"highContrast": "rust_analyzer.inlayHints.foreground"
}
},
{
"id": "rust_analyzer.inlayHints.foreground.parameterHints",
"description": "Foreground color of function parameter name inlay hints at the call site (overrides rust_analyzer.inlayHints.foreground)",
"defaults": {
"dark": "rust_analyzer.inlayHints.foreground",
"light": "rust_analyzer.inlayHints.foreground",
"highContrast": "rust_analyzer.inlayHints.foreground"
}
},
{
"id": "rust_analyzer.inlayHints.background.typeHints",
"description": "Background color of inlay type hints for variables (overrides rust_analyzer.inlayHints.background)",
"defaults": {
"dark": "rust_analyzer.inlayHints.background",
"light": "rust_analyzer.inlayHints.background",
"highContrast": "rust_analyzer.inlayHints.background"
}
},
{
"id": "rust_analyzer.inlayHints.background.chainingHints",
"description": "Background color of inlay type hints for method chains (overrides rust_analyzer.inlayHints.background)",
"defaults": {
"dark": "rust_analyzer.inlayHints.background",
"light": "rust_analyzer.inlayHints.background",
"highContrast": "rust_analyzer.inlayHints.background"
}
},
{
"id": "rust_analyzer.inlayHints.background.parameterHints",
"description": "Background color of function parameter name inlay hints at the call site (overrides rust_analyzer.inlayHints.background)",
"defaults": {
"dark": "rust_analyzer.inlayHints.background",
"light": "rust_analyzer.inlayHints.background",
"highContrast": "rust_analyzer.inlayHints.background"
}
},
{ {
"id": "rust_analyzer.syntaxTreeBorder", "id": "rust_analyzer.syntaxTreeBorder",
"description": "Color of the border displayed in the Rust source code for the selected syntax node (see \"Show Syntax Tree\" command)", "description": "Color of the border displayed in the Rust source code for the selected syntax node (see \"Show Syntax Tree\" command)",

View file

@ -1,267 +1,54 @@
import * as lc from "vscode-languageclient";
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as ra from './lsp_ext'; import * as ra from './lsp_ext';
import { Ctx, Disposable } from './ctx'; import { Ctx, Disposable } from './ctx';
import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util'; import { sendRequestWithRetry, isRustDocument } from './util';
interface InlayHintStyle {
decorationType: vscode.TextEditorDecorationType;
toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions;
};
interface InlayHintsStyles {
typeHints: InlayHintStyle;
paramHints: InlayHintStyle;
chainingHints: InlayHintStyle;
}
export function activateInlayHints(ctx: Ctx) { export function activateInlayHints(ctx: Ctx) {
const maybeUpdater = { const maybeUpdater = {
updater: null as null | HintsUpdater, hintsProvider: null as Disposable | null,
updateHintsEventEmitter: new vscode.EventEmitter<void>(),
async onConfigChange() { async onConfigChange() {
this.dispose();
const anyEnabled = ctx.config.inlayHints.typeHints const anyEnabled = ctx.config.inlayHints.typeHints
|| ctx.config.inlayHints.parameterHints || ctx.config.inlayHints.parameterHints
|| ctx.config.inlayHints.chainingHints; || ctx.config.inlayHints.chainingHints;
const enabled = ctx.config.inlayHints.enable && anyEnabled; const enabled = ctx.config.inlayHints.enable && anyEnabled;
if (!enabled) return;
if (!enabled) return this.dispose(); const event = this.updateHintsEventEmitter.event;
this.hintsProvider = vscode.languages.registerInlayHintsProvider({ scheme: 'file', language: 'rust' }, new class implements vscode.InlayHintsProvider {
await sleep(100); onDidChangeInlayHints = event;
if (this.updater) { async provideInlayHints(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise<vscode.InlayHint[]> {
this.updater.updateInlayHintsStyles(); const request = { textDocument: { uri: document.uri.toString() }, range: { start: range.start, end: range.end } };
this.updater.syncCacheAndRenderHints(); const hints = await sendRequestWithRetry(ctx.client, ra.inlayHints, request, token).catch(_ => null);
} else { if (hints == null) {
this.updater = new HintsUpdater(ctx); return [];
} } else {
return hints;
}
}
});
}, },
onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
if (contentChanges.length === 0 || !isRustDocument(document)) return;
this.updateHintsEventEmitter.fire();
},
dispose() { dispose() {
this.updater?.dispose(); this.hintsProvider?.dispose();
this.updater = null; this.hintsProvider = null;
} this.updateHintsEventEmitter.dispose();
},
}; };
ctx.pushCleanup(maybeUpdater); ctx.pushCleanup(maybeUpdater);
vscode.workspace.onDidChangeConfiguration( vscode.workspace.onDidChangeConfiguration(maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions);
maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions vscode.workspace.onDidChangeTextDocument(maybeUpdater.onDidChangeTextDocument, maybeUpdater, ctx.subscriptions);
);
maybeUpdater.onConfigChange().catch(console.error); maybeUpdater.onConfigChange().catch(console.error);
} }
function createHintStyle(hintKind: "type" | "parameter" | "chaining", smallerHints: boolean): InlayHintStyle {
// U+200C is a zero-width non-joiner to prevent the editor from forming a ligature
// between code and type hints
const [pos, render] = ({
type: ["after", (label: string) => `\u{200c}: ${label}`],
parameter: ["before", (label: string) => `${label}: `],
chaining: ["after", (label: string) => `\u{200c}: ${label}`],
} as const)[hintKind];
const fg = new vscode.ThemeColor(`rust_analyzer.inlayHints.foreground.${hintKind}Hints`);
const bg = new vscode.ThemeColor(`rust_analyzer.inlayHints.background.${hintKind}Hints`);
return {
decorationType: vscode.window.createTextEditorDecorationType({
[pos]: {
color: fg,
backgroundColor: bg,
fontStyle: "normal",
fontWeight: "normal",
textDecoration: smallerHints ? ";font-size:smaller" : "none",
},
}),
toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
return {
range: conv.asRange(hint.range),
renderOptions: { [pos]: { contentText: render(hint.label) } }
};
}
};
}
const smallHintsStyles = {
typeHints: createHintStyle("type", true),
paramHints: createHintStyle("parameter", true),
chainingHints: createHintStyle("chaining", true),
};
const biggerHintsStyles = {
typeHints: createHintStyle("type", false),
paramHints: createHintStyle("parameter", false),
chainingHints: createHintStyle("chaining", false),
};
class HintsUpdater implements Disposable {
private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
private readonly disposables: Disposable[] = [];
private pendingDisposeDecorations: undefined | InlayHintsStyles = undefined;
private inlayHintsStyles!: InlayHintsStyles;
constructor(private readonly ctx: Ctx) {
vscode.window.onDidChangeVisibleTextEditors(
this.onDidChangeVisibleTextEditors,
this,
this.disposables
);
vscode.workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
this.disposables
);
// Set up initial cache shape
ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set(
editor.document.uri.toString(),
{
document: editor.document,
inlaysRequest: null,
cachedDecorations: null
}
));
this.updateInlayHintsStyles();
this.syncCacheAndRenderHints();
}
dispose() {
this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [], chaining: [] }));
this.disposables.forEach(d => d.dispose());
}
onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
if (contentChanges.length === 0 || !isRustDocument(document)) return;
this.syncCacheAndRenderHints();
}
updateInlayHintsStyles() {
const inlayHintsStyles = this.ctx.config.inlayHints.smallerHints ? smallHintsStyles : biggerHintsStyles;
if (inlayHintsStyles !== this.inlayHintsStyles) {
this.pendingDisposeDecorations = this.inlayHintsStyles;
this.inlayHintsStyles = inlayHintsStyles;
}
}
syncCacheAndRenderHints() {
this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
if (!hints) return;
file.cachedDecorations = this.hintsToDecorations(hints);
for (const editor of this.ctx.visibleRustEditors) {
if (editor.document.uri.toString() === uri) {
this.renderDecorations(editor, file.cachedDecorations);
}
}
}));
}
onDidChangeVisibleTextEditors() {
const newSourceFiles = new Map<string, RustSourceFile>();
// Rerendering all, even up-to-date editors for simplicity
this.ctx.visibleRustEditors.forEach(async editor => {
const uri = editor.document.uri.toString();
const file = this.sourceFiles.get(uri) ?? {
document: editor.document,
inlaysRequest: null,
cachedDecorations: null
};
newSourceFiles.set(uri, file);
// No text documents changed, so we may try to use the cache
if (!file.cachedDecorations) {
const hints = await this.fetchHints(file);
if (!hints) return;
file.cachedDecorations = this.hintsToDecorations(hints);
}
this.renderDecorations(editor, file.cachedDecorations);
});
// Cancel requests for no longer visible (disposed) source files
this.sourceFiles.forEach((file, uri) => {
if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
});
this.sourceFiles = newSourceFiles;
}
private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) {
const { typeHints, paramHints, chainingHints } = this.inlayHintsStyles;
if (this.pendingDisposeDecorations !== undefined) {
const { typeHints, paramHints, chainingHints } = this.pendingDisposeDecorations;
editor.setDecorations(typeHints.decorationType, []);
editor.setDecorations(paramHints.decorationType, []);
editor.setDecorations(chainingHints.decorationType, []);
}
editor.setDecorations(typeHints.decorationType, decorations.type);
editor.setDecorations(paramHints.decorationType, decorations.param);
editor.setDecorations(chainingHints.decorationType, decorations.chaining);
}
private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
const { typeHints, paramHints, chainingHints } = this.inlayHintsStyles;
const decorations: InlaysDecorations = { type: [], param: [], chaining: [] };
const conv = this.ctx.client.protocol2CodeConverter;
for (const hint of hints) {
switch (hint.kind) {
case ra.InlayHint.Kind.TypeHint: {
decorations.type.push(typeHints.toDecoration(hint, conv));
continue;
}
case ra.InlayHint.Kind.ParamHint: {
decorations.param.push(paramHints.toDecoration(hint, conv));
continue;
}
case ra.InlayHint.Kind.ChainingHint: {
decorations.chaining.push(chainingHints.toDecoration(hint, conv));
continue;
}
}
}
return decorations;
}
private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
file.inlaysRequest?.cancel();
const tokenSource = new vscode.CancellationTokenSource();
file.inlaysRequest = tokenSource;
const request = { textDocument: { uri: file.document.uri.toString() } };
return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
.catch(_ => null)
.finally(() => {
if (file.inlaysRequest === tokenSource) {
file.inlaysRequest = null;
}
});
}
}
interface InlaysDecorations {
type: vscode.DecorationOptions[];
param: vscode.DecorationOptions[];
chaining: vscode.DecorationOptions[];
}
interface RustSourceFile {
/**
* Source of the token to cancel in-flight inlay hints request if any.
*/
inlaysRequest: null | vscode.CancellationTokenSource;
/**
* Last applied decorations.
*/
cachedDecorations: null | InlaysDecorations;
document: RustDocument;
}

View file

@ -2,6 +2,7 @@
* This file mirrors `crates/rust-analyzer/src/lsp_ext.rs` declarations. * This file mirrors `crates/rust-analyzer/src/lsp_ext.rs` declarations.
*/ */
import { InlayHint } from "vscode";
import * as lc from "vscode-languageclient"; import * as lc from "vscode-languageclient";
export interface AnalyzerStatusParams { export interface AnalyzerStatusParams {
@ -99,26 +100,11 @@ export interface TestInfo {
export const relatedTests = new lc.RequestType<lc.TextDocumentPositionParams, TestInfo[], void>("rust-analyzer/relatedTests"); export const relatedTests = new lc.RequestType<lc.TextDocumentPositionParams, TestInfo[], void>("rust-analyzer/relatedTests");
export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
export namespace InlayHint {
export const enum Kind {
TypeHint = "TypeHint",
ParamHint = "ParameterHint",
ChainingHint = "ChainingHint",
}
interface Common {
range: lc.Range;
label: string;
}
export type TypeHint = Common & { kind: Kind.TypeHint };
export type ParamHint = Common & { kind: Kind.ParamHint };
export type ChainingHint = Common & { kind: Kind.ChainingHint };
}
export interface InlayHintsParams { export interface InlayHintsParams {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;
range: lc.Range;
} }
export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void>("rust-analyzer/inlayHints"); export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void>("experimental/inlayHints");
export interface SsrParams { export interface SsrParams {
query: string; query: string;