[ty] Inlay hint call argument location (#20349)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Matthew Mckee 2025-11-17 10:33:09 +00:00 committed by GitHub
parent 58fa1d71b6
commit 901e9cdf49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1447 additions and 115 deletions

View file

@ -855,7 +855,7 @@ fn convert_resolved_definitions_to_targets(
} }
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => { ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
// For file ranges, navigate to the specific range within the file // For file ranges, navigate to the specific range within the file
crate::NavigationTarget::new(file_range.file(), file_range.range()) crate::NavigationTarget::from(file_range)
} }
}) })
.collect() .collect()

File diff suppressed because it is too large Load diff

View file

@ -132,6 +132,20 @@ impl NavigationTarget {
pub fn full_range(&self) -> TextRange { pub fn full_range(&self) -> TextRange {
self.full_range self.full_range
} }
pub fn full_file_range(&self) -> FileRange {
FileRange::new(self.file, self.full_range)
}
}
impl From<FileRange> for NavigationTarget {
fn from(value: FileRange) -> Self {
Self {
file: value.file(),
focus_range: value.range(),
full_range: value.range(),
}
}
} }
/// Specifies the kind of reference operation. /// Specifies the kind of reference operation.

View file

@ -958,6 +958,22 @@ pub struct CallSignatureDetails<'db> {
pub argument_to_parameter_mapping: Vec<MatchedArgument<'db>>, pub argument_to_parameter_mapping: Vec<MatchedArgument<'db>>,
} }
impl CallSignatureDetails<'_> {
fn get_definition_parameter_range(&self, db: &dyn Db, name: &str) -> Option<FileRange> {
let definition = self.signature.definition()?;
let file = definition.file(db);
let module_ref = parsed_module(db, file).load(db);
let parameters = match definition.kind(db) {
DefinitionKind::Function(node) => &node.node(&module_ref).parameters,
// TODO: lambda functions
_ => return None,
};
Some(FileRange::new(file, parameters.find(name)?.name().range))
}
}
/// Extract signature details from a function call expression. /// Extract signature details from a function call expression.
/// This function analyzes the callable being invoked and returns zero or more /// This function analyzes the callable being invoked and returns zero or more
/// `CallSignatureDetails` objects, each representing one possible signature /// `CallSignatureDetails` objects, each representing one possible signature
@ -1153,15 +1169,16 @@ pub fn find_active_signature_from_details(
} }
#[derive(Default)] #[derive(Default)]
pub struct InlayHintFunctionArgumentDetails { pub struct InlayHintCallArgumentDetails {
pub argument_names: HashMap<usize, String>, /// The position of the arguments mapped to their name and the range of the argument definition in the signature.
pub argument_names: HashMap<usize, (String, Option<FileRange>)>,
} }
pub fn inlay_hint_function_argument_details<'db>( pub fn inlay_hint_call_argument_details<'db>(
db: &'db dyn Db, db: &'db dyn Db,
model: &SemanticModel<'db>, model: &SemanticModel<'db>,
call_expr: &ast::ExprCall, call_expr: &ast::ExprCall,
) -> Option<InlayHintFunctionArgumentDetails> { ) -> Option<InlayHintCallArgumentDetails> {
let signature_details = call_signature_details(db, model, call_expr); let signature_details = call_signature_details(db, model, call_expr);
if signature_details.is_empty() { if signature_details.is_empty() {
@ -1173,6 +1190,7 @@ pub fn inlay_hint_function_argument_details<'db>(
let call_signature_details = signature_details.get(active_signature_index)?; let call_signature_details = signature_details.get(active_signature_index)?;
let parameters = call_signature_details.signature.parameters(); let parameters = call_signature_details.signature.parameters();
let mut argument_names = HashMap::new(); let mut argument_names = HashMap::new();
for arg_index in 0..call_expr.arguments.args.len() { for arg_index in 0..call_expr.arguments.args.len() {
@ -1195,16 +1213,19 @@ pub fn inlay_hint_function_argument_details<'db>(
continue; continue;
}; };
let parameter_label_offset =
call_signature_details.get_definition_parameter_range(db, param.name()?);
// Only add hints for parameters that can be specified by name // Only add hints for parameters that can be specified by name
if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() {
let Some(name) = param.name() else { let Some(name) = param.name() else {
continue; continue;
}; };
argument_names.insert(arg_index, name.to_string()); argument_names.insert(arg_index, (name.to_string(), parameter_label_offset));
} }
} }
Some(InlayHintFunctionArgumentDetails { argument_names }) Some(InlayHintCallArgumentDetails { argument_names })
} }
/// Find the text range of a specific parameter in function parameters by name. /// Find the text range of a specific parameter in function parameters by name.

View file

