Skip to content

Commit 011421e

Browse files
fix
1 parent 9832cb7 commit 011421e

7 files changed

Lines changed: 317 additions & 0 deletions

File tree

pyrefly/lib/lsp/non_wasm/server.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ use lsp_types::CodeActionParams;
4040
use lsp_types::CodeActionProviderCapability;
4141
use lsp_types::CodeActionResponse;
4242
use lsp_types::CodeActionTriggerKind;
43+
use lsp_types::CodeLens;
44+
use lsp_types::CodeLensOptions;
45+
use lsp_types::CodeLensParams;
46+
use lsp_types::Command;
4347
use lsp_types::CompletionItem;
4448
use lsp_types::CompletionList;
4549
use lsp_types::CompletionOptions;
@@ -152,6 +156,7 @@ use lsp_types::request::CallHierarchyIncomingCalls;
152156
use lsp_types::request::CallHierarchyOutgoingCalls;
153157
use lsp_types::request::CallHierarchyPrepare;
154158
use lsp_types::request::CodeActionRequest;
159+
use lsp_types::request::CodeLensRequest;
155160
use lsp_types::request::Completion;
156161
use lsp_types::request::DocumentDiagnosticRequest;
157162
use lsp_types::request::DocumentHighlightRequest;
@@ -949,6 +954,12 @@ struct InitializeResult<C> {
949954
server_info: Option<ServerInfo>,
950955
}
951956

957+
#[derive(Debug, Clone)]
958+
struct CodeLensTarget {
959+
range: Range,
960+
definition: FindDefinitionItemWithDocstring,
961+
}
962+
952963
pub fn initialize_finish<C: Serialize>(
953964
sender: &Sender<Message>,
954965
reader: &mut MessageReader,
@@ -1164,6 +1175,14 @@ pub fn capabilities(
11641175
]),
11651176
..Default::default()
11661177
})),
1178+
code_lens_provider: match indexing_mode {
1179+
IndexingMode::None => None,
1180+
IndexingMode::LazyNonBlockingBackground | IndexingMode::LazyBlocking => {
1181+
Some(CodeLensOptions {
1182+
resolve_provider: Some(false),
1183+
})
1184+
}
1185+
},
11671186
completion_provider: Some(CompletionOptions {
11681187
trigger_characters: Some(vec![".".to_owned(), "'".to_owned(), "\"".to_owned()]),
11691188
resolve_provider: Some(true),
@@ -2024,6 +2043,20 @@ impl Server {
20242043
)),
20252044
));
20262045
}
2046+
} else if let Some(params) = as_request::<CodeLensRequest>(&x) {
2047+
if let Some(params) = self
2048+
.extract_request_params_or_send_err_response::<CodeLensRequest>(
2049+
params, &x.id,
2050+
)
2051+
{
2052+
self.set_file_stats(params.text_document.uri.clone(), telemetry_event);
2053+
self.code_lens(
2054+
x.id,
2055+
&transaction,
2056+
params,
2057+
telemetry_event.activity_key.clone(),
2058+
);
2059+
}
20272060
} else if let Some(params) = as_request::<WorkspaceSymbolRequest>(&x) {
20282061
if let Some(params) = self
20292062
.extract_request_params_or_send_err_response::<WorkspaceSymbolRequest>(
@@ -4259,6 +4292,61 @@ impl Server {
42594292
(!actions.is_empty()).then_some(actions)
42604293
}
42614294

4295+
fn code_lens_targets(
4296+
&self,
4297+
transaction: &Transaction<'_>,
4298+
handle: &Handle,
4299+
uri: &Url,
4300+
) -> Option<Vec<CodeLensTarget>> {
4301+
fn recurse_symbols<'a>(symbols: &'a [DocumentSymbol], out: &mut Vec<&'a DocumentSymbol>) {
4302+
for symbol in symbols {
4303+
if matches!(
4304+
symbol.kind,
4305+
SymbolKind::CLASS | SymbolKind::FUNCTION | SymbolKind::METHOD
4306+
) {
4307+
out.push(symbol);
4308+
}
4309+
if let Some(children) = symbol.children.as_deref() {
4310+
recurse_symbols(children, out);
4311+
}
4312+
}
4313+
}
4314+
4315+
let module_info = transaction.get_module_info(handle)?;
4316+
let symbols = transaction.symbols(handle)?;
4317+
let mut symbol_defs = Vec::new();
4318+
recurse_symbols(&symbols, &mut symbol_defs);
4319+
4320+
let mut seen = SmallSet::new();
4321+
let mut targets = Vec::new();
4322+
for symbol in symbol_defs {
4323+
let position = self.from_lsp_position(uri, &module_info, symbol.selection_range.start);
4324+
let Some(definition) = transaction
4325+
.find_definition(
4326+
handle,
4327+
position,
4328+
FindPreference {
4329+
import_behavior: ImportBehavior::StopAtRenamedImports,
4330+
..Default::default()
4331+
},
4332+
)
4333+
.into_iter()
4334+
.next()
4335+
else {
4336+
continue;
4337+
};
4338+
let key = (definition.module.path().dupe(), definition.definition_range);
4339+
if !seen.insert(key) {
4340+
continue;
4341+
}
4342+
targets.push(CodeLensTarget {
4343+
range: symbol.selection_range,
4344+
definition,
4345+
});
4346+
}
4347+
Some(targets)
4348+
}
4349+
42624350
fn document_highlight(
42634351
&self,
42644352
transaction: &Transaction<'_>,
@@ -4508,6 +4596,113 @@ impl Server {
45084596
);
45094597
}
45104598

4599+
fn code_lens<'a>(
4600+
&'a self,
4601+
request_id: RequestId,
4602+
transaction: &Transaction<'a>,
4603+
params: CodeLensParams,
4604+
activity_key: Option<ActivityKey>,
4605+
) {
4606+
let uri = &params.text_document.uri;
4607+
if self.open_notebook_cells.read().contains_key(uri) {
4608+
return self.send_response(new_response(request_id, Ok(Some(Vec::<CodeLens>::new()))));
4609+
}
4610+
let Some(handle) = self.make_handle_if_enabled(uri, Some(CodeLensRequest::METHOD)) else {
4611+
return self.send_response(new_response::<Option<Vec<CodeLens>>>(request_id, Ok(None)));
4612+
};
4613+
let Some(targets) = self.code_lens_targets(transaction, &handle, uri) else {
4614+
return self.send_response(new_response::<Option<Vec<CodeLens>>>(request_id, Ok(None)));
4615+
};
4616+
if targets.is_empty() {
4617+
return self.send_response(new_response(request_id, Ok(Some(Vec::<CodeLens>::new()))));
4618+
}
4619+
4620+
let path_remapper = self.path_remapper.clone();
4621+
let source_uri = uri.clone();
4622+
self.find_reference_queue.queue_task(
4623+
TelemetryEventKind::FindFromDefinition,
4624+
Box::new(move |server, _telemetry, telemetry_event, _, _| {
4625+
telemetry_event.set_activity_key(activity_key);
4626+
let mut transaction = server.state.cancellable_transaction();
4627+
server
4628+
.cancellation_handles
4629+
.lock()
4630+
.insert(request_id.clone(), transaction.get_cancellation_handle());
4631+
server.validate_in_memory_for_transaction(
4632+
transaction.as_mut(),
4633+
telemetry_event,
4634+
None,
4635+
);
4636+
4637+
let mut lenses = Vec::new();
4638+
for target in targets {
4639+
let local_results = match transaction.find_global_references_from_definition(
4640+
*handle.sys_info(),
4641+
target.definition.metadata,
4642+
TextRangeWithModule::new(
4643+
target.definition.module.clone(),
4644+
target.definition.definition_range,
4645+
),
4646+
false,
4647+
) {
4648+
Ok(results) => results,
4649+
Err(Cancelled) => {
4650+
let message = format!("Request {request_id} is canceled");
4651+
info!("{message}");
4652+
server.connection.send(Message::Response(Response::new_err(
4653+
request_id,
4654+
ErrorCode::RequestCanceled as i32,
4655+
message,
4656+
)));
4657+
return;
4658+
}
4659+
};
4660+
4661+
let mut locations = Vec::new();
4662+
for (info, ranges) in local_results {
4663+
if let Some(uri) = module_info_to_uri(&info, path_remapper.as_ref()) {
4664+
for range in ranges {
4665+
locations.push(Location {
4666+
uri: uri.clone(),
4667+
range: info.to_lsp_range(range),
4668+
});
4669+
}
4670+
}
4671+
}
4672+
4673+
let reference_count = locations.len();
4674+
let title = if reference_count == 1 {
4675+
"1 reference".to_owned()
4676+
} else {
4677+
format!("{reference_count} references")
4678+
};
4679+
lenses.push(CodeLens {
4680+
range: target.range,
4681+
command: Some(Command {
4682+
title,
4683+
command: "editor.action.showReferences".to_owned(),
4684+
arguments: Some(vec![
4685+
serde_json::to_value(&source_uri)
4686+
.expect("URI should serialize for code lens"),
4687+
serde_json::to_value(target.range.start)
4688+
.expect("Position should serialize for code lens"),
4689+
serde_json::to_value(&locations)
4690+
.expect("Locations should serialize for code lens"),
4691+
]),
4692+
}),
4693+
data: None,
4694+
});
4695+
}
4696+
4697+
server.cancellation_handles.lock().remove(&request_id);
4698+
server.connection.send(Message::Response(new_response(
4699+
request_id,
4700+
Ok(Some(lenses)),
4701+
)));
4702+
}),
4703+
);
4704+
}
4705+
45114706
fn rename<'a>(
45124707
&'a self,
45134708
request_id: RequestId,

