mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 04:29:47 +00:00
[ty] Inlay hint call argument location (#20349)
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
58fa1d71b6
commit
901e9cdf49
8 changed files with 1447 additions and 115 deletions
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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": "="
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue