Skip to content

Commit 19f6e24

Browse files
fix
1 parent b430a08 commit 19f6e24

3 files changed

Lines changed: 200 additions & 37 deletions

File tree

pyrefly/lib/lsp/non_wasm/server.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ use pyrefly_build::handle::Handle;
197197
use pyrefly_config::config::ConfigSource;
198198
use pyrefly_config::error_kind::Severity;
199199
use pyrefly_python::PYTHON_EXTENSIONS;
200+
use pyrefly_python::ast::Ast;
201+
use pyrefly_python::module::Module;
200202
use pyrefly_python::module::TextRangeWithModule;
201203
use pyrefly_python::module_name::ModuleName;
202204
use pyrefly_python::module_name::ModuleNameWithKind;
@@ -314,9 +316,12 @@ use crate::lsp::wasm::provide_type::ProvideTypeResponse;
314316
use crate::lsp::wasm::provide_type::provide_type;
315317
use crate::module::bundled::BundledStub;
316318
use crate::state::load::LspFile;
319+
use crate::state::loader::LoaderFindCache;
317320
use crate::state::lsp::DisplayTypeErrors;
318321
use crate::state::lsp::FindDefinitionItemWithDocstring;
319322
use crate::state::lsp::FindPreference;
323+
use crate::state::lsp::IdentifierContext;
324+
use crate::state::lsp::IdentifierWithContext;
320325
use crate::state::lsp::ImportBehavior;
321326
use crate::state::lsp::LocalRefactorCodeAction;
322327
use crate::state::notebook::LspNotebook;
@@ -1825,6 +1830,28 @@ impl Server {
18251830
return Ok(ProcessEvent::Continue);
18261831
}
18271832

1833+
if let Some(params) = as_request::<GotoDefinition>(&x) {
1834+
if let Some(params) = self
1835+
.extract_request_params_or_send_err_response::<GotoDefinition>(
1836+
params, &x.id,
1837+
)
1838+
{
1839+
if let Some(response) = self.goto_definition_import_fast_path(&params) {
1840+
let response = match response {
1841+
Ok(response) => response,
1842+
Err(reason) => {
1843+
telemetry_event.set_empty_response_reason(reason);
1844+
None
1845+
}
1846+
};
1847+
self.send_response(new_response(x.id, Ok(response)));
1848+
return Ok(ProcessEvent::Continue);
1849+
}
1850+
} else {
1851+
return Ok(ProcessEvent::Continue);
1852+
}
1853+
}
1854+
18281855
let mut transaction =
18291856
ide_transaction_manager.non_committable_transaction(&self.state);
18301857

@@ -3933,6 +3960,84 @@ impl Server {
39333960
.map(|(handle, _)| handle)
39343961
}
39353962