pyrefly/lib/lsp/non_wasm/workspace.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ pub struct DisabledLanguageServices {
215215
#[serde(default)]
216216
pub references: bool,
217217
#[serde(default)]
218+
pub code_lens: bool,
219+
#[serde(default)]
218220
pub rename: bool,
219221
#[serde(default)]
220222
pub signature_help: bool,
@@ -242,6 +244,7 @@ impl DisabledLanguageServices {
242244
"textDocument/completion" => self.completion,
243245
"textDocument/documentHighlight" => self.document_highlight,
244246
"textDocument/references" => self.references,
247+
"textDocument/codeLens" => self.code_lens,
245248
"textDocument/rename" => self.rename,
246249
"textDocument/signatureHelp" => self.signature_help,
247250
"textDocument/hover" => self.hover,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use lsp_types::CodeLens;
9+
use lsp_types::CodeLensOptions;
10+
use lsp_types::Url;
11+
use pyrefly::commands::lsp::IndexingMode;
12+
13+
use crate::object_model::InitializeSettings;
14+
use crate::object_model::LspInteraction;
15+
use crate::util::get_test_files_root;
16+
17+
#[test]
18+
fn test_initialize_advertises_code_lens_with_indexing() {
19+
let interaction = LspInteraction::new_with_indexing_mode(IndexingMode::LazyBlocking);
20+
21+
interaction
22+
.client
23+
.send_initialize(
24+
interaction
25+
.client
26+
.get_initialize_params(&InitializeSettings::default()),
27+
)
28+
.expect_response_with(|response| {
29+
response.capabilities.code_lens_provider
30+
== Some(CodeLensOptions {
31+
resolve_provider: Some(false),
32+
})
33+
})
34+
.unwrap();
35+
interaction.client.send_initialized();
36+
interaction.shutdown().unwrap();
37+
}
38+
39+
#[test]
40+
fn test_code_lens_shows_reference_counts() {
41+
let root = get_test_files_root();
42+
let root_path = root.path().join("code_lens_references");
43+
let scope_uri = Url::from_file_path(&root_path).unwrap();
44+
let mut interaction = LspInteraction::new_with_indexing_mode(IndexingMode::LazyBlocking);
45+
interaction.set_root(root_path);
46+
interaction
47+
.initialize(InitializeSettings {
48+
workspace_folders: Some(vec![("test".to_owned(), scope_uri)]),
49+
..Default::default()
50+
})
51+
.unwrap();
52+
53+
interaction.client.did_open("symbols.py");
54+
interaction.client.did_open("usage.py");
55+
56+
interaction
57+
.client
58+
.code_lens("symbols.py")
59+
.expect_response_with(|response| {
60+
let Some(lenses) = response else {
61+
return false;
62+
};
63+
has_reference_lens(&lenses, 0, "3 references", 3)
64+
&& has_reference_lens(&lenses, 1, "2 references", 2)
65+
&& has_reference_lens(&lenses, 4, "2 references", 2)
66+
&& lenses.len() == 3
67+
})
68+
.unwrap();
69+
70+
interaction.shutdown().unwrap();
71+
}
72+
73+
fn has_reference_lens(
74+
lenses: &[CodeLens],
75+
line: u32,
76+
expected_title: &str,
77+
expected_locations: usize,
78+
) -> bool {
79+
lenses.iter().any(|lens| {
80+
let Some(command) = &lens.command else {
81+
return false;
82+
};
83+
if lens.range.start.line != line
84+
|| command.title != expected_title
85+
|| command.command != "editor.action.showReferences"
86+
{
87+
return false;
88+
}
89+
let Some(arguments) = &command.arguments else {
90+
return false;
91+
};
92+
arguments
93+
.get(2)
94+
.and_then(|value| value.as_array())
95+
.is_some_and(|locations| locations.len() == expected_locations)
96+
})
97+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod init;
1313

1414
mod basic;
1515
mod call_hierarchy;
16+
mod code_lens;
1617
mod completion;
1718
mod configuration;
1819
mod convert_module_package;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use lsp_types::notification::Initialized;
4545
use lsp_types::notification::Notification as _;
4646
use lsp_types::notification::PublishDiagnostics;
4747
use lsp_types::request::CodeActionRequest;
48+
use lsp_types::request::CodeLensRequest;
4849
use lsp_types::request::Completion;
4950
use lsp_types::request::DocumentDiagnosticRequest;
5051
use lsp_types::request::DocumentHighlightRequest;
@@ -518,6 +519,15 @@ impl TestClient {
518519
}))
519520
}
520521

522+
pub fn code_lens(&self, file: &'static str) -> ClientRequestHandle<'_, CodeLensRequest> {
523+
let path = self.get_root_or_panic().join(file);
524+
self.send_request(json!({
525+
"textDocument": {
526+
"uri": Url::from_file_path(&path).unwrap().to_string()
527+
}
528+
}))
529+
}
530+
521531
pub fn diagnostic(
522532
&self,
523533
file: &'static str,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class Greeter:
2+
def greet(self) -> str:
3+
return "hi"
4+
5+
def run(g: Greeter) -> str:
6+
return g.greet()

0 commit comments

Comments
 (0)