mirror of
https://github.com/nushell/nushell.git
synced 2025-05-19 17:30:45 +02:00
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> `tower-lsp` seems not well-maintained, I ended up with a dedicated thread for heavy computing and message passing to cancel it on any new request. During the progress, interrupting with edits or new requests. <img width="522" alt="image" src="https://github.com/user-attachments/assets/b263d73d-8ea3-4b26-a7b7-e0b30462d1af" /> Goto references are still blocking, with a hard timeout of 5 seconds. Only locations found within the time limit are returned. Technically, reference requests allow for responses with partial results, which means instant responsiveness. However, the `lsp_types` crate hasn’t enabled this. I believe I can still enable it with some JSON manipulation, but I’ll leave it for future work. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> Need some clever way to test the cancellation, no test cases added yet. # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
352 lines
11 KiB
Rust
352 lines
11 KiB
Rust
use crate::{Id, LanguageServer};
|
|
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse};
|
|
use nu_protocol::engine::StateWorkingSet;
|
|
use nu_protocol::Span;
|
|
|
|
impl LanguageServer {
|
|
pub fn find_definition_span_by_id(working_set: &StateWorkingSet, id: &Id) -> Option<Span> {
|
|
match id {
|
|
Id::Declaration(decl_id) => {
|
|
let block_id = working_set.get_decl(*decl_id).block_id()?;
|
|
working_set.get_block(block_id).span
|
|
}
|
|
Id::Variable(var_id) => {
|
|
let var = working_set.get_variable(*var_id);
|
|
Some(var.declaration_span)
|
|
}
|
|
Id::Module(module_id) => {
|
|
let module = working_set.get_module(*module_id);
|
|
module.span
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn goto_definition(
|
|
&mut self,
|
|
params: &GotoDefinitionParams,
|
|
) -> Option<GotoDefinitionResponse> {
|
|
let mut engine_state = self.new_engine_state();
|
|
|
|
let path_uri = params
|
|
.text_document_position_params
|
|
.text_document
|
|
.uri
|
|
.to_owned();
|
|
let (working_set, id, _, _) = self
|
|
.parse_and_find(
|
|
&mut engine_state,
|
|
&path_uri,
|
|
params.text_document_position_params.position,
|
|
)
|
|
.ok()?;
|
|
|
|
Some(GotoDefinitionResponse::Scalar(self.get_location_by_span(
|
|
working_set.files(),
|
|
&Self::find_definition_span_by_id(&working_set, &id)?,
|
|
)?))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::path_to_uri;
|
|
use crate::tests::{initialize_language_server, open_unchecked};
|
|
use assert_json_diff::assert_json_eq;
|
|
use lsp_server::{Connection, Message};
|
|
use lsp_types::request::{GotoDefinition, Request};
|
|
use lsp_types::{
|
|
GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier,
|
|
TextDocumentPositionParams, Uri, WorkDoneProgressParams,
|
|
};
|
|
use nu_test_support::fs::{fixtures, root};
|
|
|
|
fn send_goto_definition_request(
|
|
client_connection: &Connection,
|
|
uri: Uri,
|
|
line: u32,
|
|
character: u32,
|
|
) -> Message {
|
|
client_connection
|
|
.sender
|
|
.send(Message::Request(lsp_server::Request {
|
|
id: 2.into(),
|
|
method: GotoDefinition::METHOD.to_string(),
|
|
params: serde_json::to_value(GotoDefinitionParams {
|
|
text_document_position_params: TextDocumentPositionParams {
|
|
text_document: TextDocumentIdentifier { uri },
|
|
position: Position { line, character },
|
|
},
|
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
|
partial_result_params: PartialResultParams::default(),
|
|
})
|
|
.unwrap(),
|
|
}))
|
|
.unwrap();
|
|
|
|
client_connection
|
|
.receiver
|
|
.recv_timeout(std::time::Duration::from_secs(2))
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_for_none_existing_file() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut none_existent_path = root();
|
|
none_existent_path.push("none-existent.nu");
|
|
|
|
client_connection
|
|
.sender
|
|
.send(Message::Request(lsp_server::Request {
|
|
id: 2.into(),
|
|
method: GotoDefinition::METHOD.to_string(),
|
|
params: serde_json::to_value(GotoDefinitionParams {
|
|
text_document_position_params: TextDocumentPositionParams {
|
|
text_document: TextDocumentIdentifier {
|
|
uri: path_to_uri(&none_existent_path),
|
|
},
|
|
position: Position {
|
|
line: 0,
|
|
character: 0,
|
|
},
|
|
},
|
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
|
partial_result_params: PartialResultParams::default(),
|
|
})
|
|
.unwrap(),
|
|
}))
|
|
.unwrap();
|
|
|
|
let resp = client_connection
|
|
.receiver
|
|
.recv_timeout(std::time::Duration::from_secs(2))
|
|
.unwrap();
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(result, serde_json::json!(null));
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_variable() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("var.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 2, 12);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 4 },
|
|
"end": { "line": 0, "character": 12 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_command() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("command.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 4, 1);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 17 },
|
|
"end": { "line": 2, "character": 1 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_command_unicode() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("command_unicode.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 4, 2);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 19 },
|
|
"end": { "line": 2, "character": 1 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_command_parameter() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("command.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 1, 14);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 11 },
|
|
"end": { "line": 0, "character": 15 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_variable_in_else_block() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("else.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 1, 21);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 4 },
|
|
"end": { "line": 0, "character": 7 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_variable_in_match_guard() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("match.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 2, 9);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 4 },
|
|
"end": { "line": 0, "character": 7 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn goto_definition_of_variable_in_each() {
|
|
let (client_connection, _recv) = initialize_language_server(None);
|
|
|
|
let mut script = fixtures();
|
|
script.push("lsp");
|
|
script.push("goto");
|
|
script.push("collect.nu");
|
|
let script = path_to_uri(&script);
|
|
|
|
open_unchecked(&client_connection, script.clone());
|
|
|
|
let resp = send_goto_definition_request(&client_connection, script.clone(), 1, 16);
|
|
let result = if let Message::Response(response) = resp {
|
|
response.result
|
|
} else {
|
|
panic!()
|
|
};
|
|
|
|
assert_json_eq!(
|
|
result,
|
|
serde_json::json!({
|
|
"uri": script,
|
|
"range": {
|
|
"start": { "line": 0, "character": 4 },
|
|
"end": { "line": 0, "character": 7 }
|
|
}
|
|
})
|
|
);
|
|
}
|
|
}
|