@ -5,7 +5,8 @@ use lsp_types::{InlayHintParams, Url};
use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints}; use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints};
use ty_project::ProjectDatabase; use ty_project::ProjectDatabase;
use crate::document::{RangeExt, TextSizeExt}; use crate::PositionEncoding;
use crate::document::{RangeExt, TextSizeExt, ToLink};
use crate::server::api::traits::{ use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
}; };
@ -57,7 +58,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
.position .position
.to_lsp_position(db, file, snapshot.encoding())? .to_lsp_position(db, file, snapshot.encoding())?
.local_position(), .local_position(),
label: inlay_hint_label(&hint.label), label: inlay_hint_label(&hint.label, db, snapshot.encoding()),
kind: Some(inlay_hint_kind(&hint.kind)), kind: Some(inlay_hint_kind(&hint.kind)),
tooltip: None, tooltip: None,
padding_left: None, padding_left: None,
@ -81,12 +82,18 @@ fn inlay_hint_kind(inlay_hint_kind: &InlayHintKind) -> lsp_types::InlayHintKind
} }
} }
fn inlay_hint_label(inlay_hint_label: &InlayHintLabel) -> lsp_types::InlayHintLabel { fn inlay_hint_label(
inlay_hint_label: &InlayHintLabel,
db: &ProjectDatabase,
encoding: PositionEncoding,
) -> lsp_types::InlayHintLabel {
let mut label_parts = Vec::new(); let mut label_parts = Vec::new();
for part in inlay_hint_label.parts() { for part in inlay_hint_label.parts() {
label_parts.push(lsp_types::InlayHintLabelPart { label_parts.push(lsp_types::InlayHintLabelPart {
value: part.text().into(), value: part.text().into(),
location: None, location: part
.target()
.and_then(|target| target.to_location(db, encoding)),
tooltip: None, tooltip: None,
command: None, command: None,
}); });

View file

@ -59,7 +59,20 @@ y = foo(1)
}, },
"label": [ "label": [
{ {
"value": "a" "value": "a",
"location": {
"uri": "file://<temp_dir>/src/foo.py",
"range": {
"start": {
"line": 2,
"character": 8
},
"end": {
"line": 2,
"character": 9
}
}
}
}, },
{ {
"value": "=" "value": "="

View file

@ -19,7 +19,7 @@ use ty_ide::{
InlayHintSettings, MarkupKind, RangedValue, document_highlights, goto_declaration, InlayHintSettings, MarkupKind, RangedValue, document_highlights, goto_declaration,
goto_definition, goto_references, goto_type_definition, hover, inlay_hints, goto_definition, goto_references, goto_type_definition, hover, inlay_hints,
}; };
use ty_ide::{NavigationTargets, signature_help}; use ty_ide::{NavigationTarget, NavigationTargets, signature_help};
use ty_project::metadata::options::Options; use ty_project::metadata::options::Options;
use ty_project::metadata::value::ValueSource; use ty_project::metadata::value::ValueSource;
use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
@ -469,7 +469,22 @@ impl Workspace {
Ok(result Ok(result
.into_iter() .into_iter()
.map(|hint| InlayHint { .map(|hint| InlayHint {
markdown: hint.display().to_string(), label: hint
.label
.into_parts()
.into_iter()
.map(|part| InlayHintLabelPart {
location: part.target().map(|target| {
location_link_from_navigation_target(
target,
&self.db,
self.position_encoding,
None,
)
}),
label: part.into_text(),
})
.collect(),
position: Position::from_text_size( position: Position::from_text_size(
hint.position, hint.position,
&index, &index,
@ -639,19 +654,8 @@ fn map_targets_to_links(
targets targets
.into_iter() .into_iter()
.map(|target| LocationLink { .map(|target| {
path: target.file().path(db).to_string(), location_link_from_navigation_target(&target, db, position_encoding, Some(source_range))
full_range: Range::from_file_range(
db,
FileRange::new(target.file(), target.full_range()),
position_encoding,
),
selection_range: Some(Range::from_file_range(
db,
FileRange::new(target.file(), target.focus_range()),
position_encoding,
)),
origin_selection_range: Some(source_range),
}) })
.collect() .collect()
} }
@ -905,6 +909,7 @@ impl From<PositionEncoding> for ruff_source_file::PositionEncoding {
} }
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone)]
pub struct LocationLink { pub struct LocationLink {
/// The target file path /// The target file path
#[wasm_bindgen(getter_with_clone)] #[wasm_bindgen(getter_with_clone)]
@ -918,6 +923,24 @@ pub struct LocationLink {
pub origin_selection_range: Option<Range>, pub origin_selection_range: Option<Range>,
} }
fn location_link_from_navigation_target(
target: &NavigationTarget,
db: &dyn Db,
position_encoding: PositionEncoding,
source_range: Option<Range>,
) -> LocationLink {
LocationLink {
path: target.file().path(db).to_string(),
full_range: Range::from_file_range(db, target.full_file_range(), position_encoding),
selection_range: Some(Range::from_file_range(
db,
FileRange::new(target.file(), target.focus_range()),
position_encoding,
)),
origin_selection_range: source_range,
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hover { pub struct Hover {
@ -1032,16 +1055,25 @@ impl From<ty_ide::InlayHintKind> for InlayHintKind {
} }
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHint { pub struct InlayHint {
#[wasm_bindgen(getter_with_clone)] #[wasm_bindgen(getter_with_clone)]
pub markdown: String, pub label: Vec<InlayHintLabelPart>,
pub position: Position, pub position: Position,
pub kind: InlayHintKind, pub kind: InlayHintKind,
} }
#[wasm_bindgen]
#[derive(Clone)]
pub struct InlayHintLabelPart {
#[wasm_bindgen(getter_with_clone)]
pub label: String,
#[wasm_bindgen(getter_with_clone)]
pub location: Option<LocationLink>,
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticToken { pub struct SemanticToken {

View file

@ -28,6 +28,7 @@ import {
DocumentHighlight, DocumentHighlight,
DocumentHighlightKind, DocumentHighlightKind,
InlayHintKind, InlayHintKind,
LocationLink,
TextEdit, TextEdit,
} from "ty_wasm"; } from "ty_wasm";
import { FileId, ReadonlyFiles } from "../Playground"; import { FileId, ReadonlyFiles } from "../Playground";
@ -445,7 +446,10 @@ class PlaygroundServer
return { return {
dispose: () => {}, dispose: () => {},
hints: inlayHints.map((hint) => ({ hints: inlayHints.map((hint) => ({
label: hint.markdown, label: hint.label.map((part) => ({
label: part.label,
// As of 2025-09-23, location isn't supported by Monaco which is why we don't set it
})),
position: { position: {
lineNumber: hint.position.line, lineNumber: hint.position.line,
column: hint.position.column, column: hint.position.column,
@ -763,57 +767,59 @@ class PlaygroundServer
return null; return null;
} }
private mapNavigationTargets(links: any[]): languages.LocationLink[] { private mapNavigationTarget(link: LocationLink): languages.LocationLink {
const result = links.map((link) => { const uri = Uri.parse(link.path);
const uri = Uri.parse(link.path);
// Pre-create models to ensure peek definition works // Pre-create models to ensure peek definition works
if (this.monaco.editor.getModel(uri) == null) { if (this.monaco.editor.getModel(uri) == null) {
if (uri.scheme === "vendored") { if (uri.scheme === "vendored") {
// Handle vendored files // Handle vendored files
const vendoredPath = this.getVendoredPath(uri); const vendoredPath = this.getVendoredPath(uri);
const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath); const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath);
const content = this.props.workspace.sourceText(fileHandle); const content = this.props.workspace.sourceText(fileHandle);
this.monaco.editor.createModel(content, "python", uri); this.monaco.editor.createModel(content, "python", uri);
} else { } else {
// Handle regular files // Handle regular files
const fileId = this.props.files.index.find((file) => { const fileId = this.props.files.index.find((file) => {
return Uri.file(file.name).toString() === uri.toString(); return Uri.file(file.name).toString() === uri.toString();
})?.id; })?.id;
if (fileId != null) { if (fileId != null) {
const handle = this.props.files.handles[fileId]; const handle = this.props.files.handles[fileId];
if (handle != null) { if (handle != null) {
const language = isPythonFile(handle) ? "python" : undefined; const language = isPythonFile(handle) ? "python" : undefined;
this.monaco.editor.createModel( this.monaco.editor.createModel(
this.props.files.contents[fileId], this.props.files.contents[fileId],
language, language,
uri, uri,
); );
}
} }
} }
} }
}
const targetSelection = const targetSelection =
link.selection_range == null link.selection_range == null
? undefined ? undefined
: tyRangeToMonacoRange(link.selection_range); : tyRangeToMonacoRange(link.selection_range);
const originSelection = const originSelection =
link.origin_selection_range == null link.origin_selection_range == null
? undefined ? undefined
: tyRangeToMonacoRange(link.origin_selection_range); : tyRangeToMonacoRange(link.origin_selection_range);
return { return {
uri: uri, uri: uri,
range: tyRangeToMonacoRange(link.full_range), range: tyRangeToMonacoRange(link.full_range),
targetSelectionRange: targetSelection, targetSelectionRange: targetSelection,
originSelectionRange: originSelection, originSelectionRange: originSelection,
} as languages.LocationLink; } as languages.LocationLink;
}); }
return result; private mapNavigationTargets(
links: LocationLink[],
): languages.LocationLink[] {
return links.map((link) => this.mapNavigationTarget(link));
} }
dispose() { dispose() {