From 55d1dad08b1a0b1dd12b87440359825b44c327b5 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 11 Nov 2025 18:53:14 +0900 Subject: [PATCH 1/6] impl ? --- crates/pyrefly_types/src/display.rs | 16 ++ pyrefly/lib/lsp/non_wasm/server.rs | 14 +- pyrefly/lib/state/lsp.rs | 196 +++++++++++++++++- pyrefly/lib/test/lsp/inlay_hint.rs | 4 +- .../test/lsp/lsp_interaction/inlay_hint.rs | 48 +++-- .../lsp_interaction/notebook_inlay_hint.rs | 21 +- 6 files changed, 272 insertions(+), 27 deletions(-) diff --git a/crates/pyrefly_types/src/display.rs b/crates/pyrefly_types/src/display.rs index 6a674da76d..74d1658dd1 100644 --- a/crates/pyrefly_types/src/display.rs +++ b/crates/pyrefly_types/src/display.rs @@ -7,9 +7,11 @@ //! Display a type. The complexity comes from if we have two classes with the same name, //! we want to display disambiguating information (e.g. module name or location). +use std::cell::RefCell; use std::fmt; use std::fmt::Display; +use dupe::Dupe; use pyrefly_python::module_name::ModuleName; use pyrefly_python::qname::QName; use pyrefly_util::display::Fmt; @@ -87,6 +89,7 @@ pub struct TypeDisplayContext<'a> { /// Should we display for IDE Hover? This makes type names more readable but less precise. hover: bool, always_display_module_name: bool, + display_modules: RefCell>, } impl<'a> TypeDisplayContext<'a> { @@ -232,6 +235,9 @@ impl<'a> TypeDisplayContext<'a> { name: &str, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { + self.display_modules + .borrow_mut() + .insert(ModuleName::from_str(module)); if self.always_display_module_name { write!(f, "{module}.{name}") } else { @@ -239,6 +245,16 @@ impl<'a> TypeDisplayContext<'a> { } } + pub fn referenced_modules(&self) -> SmallSet { + let mut modules = self.display_modules.borrow().clone(); + for info in self.qnames.values() { + for module in info.info.keys() { + modules.insert(module.dupe()); + } + } + modules + } + fn fmt_helper<'b>( &self, t: &'b Type, diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index 24236a223b..1f08a4db1c 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -205,8 +205,6 @@ use crate::lsp::non_wasm::module_helpers::module_info_to_uri; use crate::lsp::non_wasm::queue::HeavyTaskQueue; use crate::lsp::non_wasm::queue::LspEvent; use crate::lsp::non_wasm::queue::LspQueue; -use crate::lsp::non_wasm::stdlib::is_python_stdlib_file; -use crate::lsp::non_wasm::stdlib::should_show_stdlib_error; use crate::lsp::non_wasm::transaction_manager::TransactionManager; use crate::lsp::non_wasm::unsaved_file_tracker::UnsavedFileTracker; use crate::lsp::non_wasm::will_rename_files::will_rename_files; @@ -2643,6 +2641,18 @@ impl Server { let position = info.to_lsp_position(text_size); // The range is half-open, so the end position is exclusive according to the spec. if position >= range.start && position < range.end { + let mut text_edits = Vec::with_capacity(1 + x.import_edits.len()); + text_edits.push(TextEdit { + range: Range::new(position, position), + new_text: x.label.clone(), + }); + for (offset, import_text) in x.import_edits { + let insert_position = info.to_lsp_position(offset); + text_edits.push(TextEdit { + range: Range::new(insert_position, insert_position), + new_text: import_text, + }); + } Some(InlayHint { position, label: InlayHintLabel::String(label_text.clone()), diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 11ce942dfe..3a95e32d0f 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -58,6 +58,7 @@ use ruff_python_ast::Keyword; use ruff_python_ast::ModModule; use ruff_python_ast::ParameterWithDefault; use ruff_python_ast::Stmt; +use ruff_python_ast::StmtImport; use ruff_python_ast::StmtImportFrom; use ruff_python_ast::UnaryOp; use ruff_python_ast::name::Name; @@ -67,6 +68,7 @@ use ruff_text_size::TextSize; use serde::Deserialize; use starlark_map::ordered_set::OrderedSet; use starlark_map::small_map::SmallMap; +use starlark_map::small_set::SmallSet; use crate::alt::attr::AttrDefinition; use crate::alt::attr::AttrInfo; @@ -91,6 +93,7 @@ use crate::state::state::CancellableTransaction; use crate::state::state::Transaction; use crate::types::callable::Param; use crate::types::callable::Params; +use crate::types::display::TypeDisplayContext; use crate::types::module::ModuleType; use crate::types::types::Type; @@ -334,6 +337,13 @@ pub enum AnnotationKind { Variable, } +#[derive(Clone, Debug)] +pub struct InlayHintWithEdits { + pub position: TextSize, + pub label: String, + pub import_edits: Vec<(TextSize, String)>, +} + #[derive(Debug)] pub struct ParameterAnnotation { pub text_size: TextSize, @@ -495,6 +505,145 @@ impl IdentifierWithContext { } } +#[derive(Default)] +struct ImportTracker { + canonical_modules: SmallSet, + alias_modules: Vec<(ModuleName, String)>, +} + +impl ImportTracker { + fn from_ast(ast: &ModModule) -> Self { + let mut tracker = Self::default(); + for stmt in &ast.body { + if let Stmt::Import(stmt_import) = stmt { + tracker.record_import(stmt_import); + } + } + tracker + .alias_modules + .sort_by_key(|(module, _)| Reverse(module.as_str().len())); + tracker + } + + fn record_import(&mut self, stmt_import: &StmtImport) { + for alias in &stmt_import.names { + let module_name = ModuleName::from_str(alias.name.as_str()); + if let Some(asname) = &alias.asname { + self.alias_modules + .push((module_name, asname.id.to_string())); + } else { + self.canonical_modules.insert(module_name); + } + } + } + + fn apply_aliases(&self, text: &str) -> String { + if self.alias_modules.is_empty() { + return text.to_owned(); + } + let bytes = text.as_bytes(); + let mut result = String::with_capacity(text.len()); + let mut i = 0; + while i < bytes.len() { + let mut replaced = false; + for (module, alias) in &self.alias_modules { + let module_str = module.as_str(); + if module_str.is_empty() { + continue; + } + let module_bytes = module_str.as_bytes(); + if i + module_bytes.len() <= bytes.len() + && &bytes[i..i + module_bytes.len()] == module_bytes + && Self::is_boundary(bytes, i, i + module_bytes.len()) + { + result.push_str(alias); + i += module_bytes.len(); + replaced = true; + break; + } + } + if !replaced { + result.push(bytes[i] as char); + i += 1; + } + } + result + } + + fn missing_modules( + &self, + modules: &SmallSet, + current_module: ModuleName, + ) -> SmallSet { + let mut missing = SmallSet::new(); + for module in modules.iter() { + let module = module.dupe(); + if module.as_str().is_empty() + || module == current_module + || module == ModuleName::builtins() + || module == ModuleName::extra_builtins() + { + continue; + } + if self.module_is_imported(&module) { + continue; + } + missing.insert(module); + } + missing + } + + fn module_is_imported(&self, module: &ModuleName) -> bool { + self.alias_for(module).is_some() || self.has_canonical(module) + } + + fn alias_for(&self, module: &ModuleName) -> Option { + let target = module.as_str(); + for (alias_module, alias_name) in &self.alias_modules { + let alias_module_str = alias_module.as_str(); + if alias_module_str.is_empty() { + continue; + } + if target == alias_module_str { + return Some(alias_name.clone()); + } + if target.len() > alias_module_str.len() + && target.starts_with(alias_module_str) + && target.as_bytes()[alias_module_str.len()] == b'.' + { + let remainder = &target[alias_module_str.len()..]; + return Some(format!("{alias_name}{remainder}")); + } + } + None + } + + fn has_canonical(&self, module: &ModuleName) -> bool { + let target = module.as_str(); + self.canonical_modules.iter().any(|imported| { + let imported_str = imported.as_str(); + imported_str == target + || (target.len() > imported_str.len() + && target.starts_with(imported_str) + && target.as_bytes()[imported_str.len()] == b'.') + }) + } + + fn is_boundary(bytes: &[u8], start: usize, end: usize) -> bool { + (start == 0 || !Self::is_ident(bytes[start - 1])) + && (end == bytes.len() || !Self::is_ident(bytes[end])) + } + + fn is_ident(byte: u8) -> bool { + matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') + } +} + +struct RenderedTypeHint { + text: String, + import_edits: Vec<(TextSize, String)>, +} + #[derive(Debug, Clone)] pub struct FindDefinitionItemWithDocstring { pub metadata: DefinitionMetadata, @@ -3220,6 +3369,9 @@ impl<'a> Transaction<'a> { } }; let bindings = self.get_bindings(handle)?; + let ast_arc = self.get_ast(handle); + let ast_ref = ast_arc.as_deref(); + let import_tracker = ast_ref.map(ImportTracker::from_ast); let mut res = Vec::new(); for idx in bindings.keys::() { match bindings.idx_to_key(idx) { @@ -3308,8 +3460,8 @@ impl<'a> Transaction<'a> { fn add_inlay_hints_for_positional_function_args( &self, handle: &Handle, - ) -> Vec<(TextSize, String)> { - let mut param_hints: Vec<(TextSize, String)> = Vec::new(); + ) -> Vec { + let mut param_hints: Vec = Vec::new(); if let Some(mod_module) = self.get_ast(handle) { let function_calls = Self::collect_function_calls_from_ast(mod_module); @@ -3348,8 +3500,11 @@ impl<'a> Transaction<'a> { && name.as_str() != "self" && name.as_str() != "cls" { - param_hints - .push((arg.range().start(), format!("{}= ", name.as_str()))); + param_hints.push(InlayHintWithEdits { + position: arg.range().start(), + label: format!("{}= ", name.as_str()), + import_edits: Vec::new(), + }); } } } @@ -3357,10 +3512,41 @@ impl<'a> Transaction<'a> { } } - param_hints.sort_by_key(|(pos, _)| *pos); + param_hints.sort_by_key(|hint| hint.position); param_hints } + fn render_type_hint( + &self, + ty: &Type, + handle: &Handle, + tracker: Option<&ImportTracker>, + ast: Option<&ModModule>, + ) -> RenderedTypeHint { + let (mut text, modules) = Self::format_type_for_annotation(ty); + let mut import_edits = Vec::new(); + if let (Some(tracker), Some(ast)) = (tracker, ast) { + text = tracker.apply_aliases(&text); + for module in tracker + .missing_modules(&modules, handle.module()) + .into_iter() + { + if let Some(handle_to_import) = self.import_handle(handle, module, None).finding() { + let (position, insert_text) = import_regular_import_edit(ast, handle_to_import); + import_edits.push((position, insert_text)); + } + } + } + RenderedTypeHint { text, import_edits } + } + + fn format_type_for_annotation(ty: &Type) -> (String, SmallSet) { + let mut ctx = TypeDisplayContext::new(&[ty]); + ctx.always_display_module_name_except_builtins(); + let text = ctx.display(ty).to_string(); + (text, ctx.referenced_modules()) + } + pub fn semantic_tokens( &self, handle: &Handle, diff --git a/pyrefly/lib/test/lsp/inlay_hint.rs b/pyrefly/lib/test/lsp/inlay_hint.rs index f8bb5e08ec..fc84f7c288 100644 --- a/pyrefly/lib/test/lsp/inlay_hint.rs +++ b/pyrefly/lib/test/lsp/inlay_hint.rs @@ -27,9 +27,9 @@ fn generate_inlay_hint_report(code: &str, hint_config: InlayHintConfig) -> Strin .inlay_hints(handle, hint_config) .unwrap() { - report.push_str(&code_frame_of_source_at_position(code, pos)); + report.push_str(&code_frame_of_source_at_position(code, hint.position)); report.push_str(" inlay-hint: `"); - report.push_str(&hint); + report.push_str(&hint.label); report.push_str("`\n\n"); } report.push('\n'); diff --git a/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs b/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs index fbd7aacb9b..0611dc6d69 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs @@ -30,27 +30,39 @@ fn test_inlay_hint_default_config() { id: interaction.server.current_request_id(), result: Some(serde_json::json!([ { - "label":" -> tuple[Literal[1], Literal[2]]", + "label":" -> tuple[typing.Literal[1], typing.Literal[2]]", "position":{"character":21,"line":6}, "textEdits":[{ - "newText":" -> tuple[Literal[1], Literal[2]]", + "newText":" -> tuple[typing.Literal[1], typing.Literal[2]]", "range":{"end":{"character":21,"line":6},"start":{"character":21,"line":6}} + }, + { + "newText":"import typing\n", + "range":{"end":{"character":0,"line":6},"start":{"character":0,"line":6}} }] }, { - "label":": tuple[Literal[1], Literal[2]]", + "label":": tuple[typing.Literal[1], typing.Literal[2]]", "position":{"character":6,"line":11}, "textEdits":[{ - "newText":": tuple[Literal[1], Literal[2]]", + "newText":": tuple[typing.Literal[1], typing.Literal[2]]", "range":{"end":{"character":6,"line":11},"start":{"character":6,"line":11}} + }, + { + "newText":"import typing\n", + "range":{"end":{"character":0,"line":6},"start":{"character":0,"line":6}} }] }, { - "label":" -> Literal[0]", + "label":" -> typing.Literal[0]", "position":{"character":15,"line":14}, "textEdits":[{ - "newText":" -> Literal[0]", + "newText":" -> typing.Literal[0]", "range":{"end":{"character":15,"line":14},"start":{"character":15,"line":14}} + }, + { + "newText":"import typing\n", + "range":{"end":{"character":0,"line":6},"start":{"character":0,"line":6}} }] } ])), @@ -153,19 +165,27 @@ fn test_inlay_hint_disable_variables() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label":" -> tuple[Literal[1], Literal[2]]", + "label":" -> tuple[typing.Literal[1], typing.Literal[2]]", "position":{"character":21,"line":6}, "textEdits":[{ - "newText":" -> tuple[Literal[1], Literal[2]]", + "newText":" -> tuple[typing.Literal[1], typing.Literal[2]]", "range":{"end":{"character":21,"line":6},"start":{"character":21,"line":6}} + }, + { + "newText":"import typing\n", + "range":{"end":{"character":0,"line":6},"start":{"character":0,"line":6}} }] }, { - "label":" -> Literal[0]", + "label":" -> typing.Literal[0]", "position":{"character":15,"line":14}, "textEdits":[{ - "newText":" -> Literal[0]", + "newText":" -> typing.Literal[0]", "range":{"end":{"character":15,"line":14},"start":{"character":15,"line":14}} + }, + { + "newText":"import typing\n", + "range":{"end":{"character":0,"line":6},"start":{"character":0,"line":6}} }] }])), error: None, @@ -199,11 +219,15 @@ fn test_inlay_hint_disable_returns() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label":": tuple[Literal[1], Literal[2]]", + "label":": tuple[typing.Literal[1], typing.Literal[2]]", "position":{"character":6,"line":11}, "textEdits":[{ - "newText":": tuple[Literal[1], Literal[2]]", + "newText":": tuple[typing.Literal[1], typing.Literal[2]]", "range":{"end":{"character":6,"line":11},"start":{"character":6,"line":11}} + }, + { + "newText":"import typing\n", + "range":{"end":{"character":0,"line":6},"start":{"character":0,"line":6}} }] }])), error: None, diff --git a/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs b/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs index 22effd4f83..ab4f60bff5 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs @@ -33,11 +33,14 @@ fn test_inlay_hints() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label": " -> tuple[Literal[1], Literal[2]]", + "label": " -> tuple[typing.Literal[1], typing.Literal[2]]", "position": {"character": 21, "line": 0}, "textEdits": [{ - "newText": " -> tuple[Literal[1], Literal[2]]", + "newText": " -> tuple[typing.Literal[1], typing.Literal[2]]", "range": {"end": {"character": 21, "line": 0}, "start": {"character": 21, "line": 0}} + }, { + "newText": "import typing\n", + "range": {"end": {"character": 0, "line": 0}, "start": {"character": 0, "line": 0}} }] }])), error: None, @@ -47,11 +50,14 @@ fn test_inlay_hints() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label": ": tuple[Literal[1], Literal[2]]", + "label": ": tuple[typing.Literal[1], typing.Literal[2]]", "position": {"character": 6, "line": 0}, "textEdits": [{ - "newText": ": tuple[Literal[1], Literal[2]]", + "newText": ": tuple[typing.Literal[1], typing.Literal[2]]", "range": {"end": {"character": 6, "line": 0}, "start": {"character": 6, "line": 0}} + }, { + "newText": "import typing\n", + "range": {"end": {"character": 0, "line": 0}, "start": {"character": 0, "line": 0}} }] }])), error: None, @@ -61,11 +67,14 @@ fn test_inlay_hints() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label": " -> Literal[0]", + "label": " -> typing.Literal[0]", "position": {"character": 15, "line": 0}, "textEdits": [{ - "newText": " -> Literal[0]", + "newText": " -> typing.Literal[0]", "range": {"end": {"character": 15, "line": 0}, "start": {"character": 15, "line": 0}} + }, { + "newText": "import typing\n", + "range": {"end": {"character": 0, "line": 0}, "start": {"character": 0, "line": 0}} }] }])), error: None, From b907b4c9c27c63ecccd2ccfe0203dc43c2bede91 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 14 Nov 2025 04:17:26 +0900 Subject: [PATCH 2/6] fix import --- pyrefly/lib/lsp/non_wasm/server.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index 1f08a4db1c..4ad52d5ed7 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -205,6 +205,7 @@ use crate::lsp::non_wasm::module_helpers::module_info_to_uri; use crate::lsp::non_wasm::queue::HeavyTaskQueue; use crate::lsp::non_wasm::queue::LspEvent; use crate::lsp::non_wasm::queue::LspQueue; +use crate::lsp::non_wasm::stdlib::should_show_stdlib_error; use crate::lsp::non_wasm::transaction_manager::TransactionManager; use crate::lsp::non_wasm::unsaved_file_tracker::UnsavedFileTracker; use crate::lsp::non_wasm::will_rename_files::will_rename_files; From 98a417c1daeff3d2d88e74cd8ab6096cc98c286c Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 14 Nov 2025 04:20:01 +0900 Subject: [PATCH 3/6] fix --- pyrefly/lib/lsp/non_wasm/server.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index 4ad52d5ed7..c1229c3fa3 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -205,6 +205,7 @@ use crate::lsp::non_wasm::module_helpers::module_info_to_uri; use crate::lsp::non_wasm::queue::HeavyTaskQueue; use crate::lsp::non_wasm::queue::LspEvent; use crate::lsp::non_wasm::queue::LspQueue; +use crate::lsp::non_wasm::stdlib::is_python_stdlib_file; use crate::lsp::non_wasm::stdlib::should_show_stdlib_error; use crate::lsp::non_wasm::transaction_manager::TransactionManager; use crate::lsp::non_wasm::unsaved_file_tracker::UnsavedFileTracker; From 14b0eedbb4a17ba81807e95613c89a4714357b0c Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 14 Nov 2025 04:25:41 +0900 Subject: [PATCH 4/6] clippy --- pyrefly/lib/state/lsp.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 3a95e32d0f..150c60cf8d 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -585,7 +585,7 @@ impl ImportTracker { { continue; } - if self.module_is_imported(&module) { + if self.module_is_imported(module) { continue; } missing.insert(module); @@ -593,11 +593,11 @@ impl ImportTracker { missing } - fn module_is_imported(&self, module: &ModuleName) -> bool { + fn module_is_imported(&self, module: ModuleName) -> bool { self.alias_for(module).is_some() || self.has_canonical(module) } - fn alias_for(&self, module: &ModuleName) -> Option { + fn alias_for(&self, module: ModuleName) -> Option { let target = module.as_str(); for (alias_module, alias_name) in &self.alias_modules { let alias_module_str = alias_module.as_str(); @@ -618,7 +618,7 @@ impl ImportTracker { None } - fn has_canonical(&self, module: &ModuleName) -> bool { + fn has_canonical(&self, module: ModuleName) -> bool { let target = module.as_str(); self.canonical_modules.iter().any(|imported| { let imported_str = imported.as_str(); From 2cec39af48feac6acc58eae5076589fc7fd7c52b Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 18 Nov 2025 19:29:06 +0900 Subject: [PATCH 5/6] fix --- crates/pyrefly_types/src/display.rs | 7 +- pyrefly/lib/lsp/non_wasm/server.rs | 24 +- pyrefly/lib/playground.rs | 9 +- pyrefly/lib/state/import_tracker.rs | 212 ++++++++++++++++++ pyrefly/lib/state/lsp.rs | 202 +++-------------- pyrefly/lib/state/mod.rs | 1 + pyrefly/lib/test/lsp/inlay_hint.rs | 2 +- .../test/lsp/lsp_interaction/inlay_hint.rs | 24 +- .../lsp_interaction/notebook_inlay_hint.rs | 12 +- 9 files changed, 287 insertions(+), 206 deletions(-) create mode 100644 pyrefly/lib/state/import_tracker.rs diff --git a/crates/pyrefly_types/src/display.rs b/crates/pyrefly_types/src/display.rs index 74d1658dd1..50a8600586 100644 --- a/crates/pyrefly_types/src/display.rs +++ b/crates/pyrefly_types/src/display.rs @@ -89,7 +89,8 @@ pub struct TypeDisplayContext<'a> { /// Should we display for IDE Hover? This makes type names more readable but less precise. hover: bool, always_display_module_name: bool, - display_modules: RefCell>, + /// Modules encountered while formatting, used downstream (e.g. to decide which imports are required). + modules: RefCell>, } impl<'a> TypeDisplayContext<'a> { @@ -235,7 +236,7 @@ impl<'a> TypeDisplayContext<'a> { name: &str, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { - self.display_modules + self.modules .borrow_mut() .insert(ModuleName::from_str(module)); if self.always_display_module_name { @@ -246,7 +247,7 @@ impl<'a> TypeDisplayContext<'a> { } pub fn referenced_modules(&self) -> SmallSet { - let mut modules = self.display_modules.borrow().clone(); + let mut modules = self.modules.borrow().clone(); for info in self.qnames.values() { for module in info.info.keys() { modules.insert(module.dupe()); diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index c1229c3fa3..fb3db90017 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -2260,12 +2260,11 @@ impl Server { // Group consecutive implementations by module, preserving the sorted order let mut grouped: Vec<(ModuleInfo, Vec)> = Vec::new(); for impl_with_module in implementations { - if let Some((last_module, ranges)) = grouped.last_mut() { - if last_module.path() == impl_with_module.module.path() { + if let Some((last_module, ranges)) = grouped.last_mut() + && last_module.path() == impl_with_module.module.path() { ranges.push(impl_with_module.range); continue; } - } grouped.push((impl_with_module.module, vec![impl_with_module.range])); } Ok(grouped) @@ -2635,20 +2634,20 @@ impl Server { )?; let res = t .into_iter() - .filter_map(|(text_size, label_text, _locations)| { + .filter_map(|hint| { // If the url is a notebook cell, filter out inlay hints for other cells - if info.to_cell_for_lsp(text_size) != maybe_cell_idx { + if info.to_cell_for_lsp(hint.position) != maybe_cell_idx { return None; } - let position = info.to_lsp_position(text_size); + let position = info.to_lsp_position(hint.position); // The range is half-open, so the end position is exclusive according to the spec. if position >= range.start && position < range.end { - let mut text_edits = Vec::with_capacity(1 + x.import_edits.len()); + let mut text_edits = Vec::with_capacity(1 + hint.import_edits.len()); text_edits.push(TextEdit { range: Range::new(position, position), - new_text: x.label.clone(), + new_text: hint.label.clone(), }); - for (offset, import_text) in x.import_edits { + for (offset, import_text) in hint.import_edits { let insert_position = info.to_lsp_position(offset); text_edits.push(TextEdit { range: Range::new(insert_position, insert_position), @@ -2657,12 +2656,9 @@ impl Server { } Some(InlayHint { position, - label: InlayHintLabel::String(label_text.clone()), + label: InlayHintLabel::String(hint.label), kind: None, - text_edits: Some(vec![TextEdit { - range: Range::new(position, position), - new_text: label_text, - }]), + text_edits: Some(text_edits), tooltip: None, padding_left: None, padding_right: None, diff --git a/pyrefly/lib/playground.rs b/pyrefly/lib/playground.rs index d9a84081cb..905666bd85 100644 --- a/pyrefly/lib/playground.rs +++ b/pyrefly/lib/playground.rs @@ -521,9 +521,12 @@ impl Playground { .get_module_info(handle) .zip(transaction.inlay_hints(handle, Default::default())) .map(|(info, hints)| { - hints.into_map(|(position, label, _locations)| { - let position = Position::from_display_pos(info.display_pos(position)); - InlayHint { label, position } + hints.into_map(|hint| { + let position = Position::from_display_pos(info.display_pos(hint.position)); + InlayHint { + label: hint.label, + position, + } }) }) .unwrap_or_default() diff --git a/pyrefly/lib/state/import_tracker.rs b/pyrefly/lib/state/import_tracker.rs new file mode 100644 index 0000000000..c6bab25a19 --- /dev/null +++ b/pyrefly/lib/state/import_tracker.rs @@ -0,0 +1,212 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +//! Helpers for harvesting imports and formatting type strings for inlay hints. + +use std::cmp::Reverse; + +use dupe::Dupe; +use pyrefly_python::module_name::ModuleName; +use ruff_python_ast::ModModule; +use ruff_python_ast::Stmt; +use ruff_python_ast::StmtImport; +use starlark_map::small_set::SmallSet; + +use crate::types::display::TypeDisplayContext; +use crate::types::types::Type; + +/// Tracks imports already present in a module and can determine which modules are still missing +/// for a given set of referenced modules. Also supports alias-aware replacement when displaying +/// type strings. +#[derive(Default)] +pub struct ImportTracker { + canonical_modules: SmallSet, + alias_modules: Vec<(ModuleName, String)>, +} + +impl ImportTracker { + /// Build an import tracker from the top-level `import ...` statements in a module. + pub fn from_ast(ast: &ModModule) -> Self { + let mut tracker = Self::default(); + for stmt in &ast.body { + if let Stmt::Import(stmt_import) = stmt { + tracker.record_import(stmt_import); + } + } + tracker + .alias_modules + .sort_by_key(|(module, _)| Reverse(module.as_str().len())); + tracker + } + + /// Record an `import ...` statement into the tracker. + pub fn record_import(&mut self, stmt_import: &StmtImport) { + for alias in &stmt_import.names { + let module_name = ModuleName::from_str(alias.name.as_str()); + if let Some(asname) = &alias.asname { + self.alias_modules + .push((module_name, asname.id.to_string())); + } else { + self.canonical_modules.insert(module_name); + } + } + } + + /// Replace any module prefixes that have been imported under an alias (e.g. `import typing as t`). + pub fn apply_aliases(&self, text: &str) -> String { + if self.alias_modules.is_empty() { + return text.to_owned(); + } + let bytes = text.as_bytes(); + let mut result = String::with_capacity(text.len()); + let mut i = 0; + while i < bytes.len() { + let mut replaced = false; + for (module, alias) in &self.alias_modules { + let module_str = module.as_str(); + if module_str.is_empty() { + continue; + } + let module_bytes = module_str.as_bytes(); + if i + module_bytes.len() <= bytes.len() + && &bytes[i..i + module_bytes.len()] == module_bytes + && Self::is_boundary(bytes, i, i + module_bytes.len()) + { + result.push_str(alias); + i += module_bytes.len(); + replaced = true; + break; + } + } + if !replaced { + result.push(bytes[i] as char); + i += 1; + } + } + result + } + + /// Modules that are referenced in the type string but not yet imported (excluding builtins/current). + pub fn missing_modules( + &self, + modules: &SmallSet, + current_module: ModuleName, + ) -> SmallSet { + let mut missing = SmallSet::new(); + for module in modules.iter() { + let module = module.dupe(); + if module.as_str().is_empty() + || module == current_module + || module == ModuleName::builtins() + || module == ModuleName::extra_builtins() + { + continue; + } + if self.module_is_imported(module) { + continue; + } + missing.insert(module); + } + missing + } + + fn module_is_imported(&self, module: ModuleName) -> bool { + self.alias_for(module).is_some() || self.has_canonical(module) + } + + fn alias_for(&self, module: ModuleName) -> Option { + let target = module.as_str(); + for (alias_module, alias_name) in &self.alias_modules { + let alias_module_str = alias_module.as_str(); + if alias_module_str.is_empty() { + continue; + } + if target == alias_module_str { + return Some(alias_name.clone()); + } + if target.len() > alias_module_str.len() + && target.starts_with(alias_module_str) + && target.as_bytes()[alias_module_str.len()] == b'.' + { + let remainder = &target[alias_module_str.len()..]; + return Some(format!("{alias_name}{remainder}")); + } + } + None + } + + fn has_canonical(&self, module: ModuleName) -> bool { + let target = module.as_str(); + self.canonical_modules.iter().any(|imported| { + let imported_str = imported.as_str(); + imported_str == target + || (target.len() > imported_str.len() + && target.starts_with(imported_str) + && target.as_bytes()[imported_str.len()] == b'.') + }) + } + + fn is_boundary(bytes: &[u8], start: usize, end: usize) -> bool { + (start == 0 || !Self::is_ident(bytes[start - 1])) + && (end == bytes.len() || !Self::is_ident(bytes[end])) + } + + fn is_ident(byte: u8) -> bool { + matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') + } +} + +/// Produce a user-facing type string (without module qualifiers) together with all referenced modules +/// (captured with module qualification) so callers can insert the necessary imports. +pub fn format_type_for_annotation(ty: &Type) -> (String, SmallSet) { + // First pass: force module names so referenced_modules collects everything, but ignore the text. + let mut module_ctx = TypeDisplayContext::new(&[ty]); + module_ctx.always_display_module_name_except_builtins(); + let _ = module_ctx.display(ty).to_string(); + let modules = module_ctx.referenced_modules(); + + // Second pass: produce a concise label without module qualifiers. + let display_ctx = TypeDisplayContext::new(&[ty]); + let text = display_ctx.display(ty).to_string(); + (text, modules) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aliases_are_applied_at_boundaries_only() { + let module = ModuleName::from_str("typing"); + let mut tracker = ImportTracker::default(); + tracker.alias_modules.push((module, "t".to_owned())); + assert_eq!(tracker.apply_aliases("typing.Literal"), "t.Literal"); + // Do not replace inside longer identifiers + assert_eq!(tracker.apply_aliases("mytyping"), "mytyping"); + } + + #[test] + fn missing_modules_skips_builtin_and_current() { + let tracker = ImportTracker::default(); + let mut modules = SmallSet::new(); + let current = ModuleName::from_str("pkg.mod"); + modules.insert(current.dupe()); + modules.insert(ModuleName::builtins()); + modules.insert(ModuleName::from_str("typing")); + let missing = tracker.missing_modules(&modules, current); + assert!(missing.contains(&ModuleName::from_str("typing"))); + assert_eq!(missing.len(), 1); + } + + #[test] + fn format_type_collects_modules_but_returns_short_label() { + let ty = Type::LiteralString; + let (text, modules) = format_type_for_annotation(&ty); + assert_eq!(text, "LiteralString"); + assert!(modules.contains(&ModuleName::from_str("typing"))); + } +} diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 150c60cf8d..247f1b9a3b 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -58,7 +58,6 @@ use ruff_python_ast::Keyword; use ruff_python_ast::ModModule; use ruff_python_ast::ParameterWithDefault; use ruff_python_ast::Stmt; -use ruff_python_ast::StmtImport; use ruff_python_ast::StmtImportFrom; use ruff_python_ast::UnaryOp; use ruff_python_ast::name::Name; @@ -68,7 +67,6 @@ use ruff_text_size::TextSize; use serde::Deserialize; use starlark_map::ordered_set::OrderedSet; use starlark_map::small_map::SmallMap; -use starlark_map::small_set::SmallSet; use crate::alt::attr::AttrDefinition; use crate::alt::attr::AttrInfo; @@ -84,6 +82,8 @@ use crate::state::ide::IntermediateDefinition; use crate::state::ide::import_regular_import_edit; use crate::state::ide::insert_import_edit; use crate::state::ide::key_to_intermediate_definition; +use crate::state::import_tracker::ImportTracker; +use crate::state::import_tracker::format_type_for_annotation; use crate::state::lsp_attributes::AttributeContext; use crate::state::require::Require; use crate::state::semantic_tokens::SemanticTokenBuilder; @@ -93,7 +93,6 @@ use crate::state::state::CancellableTransaction; use crate::state::state::Transaction; use crate::types::callable::Param; use crate::types::callable::Params; -use crate::types::display::TypeDisplayContext; use crate::types::module::ModuleType; use crate::types::types::Type; @@ -344,6 +343,11 @@ pub struct InlayHintWithEdits { pub import_edits: Vec<(TextSize, String)>, } +struct RenderedTypeHint { + text: String, + import_edits: Vec<(TextSize, String)>, +} + #[derive(Debug)] pub struct ParameterAnnotation { pub text_size: TextSize, @@ -505,145 +509,6 @@ impl IdentifierWithContext { } } -#[derive(Default)] -struct ImportTracker { - canonical_modules: SmallSet, - alias_modules: Vec<(ModuleName, String)>, -} - -impl ImportTracker { - fn from_ast(ast: &ModModule) -> Self { - let mut tracker = Self::default(); - for stmt in &ast.body { - if let Stmt::Import(stmt_import) = stmt { - tracker.record_import(stmt_import); - } - } - tracker - .alias_modules - .sort_by_key(|(module, _)| Reverse(module.as_str().len())); - tracker - } - - fn record_import(&mut self, stmt_import: &StmtImport) { - for alias in &stmt_import.names { - let module_name = ModuleName::from_str(alias.name.as_str()); - if let Some(asname) = &alias.asname { - self.alias_modules - .push((module_name, asname.id.to_string())); - } else { - self.canonical_modules.insert(module_name); - } - } - } - - fn apply_aliases(&self, text: &str) -> String { - if self.alias_modules.is_empty() { - return text.to_owned(); - } - let bytes = text.as_bytes(); - let mut result = String::with_capacity(text.len()); - let mut i = 0; - while i < bytes.len() { - let mut replaced = false; - for (module, alias) in &self.alias_modules { - let module_str = module.as_str(); - if module_str.is_empty() { - continue; - } - let module_bytes = module_str.as_bytes(); - if i + module_bytes.len() <= bytes.len() - && &bytes[i..i + module_bytes.len()] == module_bytes - && Self::is_boundary(bytes, i, i + module_bytes.len()) - { - result.push_str(alias); - i += module_bytes.len(); - replaced = true; - break; - } - } - if !replaced { - result.push(bytes[i] as char); - i += 1; - } - } - result - } - - fn missing_modules( - &self, - modules: &SmallSet, - current_module: ModuleName, - ) -> SmallSet { - let mut missing = SmallSet::new(); - for module in modules.iter() { - let module = module.dupe(); - if module.as_str().is_empty() - || module == current_module - || module == ModuleName::builtins() - || module == ModuleName::extra_builtins() - { - continue; - } - if self.module_is_imported(module) { - continue; - } - missing.insert(module); - } - missing - } - - fn module_is_imported(&self, module: ModuleName) -> bool { - self.alias_for(module).is_some() || self.has_canonical(module) - } - - fn alias_for(&self, module: ModuleName) -> Option { - let target = module.as_str(); - for (alias_module, alias_name) in &self.alias_modules { - let alias_module_str = alias_module.as_str(); - if alias_module_str.is_empty() { - continue; - } - if target == alias_module_str { - return Some(alias_name.clone()); - } - if target.len() > alias_module_str.len() - && target.starts_with(alias_module_str) - && target.as_bytes()[alias_module_str.len()] == b'.' - { - let remainder = &target[alias_module_str.len()..]; - return Some(format!("{alias_name}{remainder}")); - } - } - None - } - - fn has_canonical(&self, module: ModuleName) -> bool { - let target = module.as_str(); - self.canonical_modules.iter().any(|imported| { - let imported_str = imported.as_str(); - imported_str == target - || (target.len() > imported_str.len() - && target.starts_with(imported_str) - && target.as_bytes()[imported_str.len()] == b'.') - }) - } - - fn is_boundary(bytes: &[u8], start: usize, end: usize) -> bool { - (start == 0 || !Self::is_ident(bytes[start - 1])) - && (end == bytes.len() || !Self::is_ident(bytes[end])) - } - - fn is_ident(byte: u8) -> bool { - matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') - } -} - -struct RenderedTypeHint { - text: String, - import_edits: Vec<(TextSize, String)>, -} - #[derive(Debug, Clone)] pub struct FindDefinitionItemWithDocstring { pub metadata: DefinitionMetadata, @@ -3345,7 +3210,7 @@ impl<'a> Transaction<'a> { &self, handle: &Handle, inlay_hint_config: InlayHintConfig, - ) -> Option>)>> { + ) -> Option> { let is_interesting = |e: &Expr, ty: &Type, class_name: Option<&Name>| { !ty.is_any() && match e { @@ -3369,10 +3234,9 @@ impl<'a> Transaction<'a> { } }; let bindings = self.get_bindings(handle)?; - let ast_arc = self.get_ast(handle); - let ast_ref = ast_arc.as_deref(); - let import_tracker = ast_ref.map(ImportTracker::from_ast); - let mut res = Vec::new(); + let ast = self.get_ast(handle); + let import_tracker = ast.as_deref().map(ImportTracker::from_ast); + let mut res: Vec = Vec::new(); for idx in bindings.keys::() { match bindings.idx_to_key(idx) { key @ Key::ReturnType(id) => { @@ -3392,11 +3256,17 @@ impl<'a> Transaction<'a> { { ty = return_ty; } - res.push(( - fun.def.parameters.range.end(), - format!(" -> {ty}"), - None, // Location info will be added in a later diff - )); + let rendered = self.render_type_hint( + &ty, + handle, + import_tracker.as_ref(), + ast.as_deref(), + ); + res.push(InlayHintWithEdits { + position: fun.def.parameters.range.end(), + label: format!(" -> {}", rendered.text), + import_edits: rendered.import_edits, + }); } } _ => {} @@ -3425,8 +3295,17 @@ impl<'a> Transaction<'a> { if let Some(e) = e && is_interesting(e, &ty, class_name) { - let ty = format!(": {ty}"); - res.push((key.range().end(), ty, None)); // Location info will be added in a later diff + let rendered = self.render_type_hint( + &ty, + handle, + import_tracker.as_ref(), + ast.as_deref(), + ); + res.push(InlayHintWithEdits { + position: key.range().end(), + label: format!(": {}", rendered.text), + import_edits: rendered.import_edits, + }); } } _ => {} @@ -3434,11 +3313,7 @@ impl<'a> Transaction<'a> { } if inlay_hint_config.call_argument_names != AllOffPartial::Off { - res.extend( - self.add_inlay_hints_for_positional_function_args(handle) - .into_iter() - .map(|(pos, text)| (pos, text, None)), - ); + res.extend(self.add_inlay_hints_for_positional_function_args(handle)); } Some(res) @@ -3523,7 +3398,7 @@ impl<'a> Transaction<'a> { tracker: Option<&ImportTracker>, ast: Option<&ModModule>, ) -> RenderedTypeHint { - let (mut text, modules) = Self::format_type_for_annotation(ty); + let (mut text, modules) = format_type_for_annotation(ty); let mut import_edits = Vec::new(); if let (Some(tracker), Some(ast)) = (tracker, ast) { text = tracker.apply_aliases(&text); @@ -3540,13 +3415,6 @@ impl<'a> Transaction<'a> { RenderedTypeHint { text, import_edits } } - fn format_type_for_annotation(ty: &Type) -> (String, SmallSet) { - let mut ctx = TypeDisplayContext::new(&[ty]); - ctx.always_display_module_name_except_builtins(); - let text = ctx.display(ty).to_string(); - (text, ctx.referenced_modules()) - } - pub fn semantic_tokens( &self, handle: &Handle, diff --git a/pyrefly/lib/state/mod.rs b/pyrefly/lib/state/mod.rs index 358473b832..f6bc86516c 100644 --- a/pyrefly/lib/state/mod.rs +++ b/pyrefly/lib/state/mod.rs @@ -9,6 +9,7 @@ pub mod dirty; pub mod epoch; pub mod errors; pub mod ide; +pub mod import_tracker; pub mod load; pub mod loader; pub mod lsp; diff --git a/pyrefly/lib/test/lsp/inlay_hint.rs b/pyrefly/lib/test/lsp/inlay_hint.rs index fc84f7c288..51765b5254 100644 --- a/pyrefly/lib/test/lsp/inlay_hint.rs +++ b/pyrefly/lib/test/lsp/inlay_hint.rs @@ -22,7 +22,7 @@ fn generate_inlay_hint_report(code: &str, hint_config: InlayHintConfig) -> Strin report.push_str(name); report.push_str(".py\n"); let handle = handles.get(name).unwrap(); - for (pos, hint, _) in state + for hint in state .transaction() .inlay_hints(handle, hint_config) .unwrap() diff --git a/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs b/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs index 0611dc6d69..2781fde6be 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/inlay_hint.rs @@ -30,10 +30,10 @@ fn test_inlay_hint_default_config() { id: interaction.server.current_request_id(), result: Some(serde_json::json!([ { - "label":" -> tuple[typing.Literal[1], typing.Literal[2]]", + "label":" -> tuple[Literal[1], Literal[2]]", "position":{"character":21,"line":6}, "textEdits":[{ - "newText":" -> tuple[typing.Literal[1], typing.Literal[2]]", + "newText":" -> tuple[Literal[1], Literal[2]]", "range":{"end":{"character":21,"line":6},"start":{"character":21,"line":6}} }, { @@ -42,10 +42,10 @@ fn test_inlay_hint_default_config() { }] }, { - "label":": tuple[typing.Literal[1], typing.Literal[2]]", + "label":": tuple[Literal[1], Literal[2]]", "position":{"character":6,"line":11}, "textEdits":[{ - "newText":": tuple[typing.Literal[1], typing.Literal[2]]", + "newText":": tuple[Literal[1], Literal[2]]", "range":{"end":{"character":6,"line":11},"start":{"character":6,"line":11}} }, { @@ -54,10 +54,10 @@ fn test_inlay_hint_default_config() { }] }, { - "label":" -> typing.Literal[0]", + "label":" -> Literal[0]", "position":{"character":15,"line":14}, "textEdits":[{ - "newText":" -> typing.Literal[0]", + "newText":" -> Literal[0]", "range":{"end":{"character":15,"line":14},"start":{"character":15,"line":14}} }, { @@ -165,10 +165,10 @@ fn test_inlay_hint_disable_variables() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label":" -> tuple[typing.Literal[1], typing.Literal[2]]", + "label":" -> tuple[Literal[1], Literal[2]]", "position":{"character":21,"line":6}, "textEdits":[{ - "newText":" -> tuple[typing.Literal[1], typing.Literal[2]]", + "newText":" -> tuple[Literal[1], Literal[2]]", "range":{"end":{"character":21,"line":6},"start":{"character":21,"line":6}} }, { @@ -177,10 +177,10 @@ fn test_inlay_hint_disable_variables() { }] }, { - "label":" -> typing.Literal[0]", + "label":" -> Literal[0]", "position":{"character":15,"line":14}, "textEdits":[{ - "newText":" -> typing.Literal[0]", + "newText":" -> Literal[0]", "range":{"end":{"character":15,"line":14},"start":{"character":15,"line":14}} }, { @@ -219,10 +219,10 @@ fn test_inlay_hint_disable_returns() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label":": tuple[typing.Literal[1], typing.Literal[2]]", + "label":": tuple[Literal[1], Literal[2]]", "position":{"character":6,"line":11}, "textEdits":[{ - "newText":": tuple[typing.Literal[1], typing.Literal[2]]", + "newText":": tuple[Literal[1], Literal[2]]", "range":{"end":{"character":6,"line":11},"start":{"character":6,"line":11}} }, { diff --git a/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs b/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs index ab4f60bff5..a81853292c 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/notebook_inlay_hint.rs @@ -33,10 +33,10 @@ fn test_inlay_hints() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label": " -> tuple[typing.Literal[1], typing.Literal[2]]", + "label": " -> tuple[Literal[1], Literal[2]]", "position": {"character": 21, "line": 0}, "textEdits": [{ - "newText": " -> tuple[typing.Literal[1], typing.Literal[2]]", + "newText": " -> tuple[Literal[1], Literal[2]]", "range": {"end": {"character": 21, "line": 0}, "start": {"character": 21, "line": 0}} }, { "newText": "import typing\n", @@ -50,10 +50,10 @@ fn test_inlay_hints() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label": ": tuple[typing.Literal[1], typing.Literal[2]]", + "label": ": tuple[Literal[1], Literal[2]]", "position": {"character": 6, "line": 0}, "textEdits": [{ - "newText": ": tuple[typing.Literal[1], typing.Literal[2]]", + "newText": ": tuple[Literal[1], Literal[2]]", "range": {"end": {"character": 6, "line": 0}, "start": {"character": 6, "line": 0}} }, { "newText": "import typing\n", @@ -67,10 +67,10 @@ fn test_inlay_hints() { interaction.client.expect_response(Response { id: interaction.server.current_request_id(), result: Some(serde_json::json!([{ - "label": " -> typing.Literal[0]", + "label": " -> Literal[0]", "position": {"character": 15, "line": 0}, "textEdits": [{ - "newText": " -> typing.Literal[0]", + "newText": " -> Literal[0]", "range": {"end": {"character": 15, "line": 0}, "start": {"character": 15, "line": 0}} }, { "newText": "import typing\n", From 1b9a66d1e9dc46ee1845cd3174b73585805fa715 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 18 Nov 2025 19:35:02 +0900 Subject: [PATCH 6/6] fmt --- pyrefly/lib/lsp/non_wasm/server.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index 34a3d1b884..fb249aa8f7 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -2243,10 +2243,11 @@ impl Server { let mut grouped: Vec<(ModuleInfo, Vec)> = Vec::new(); for impl_with_module in implementations { if let Some((last_module, ranges)) = grouped.last_mut() - && last_module.path() == impl_with_module.module.path() { - ranges.push(impl_with_module.range); - continue; - } + && last_module.path() == impl_with_module.module.path() + { + ranges.push(impl_with_module.range); + continue; + } grouped.push((impl_with_module.module, vec![impl_with_module.range])); } Ok(grouped)