3963+
fn module_for_uri_fast_path(&self, uri: &Url, handle: &Handle) -> Option<Module> {
3964+
let path = self.path_for_uri_or_notebook_cell(uri)?;
3965+
if let Some(file) = self.open_files.read().get(&path).duped() {
3966+
return Some(match &*file {
3967+
LspFile::Source(contents) => {
3968+
Module::new(handle.module(), handle.path().dupe(), Arc::clone(contents))
3969+
}
3970+
LspFile::Notebook(notebook) => Module::new_notebook(
3971+
handle.module(),
3972+
handle.path().dupe(),
3973+
Arc::clone(notebook.ruff_notebook()),
3974+
),
3975+
});
3976+
}
3977+
let contents = std::fs::read_to_string(&path).ok()?;
3978+
Some(Module::new(
3979+
handle.module(),
3980+
handle.path().dupe(),
3981+
Arc::new(contents),
3982+
))
3983+
}
3984+
3985+
fn goto_definition_import_fast_path(
3986+
&self,
3987+
params: &GotoDefinitionParams,
3988+
) -> Option<Result<Option<GotoDefinitionResponse>, EmptyResponseReason>> {
3989+
let uri = &params.text_document_position_params.text_document.uri;
3990+
let handle = match self.make_handle_if_enabled(uri, Some(GotoDefinition::METHOD)) {
3991+
Ok(handle) => handle,
3992+
Err(err) => return Some(Err(err.into())),
3993+
};
3994+
let module = match self.module_for_uri_fast_path(uri, &handle) {
3995+
Some(module) => module,
3996+
None => return Some(Err(EmptyResponseReason::ModuleInfoNotFound)),
3997+
};
3998+
let position = module.from_lsp_position(
3999+
params.text_document_position_params.position,
4000+
self.maybe_get_cell_index(uri),
4001+
);
4002+
let ast = Ast::parse(module.contents(), module.source_type()).0;
4003+
let covering_nodes = Ast::locate_node(&ast, position);
4004+
let Some(IdentifierWithContext {
4005+
identifier,
4006+
context: IdentifierContext::ImportedModule { name, dots },
4007+
}) = Transaction::identifier_from_covering_nodes(&covering_nodes)
4008+
else {
4009+
return None;
4010+
};
4011+
let target_module =
4012+
Transaction::target_imported_module_name(&handle, &identifier, name, dots, position);
4013+
let config = self
4014+
.state
4015+
.config_finder()
4016+
.python_file(handle.module_kind(), handle.path());
4017+
let loader = LoaderFindCache::new(config.dupe());
4018+
let Some(target_path) = loader
4019+
.find_import_prefer_executable(target_module, Some(handle.path()))
4020+
.finding()
4021+
else {
4022+
return Some(Err(EmptyResponseReason::ModuleNotFound));
4023+
};
4024+
let Some(path) = to_real_path(&target_path) else {
4025+
return Some(Err(EmptyResponseReason::ModuleInfoNotFound));
4026+
};
4027+
let path = match &self.path_remapper {
4028+
Some(remapper) => remapper(&path).into_owned(),
4029+
None => path,
4030+
};
4031+
let uri = match Url::from_file_path(path.absolutize()) {
4032+
Ok(uri) => uri,
4033+
Err(_) => return Some(Err(EmptyResponseReason::NoFilePath)),
4034+
};
4035+
Some(Ok(Some(GotoDefinitionResponse::Scalar(Location {
4036+
uri,
4037+
range: Range::default(),
4038+
}))))
4039+
}
4040+
39364041
fn goto_definition(
39374042
&self,
39384043
transaction: &Transaction<'_>,

pyrefly/lib/state/lsp.rs

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ impl<'a> Transaction<'a> {
682682
Self::identifier_from_covering_nodes(&covering_nodes)
683683
}
684684

685-
fn identifier_from_covering_nodes(
685+
pub(crate) fn identifier_from_covering_nodes(
686686
covering_nodes: &[AnyNodeRef],
687687
) -> Option<IdentifierWithContext> {
688688
match (
@@ -825,6 +825,43 @@ impl<'a> Transaction<'a> {
825825
}
826826
}
827827

828+
pub(crate) fn target_imported_module_name(
829+
handle: &Handle,
830+
identifier: &Identifier,
831+
module_name: ModuleName,
832+
dots: u32,
833+
position: TextSize,
834+
) -> ModuleName {
835+
let resolved_module_name = if dots > 0 {
836+
let is_init = handle.path().is_init();
837+
let suffix = if module_name.as_str().is_empty() {
838+
None
839+
} else {
840+
Some(&Name::new(module_name.as_str()))
841+
};
842+
handle
843+
.module()
844+
.new_maybe_relative(is_init, dots, suffix)
845+
.unwrap_or(module_name)
846+
} else {
847+
module_name
848+
};
849+
850+
let components = resolved_module_name.components();
851+
if let Some(idx) = components.iter().position(|c| c == &identifier.id) {
852+
ModuleName::from_parts(&components[..=idx])
853+
} else if identifier.as_str() == resolved_module_name.as_str() {
854+
let module_str = resolved_module_name.as_str();
855+
let offset = (position - identifier.range.start())
856+
.to_usize()
857+
.min(module_str.len());
858+
let idx = module_str[..offset].matches('.').count();
859+
ModuleName::from_parts(&components[..=idx])
860+
} else {
861+
resolved_module_name
862+
}
863+
}
864+
828865
fn callee_at(&self, handle: &Handle, position: TextSize) -> Option<ExprCall> {
829866
let mod_module = self.get_ast(handle)?;
830867
fn f(x: &Expr, find: TextSize, res: &mut Option<ExprCall>) {
@@ -1929,42 +1966,13 @@ impl<'a> Transaction<'a> {
19291966
dots,
19301967
},
19311968
}) => {
1932-
// For relative imports (dots > 0), resolve the module name using
1933-
// the current file's module name as context.
1934-
let resolved_module_name = if dots > 0 {
1935-
let is_init = handle.path().is_init();
1936-
let suffix = if module_name.as_str().is_empty() {
1937-
None
1938-
} else {
1939-
Some(&Name::new(module_name.as_str()))
1940-
};
1941-
handle
1942-
.module()
1943-
.new_maybe_relative(is_init, dots, suffix)
1944-
.unwrap_or(module_name)
1945-
} else {
1946-
module_name
1947-
};
1948-
1949-
// Build the module name for lookup based on identifier position.
1950-
let components = resolved_module_name.components();
1951-
1952-
let target_module_name =
1953-
if let Some(idx) = components.iter().position(|c| c == &identifier.id) {
1954-
// Identifier matches a module component.
1955-
ModuleName::from_parts(&components[..=idx])
1956-
} else if identifier.as_str() == resolved_module_name.as_str() {
1957-
// Identifier matches full module name; decide which component based on position offset.
1958-
let module_str = resolved_module_name.as_str();
1959-
let offset = (position - identifier.range.start())
1960-
.to_usize()
1961-
.min(module_str.len());
1962-
let idx = module_str[..offset].matches('.').count();
1963-
ModuleName::from_parts(&components[..=idx])
1964-
} else {
1965-
// Fallback: use the whole module name.
1966-
resolved_module_name
1967-
};
1969+
let target_module_name = Self::target_imported_module_name(
1970+
handle,
1971+
&identifier,
1972+
module_name,
1973+
dots,
1974+
position,
1975+
);
19681976
match self.find_definition_for_imported_module(
19691977
handle,
19701978
target_module_name,

pyrefly/lib/test/lsp/lsp_interaction/definition.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::fs;
89
use std::path::PathBuf;
910
use std::sync::Arc;
1011

@@ -199,6 +200,55 @@ fn test_go_to_def_relative_path_helper() {
199200
);
200201
}
201202

203+
#[test]
204+
fn definition_on_import_module_while_recheck_is_blocked() {
205+
let root = get_test_files_root();
206+
let root_path = root.path().join("basic");
207+
let scope_uri = Url::from_file_path(&root_path).unwrap();
208+
let bar_path = root_path.join("bar.py");
209+
let bar_contents = fs::read_to_string(&bar_path).unwrap();
210+
let mut interaction = LspInteraction::new_with_indexing_mode(IndexingMode::LazyBlocking);
211+
interaction.set_root(root_path.clone());
212+
interaction
213+
.initialize(InitializeSettings {
214+
configuration: Some(Some(
215+
json!([{"pyrefly": {"displayTypeErrors": "force-on"}}]),
216+
)),
217+
workspace_folders: Some(vec![("test".to_owned(), scope_uri)]),
218+
file_watch: true,
219+
..Default::default()
220+
})
221+
.unwrap();
222+
interaction.client.did_open("bar.py");
223+
interaction
224+
.client
225+
.expect_publish_diagnostics_eventual_error_count(bar_path.clone(), 0)
226+
.expect("Failed to receive initial diagnostics for bar");
227+
interaction.client.did_open("foo.py");
228+
interaction
229+
.client
230+
.expect_publish_diagnostics_eventual_error_count(root_path.join("foo.py"), 0)
231+
.expect("Failed to receive initial diagnostics for foo");
232+
233+
interaction.do_not_commit_next_recheck();
234+
interaction
235+
.client
236+
.edit_file("bar.py", &bar_contents.replace("foo = 3", "foo = 4"));
237+
interaction
238+
.client
239+
.expect_publish_diagnostics_eventual_error_count(bar_path, 0)
240+
.expect("Failed to receive diagnostics while the recheck was blocked");
241+
242+
interaction
243+
.client
244+
.definition("foo.py", 5, 7)
245+
.expect_definition_response_from_root("bar.py", 0, 0, 0, 0)
246+
.unwrap();
247+
248+
interaction.continue_recheck();
249+
interaction.shutdown().unwrap();
250+
}
251+
202252
#[test]
203253
fn definition_in_builtins() {
204254
let root = get_test_files_root();

0 commit comments

Comments
 (0)