mirror of
https://github.com/nushell/nushell.git
synced 2025-01-19 04:40:41 +01:00
feat(lsp): workspace wide operations: rename/goto references
(#14837)
# 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. --> <!-- 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! --> goto reference: <img width="885" alt="image" src="https://github.com/user-attachments/assets/6f85cd10-0c2d-46b2-b99e-47a9bbf90822" /> rename: <img width="483" alt="image" src="https://github.com/user-attachments/assets/828e7586-c2b7-414d-9085-5188b10f5f5f" /> Caveats: 1. Module reference/rename is not supported yet 2. names in `use` command should also be renamed, which is not handled now 3. workspace wide actions can be time-consuming, as it requires parsing of all `**/*.nu` files in the workspace (if its text contains the name). Added a progress bar for such requests. 4. In case these requests are triggered accidentally in a root folder with a large depth, I hard-coded the max depth to search to 5 right now. # 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 > ``` --> Limited test cases # 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. -->
This commit is contained in:
parent
06938659d2
commit
d66f8cca40
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -3960,10 +3960,10 @@ dependencies = [
|
|||||||
"nu-cli",
|
"nu-cli",
|
||||||
"nu-cmd-lang",
|
"nu-cmd-lang",
|
||||||
"nu-command",
|
"nu-command",
|
||||||
|
"nu-glob",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"reedline",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
|
@ -9,20 +9,19 @@ license = "MIT"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cli = { path = "../nu-cli", version = "0.101.1" }
|
nu-cli = { path = "../nu-cli", version = "0.101.1" }
|
||||||
|
nu-glob = { path = "../nu-glob", version = "0.101.1" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.101.1" }
|
nu-parser = { path = "../nu-parser", version = "0.101.1" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.101.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.1" }
|
||||||
|
|
||||||
reedline = { workspace = true }
|
|
||||||
|
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
|
fuzzy-matcher = { workspace = true }
|
||||||
lsp-server = { workspace = true }
|
lsp-server = { workspace = true }
|
||||||
lsp-types = { workspace = true }
|
|
||||||
lsp-textdocument = { workspace = true }
|
lsp-textdocument = { workspace = true }
|
||||||
|
lsp-types = { workspace = true }
|
||||||
miette = { workspace = true }
|
miette = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
fuzzy-matcher = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.1" }
|
||||||
|
@ -2,10 +2,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{
|
ast::{
|
||||||
Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern,
|
Argument, Block, Call, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern,
|
||||||
PipelineRedirection, RecordItem,
|
PipelineRedirection, RecordItem,
|
||||||
},
|
},
|
||||||
engine::StateWorkingSet,
|
engine::StateWorkingSet,
|
||||||
|
DeclId, Span,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Id;
|
use crate::Id;
|
||||||
@ -162,7 +163,75 @@ fn redirect_flat_map<T, E>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_id_in_expr(expr: &Expression, _: &StateWorkingSet, location: &usize) -> Option<Vec<Id>> {
|
/// For situations like
|
||||||
|
/// ```nushell
|
||||||
|
/// def foo [] {}
|
||||||
|
/// # |__________ location
|
||||||
|
/// ```
|
||||||
|
/// `def` is an internal call with name/signature/closure as its arguments
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `location`: None if no `contains` check required, typically when we want to compare the `decl_id` directly
|
||||||
|
fn try_find_id_in_def(
|
||||||
|
call: &Call,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
location: Option<&usize>,
|
||||||
|
) -> Option<(Id, Span)> {
|
||||||
|
let call_name = String::from_utf8(working_set.get_span_contents(call.head).to_vec()).ok()?;
|
||||||
|
if !(call_name == "def" || call_name == "export def") {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mut span = None;
|
||||||
|
for arg in call.arguments.iter() {
|
||||||
|
if location
|
||||||
|
.map(|pos| arg.span().contains(*pos))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
// String means this argument is the name
|
||||||
|
if let Argument::Positional(expr) = arg {
|
||||||
|
if let Expr::String(_) = &expr.expr {
|
||||||
|
span = Some(expr.span);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if we do care the location,
|
||||||
|
// reaching here means this argument is not the name
|
||||||
|
if location.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut span = span?;
|
||||||
|
// adjust span if quoted
|
||||||
|
let text = String::from_utf8(working_set.get_span_contents(span).to_vec()).ok()?;
|
||||||
|
if text.len() > 1
|
||||||
|
&& ((text.starts_with('"') && text.ends_with('"'))
|
||||||
|
|| (text.starts_with('\'') && text.ends_with('\'')))
|
||||||
|
{
|
||||||
|
span = Span::new(span.start.saturating_add(1), span.end.saturating_sub(1));
|
||||||
|
}
|
||||||
|
let call_span = call.span();
|
||||||
|
// find decl_ids whose span is covered by the `def` call
|
||||||
|
let mut matched_ids: Vec<(Id, Span)> = (0..working_set.num_decls())
|
||||||
|
.filter_map(|id| {
|
||||||
|
let decl_id = DeclId::new(id);
|
||||||
|
let block_id = working_set.get_decl(decl_id).block_id()?;
|
||||||
|
let decl_span = working_set.get_block(block_id).span?;
|
||||||
|
// find those within the `def` call
|
||||||
|
call_span
|
||||||
|
.contains_span(decl_span)
|
||||||
|
.then_some((Id::Declaration(decl_id), decl_span))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
matched_ids.sort_by_key(|(_, s)| s.start);
|
||||||
|
matched_ids.first().cloned().map(|(id, _)| (id, span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_id_in_expr(
|
||||||
|
expr: &Expression,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
location: &usize,
|
||||||
|
) -> Option<Vec<(Id, Span)>> {
|
||||||
// skip the entire expression if the location is not in it
|
// skip the entire expression if the location is not in it
|
||||||
if !expr.span.contains(*location) {
|
if !expr.span.contains(*location) {
|
||||||
// TODO: the span of Keyword does not include its subsidiary expression
|
// TODO: the span of Keyword does not include its subsidiary expression
|
||||||
@ -174,16 +243,23 @@ fn find_id_in_expr(expr: &Expression, _: &StateWorkingSet, location: &usize) ->
|
|||||||
}
|
}
|
||||||
return Some(Vec::new());
|
return Some(Vec::new());
|
||||||
}
|
}
|
||||||
|
let span = expr.span;
|
||||||
match &expr.expr {
|
match &expr.expr {
|
||||||
Expr::Var(var_id) | Expr::VarDecl(var_id) => Some(vec![Id::Variable(*var_id)]),
|
Expr::VarDecl(var_id) => Some(vec![(Id::Variable(*var_id), span)]),
|
||||||
|
// trim leading `$` sign
|
||||||
|
Expr::Var(var_id) => Some(vec![(
|
||||||
|
Id::Variable(*var_id),
|
||||||
|
Span::new(span.start.saturating_add(1), span.end),
|
||||||
|
)]),
|
||||||
Expr::Call(call) => {
|
Expr::Call(call) => {
|
||||||
if call.head.contains(*location) {
|
if call.head.contains(*location) {
|
||||||
Some(vec![Id::Declaration(call.decl_id)])
|
Some(vec![(Id::Declaration(call.decl_id), call.head)])
|
||||||
} else {
|
} else {
|
||||||
None
|
try_find_id_in_def(call, working_set, Some(location)).map(|p| vec![p])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Overlay(Some(module_id)) => Some(vec![Id::Module(*module_id)]),
|
// TODO: module id of `use`
|
||||||
|
Expr::Overlay(Some(module_id)) => Some(vec![(Id::Module(*module_id), span)]),
|
||||||
// terminal value expressions
|
// terminal value expressions
|
||||||
Expr::Bool(_)
|
Expr::Bool(_)
|
||||||
| Expr::Binary(_)
|
| Expr::Binary(_)
|
||||||
@ -197,14 +273,57 @@ fn find_id_in_expr(expr: &Expression, _: &StateWorkingSet, location: &usize) ->
|
|||||||
| Expr::Nothing
|
| Expr::Nothing
|
||||||
| Expr::RawString(_)
|
| Expr::RawString(_)
|
||||||
| Expr::Signature(_)
|
| Expr::Signature(_)
|
||||||
| Expr::String(_) => Some(vec![Id::Value(expr.ty.clone())]),
|
| Expr::String(_) => Some(vec![(Id::Value(expr.ty.clone()), span)]),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// find the leaf node at the given location from ast
|
/// find the leaf node at the given location from ast
|
||||||
pub fn find_id(ast: &Arc<Block>, working_set: &StateWorkingSet, location: &usize) -> Option<Id> {
|
pub fn find_id(
|
||||||
|
ast: &Arc<Block>,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
location: &usize,
|
||||||
|
) -> Option<(Id, Span)> {
|
||||||
ast_flat_map(ast, working_set, location, find_id_in_expr)
|
ast_flat_map(ast, working_set, location, find_id_in_expr)
|
||||||
.first()
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: module id support
|
||||||
|
fn find_reference_by_id_in_expr(
|
||||||
|
expr: &Expression,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
id: &Id,
|
||||||
|
) -> Option<Vec<Span>> {
|
||||||
|
let recur = |expr| expr_flat_map(expr, working_set, id, find_reference_by_id_in_expr);
|
||||||
|
match (&expr.expr, id) {
|
||||||
|
(Expr::Var(vid1), Id::Variable(vid2)) if *vid1 == *vid2 => Some(vec![Span::new(
|
||||||
|
// we want to exclude the `$` sign for renaming
|
||||||
|
expr.span.start.saturating_add(1),
|
||||||
|
expr.span.end,
|
||||||
|
)]),
|
||||||
|
(Expr::VarDecl(vid1), Id::Variable(vid2)) if *vid1 == *vid2 => Some(vec![expr.span]),
|
||||||
|
(Expr::Call(call), Id::Declaration(decl_id)) => {
|
||||||
|
let mut occurs: Vec<Span> = call
|
||||||
|
.arguments
|
||||||
|
.iter()
|
||||||
|
.filter_map(|arg| arg.expr())
|
||||||
|
.flat_map(recur)
|
||||||
|
.collect();
|
||||||
|
if *decl_id == call.decl_id {
|
||||||
|
occurs.push(call.head);
|
||||||
|
}
|
||||||
|
if let Some((id_found, span_found)) = try_find_id_in_def(call, working_set, None) {
|
||||||
|
if id_found == *id {
|
||||||
|
occurs.push(span_found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(occurs)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_reference_by_id(ast: &Arc<Block>, working_set: &StateWorkingSet, id: &Id) -> Vec<Span> {
|
||||||
|
ast_flat_map(ast, working_set, id, find_reference_by_id_in_expr)
|
||||||
|
}
|
||||||
|
@ -51,7 +51,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn publish_diagnostics_variable_does_not_exists() {
|
fn publish_diagnostics_variable_does_not_exists() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -82,7 +82,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn publish_diagnostics_fixed_unknown_variable() {
|
fn publish_diagnostics_fixed_unknown_variable() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -121,7 +121,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn publish_diagnostics_none() {
|
fn publish_diagnostics_none() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
|
@ -1,8 +1,27 @@
|
|||||||
use crate::ast::find_id;
|
|
||||||
use crate::{Id, LanguageServer};
|
use crate::{Id, LanguageServer};
|
||||||
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse};
|
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse};
|
||||||
|
use nu_protocol::engine::StateWorkingSet;
|
||||||
|
use nu_protocol::Span;
|
||||||
|
|
||||||
impl LanguageServer {
|
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(
|
pub fn goto_definition(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: &GotoDefinitionParams,
|
params: &GotoDefinitionParams,
|
||||||
@ -14,30 +33,18 @@ impl LanguageServer {
|
|||||||
.text_document
|
.text_document
|
||||||
.uri
|
.uri
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let (block, file_offset, working_set, file) =
|
let (working_set, id, _, _, _) = self
|
||||||
self.parse_file(&mut engine_state, &path_uri, false)?;
|
.parse_and_find(
|
||||||
let location =
|
&mut engine_state,
|
||||||
file.offset_at(params.text_document_position_params.position) as usize + file_offset;
|
&path_uri,
|
||||||
let id = find_id(&block, &working_set, &location)?;
|
params.text_document_position_params.position,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
let span = match id {
|
Some(GotoDefinitionResponse::Scalar(self.get_location_by_span(
|
||||||
Id::Declaration(decl_id) => {
|
working_set.files(),
|
||||||
let block_id = working_set.get_decl(decl_id).block_id()?;
|
&Self::find_definition_span_by_id(&working_set, &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,
|
|
||||||
}?;
|
|
||||||
Some(GotoDefinitionResponse::Scalar(
|
|
||||||
self.get_location_by_span(working_set.files(), &span)?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +92,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_for_none_existing_file() {
|
fn goto_definition_for_none_existing_file() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut none_existent_path = root();
|
let mut none_existent_path = root();
|
||||||
none_existent_path.push("none-existent.nu");
|
none_existent_path.push("none-existent.nu");
|
||||||
@ -127,7 +134,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_variable() {
|
fn goto_definition_of_variable() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -158,7 +165,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_command() {
|
fn goto_definition_of_command() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -189,7 +196,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_command_unicode() {
|
fn goto_definition_of_command_unicode() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -220,7 +227,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_command_parameter() {
|
fn goto_definition_of_command_parameter() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -251,7 +258,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_variable_in_else_block() {
|
fn goto_definition_of_variable_in_else_block() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -282,7 +289,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_variable_in_match_guard() {
|
fn goto_definition_of_variable_in_match_guard() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -313,7 +320,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_variable_in_each() {
|
fn goto_definition_of_variable_in_each() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
|
@ -217,7 +217,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inlay_hint_variable_type() {
|
fn inlay_hint_variable_type() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -250,7 +250,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inlay_hint_assignment_type() {
|
fn inlay_hint_assignment_type() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -284,7 +284,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inlay_hint_parameter_names() {
|
fn inlay_hint_parameter_names() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
|
@ -3,12 +3,14 @@ use ast::find_id;
|
|||||||
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
|
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
|
||||||
use lsp_textdocument::{FullTextDocument, TextDocuments};
|
use lsp_textdocument::{FullTextDocument, TextDocuments};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
request, request::Request, CompletionItem, CompletionItemKind, CompletionParams,
|
request::{self, Request},
|
||||||
CompletionResponse, CompletionTextEdit, Hover, HoverContents, HoverParams, InlayHint, Location,
|
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit,
|
||||||
MarkupContent, MarkupKind, OneOf, Range, RenameOptions, ServerCapabilities,
|
Hover, HoverContents, HoverParams, InlayHint, Location, MarkupContent, MarkupKind, OneOf,
|
||||||
TextDocumentSyncKind, TextEdit, Uri, WorkDoneProgressOptions,
|
Position, Range, ReferencesOptions, RenameOptions, ServerCapabilities, TextDocumentSyncKind,
|
||||||
|
TextEdit, Uri, WorkDoneProgressOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
|
||||||
|
WorkspaceServerCapabilities,
|
||||||
};
|
};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{miette, IntoDiagnostic, Result};
|
||||||
use nu_cli::{NuCompleter, SuggestionKind};
|
use nu_cli::{NuCompleter, SuggestionKind};
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
@ -32,9 +34,10 @@ mod goto;
|
|||||||
mod hints;
|
mod hints;
|
||||||
mod notification;
|
mod notification;
|
||||||
mod symbols;
|
mod symbols;
|
||||||
|
mod workspace;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
enum Id {
|
pub enum Id {
|
||||||
Variable(VarId),
|
Variable(VarId),
|
||||||
Declaration(DeclId),
|
Declaration(DeclId),
|
||||||
Value(Type),
|
Value(Type),
|
||||||
@ -48,6 +51,9 @@ pub struct LanguageServer {
|
|||||||
engine_state: EngineState,
|
engine_state: EngineState,
|
||||||
symbol_cache: SymbolCache,
|
symbol_cache: SymbolCache,
|
||||||
inlay_hints: BTreeMap<Uri, Vec<InlayHint>>,
|
inlay_hints: BTreeMap<Uri, Vec<InlayHint>>,
|
||||||
|
workspace_folders: BTreeMap<String, WorkspaceFolder>,
|
||||||
|
// for workspace wide requests
|
||||||
|
occurrences: BTreeMap<Uri, Vec<Range>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_to_uri(path: impl AsRef<Path>) -> Uri {
|
pub fn path_to_uri(path: impl AsRef<Path>) -> Uri {
|
||||||
@ -90,10 +96,15 @@ impl LanguageServer {
|
|||||||
engine_state,
|
engine_state,
|
||||||
symbol_cache: SymbolCache::new(),
|
symbol_cache: SymbolCache::new(),
|
||||||
inlay_hints: BTreeMap::new(),
|
inlay_hints: BTreeMap::new(),
|
||||||
|
workspace_folders: BTreeMap::new(),
|
||||||
|
occurrences: BTreeMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve_requests(mut self) -> Result<()> {
|
pub fn serve_requests(mut self) -> Result<()> {
|
||||||
|
let work_done_progress_options = WorkDoneProgressOptions {
|
||||||
|
work_done_progress: Some(true),
|
||||||
|
};
|
||||||
let server_capabilities = serde_json::to_value(ServerCapabilities {
|
let server_capabilities = serde_json::to_value(ServerCapabilities {
|
||||||
text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind(
|
text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind(
|
||||||
TextDocumentSyncKind::INCREMENTAL,
|
TextDocumentSyncKind::INCREMENTAL,
|
||||||
@ -106,20 +117,28 @@ impl LanguageServer {
|
|||||||
inlay_hint_provider: Some(OneOf::Left(true)),
|
inlay_hint_provider: Some(OneOf::Left(true)),
|
||||||
rename_provider: Some(OneOf::Right(RenameOptions {
|
rename_provider: Some(OneOf::Right(RenameOptions {
|
||||||
prepare_provider: Some(true),
|
prepare_provider: Some(true),
|
||||||
work_done_progress_options: WorkDoneProgressOptions {
|
work_done_progress_options,
|
||||||
work_done_progress: Some(true),
|
|
||||||
},
|
|
||||||
})),
|
})),
|
||||||
references_provider: Some(OneOf::Left(true)),
|
references_provider: Some(OneOf::Right(ReferencesOptions {
|
||||||
|
work_done_progress_options,
|
||||||
|
})),
|
||||||
|
workspace: Some(WorkspaceServerCapabilities {
|
||||||
|
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||||
|
supported: Some(true),
|
||||||
|
change_notifications: Some(OneOf::Left(true)),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.expect("Must be serializable");
|
.expect("Must be serializable");
|
||||||
let _ = self
|
let init_params = self
|
||||||
.connection
|
.connection
|
||||||
.initialize_while(server_capabilities, || {
|
.initialize_while(server_capabilities, || {
|
||||||
!self.engine_state.signals().interrupted()
|
!self.engine_state.signals().interrupted()
|
||||||
})
|
})
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
|
self.initialize_workspace_folders(init_params)?;
|
||||||
|
|
||||||
while !self.engine_state.signals().interrupted() {
|
while !self.engine_state.signals().interrupted() {
|
||||||
let msg = match self
|
let msg = match self
|
||||||
@ -157,11 +176,24 @@ impl LanguageServer {
|
|||||||
request::DocumentSymbolRequest::METHOD => {
|
request::DocumentSymbolRequest::METHOD => {
|
||||||
Self::handle_lsp_request(request, |params| self.document_symbol(params))
|
Self::handle_lsp_request(request, |params| self.document_symbol(params))
|
||||||
}
|
}
|
||||||
|
request::References::METHOD => {
|
||||||
|
Self::handle_lsp_request(request, |params| self.references(params))
|
||||||
|
}
|
||||||
request::WorkspaceSymbolRequest::METHOD => {
|
request::WorkspaceSymbolRequest::METHOD => {
|
||||||
Self::handle_lsp_request(request, |params| {
|
Self::handle_lsp_request(request, |params| {
|
||||||
self.workspace_symbol(params)
|
self.workspace_symbol(params)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
request::Rename::METHOD => {
|
||||||
|
Self::handle_lsp_request(request, |params| self.rename(params))
|
||||||
|
}
|
||||||
|
request::PrepareRenameRequest::METHOD => {
|
||||||
|
let id = request.id.clone();
|
||||||
|
if let Err(e) = self.prepare_rename(request) {
|
||||||
|
self.send_error_message(id, 2, e.to_string())?
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
request::InlayHintRequest::METHOD => {
|
request::InlayHintRequest::METHOD => {
|
||||||
Self::handle_lsp_request(request, |params| self.get_inlay_hints(params))
|
Self::handle_lsp_request(request, |params| self.get_inlay_hints(params))
|
||||||
}
|
}
|
||||||
@ -199,6 +231,24 @@ impl LanguageServer {
|
|||||||
engine_state
|
engine_state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_and_find<'a>(
|
||||||
|
&mut self,
|
||||||
|
engine_state: &'a mut EngineState,
|
||||||
|
uri: &Uri,
|
||||||
|
pos: Position,
|
||||||
|
) -> Result<(StateWorkingSet<'a>, Id, Span, usize, &FullTextDocument)> {
|
||||||
|
let (block, file_offset, mut working_set, file) = self
|
||||||
|
.parse_file(engine_state, uri, false)
|
||||||
|
.ok_or_else(|| miette!("\nFailed to parse current file"))?;
|
||||||
|
|
||||||
|
let location = file.offset_at(pos) as usize + file_offset;
|
||||||
|
let (id, span) = find_id(&block, &working_set, &location)
|
||||||
|
.ok_or_else(|| miette!("\nFailed to find current name"))?;
|
||||||
|
// add block to working_set for later references
|
||||||
|
working_set.add_block(block);
|
||||||
|
Ok((working_set, id, span, file_offset, file))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_file<'a>(
|
pub fn parse_file<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
engine_state: &'a mut EngineState,
|
engine_state: &'a mut EngineState,
|
||||||
@ -231,7 +281,7 @@ impl LanguageServer {
|
|||||||
for cached_file in files.into_iter() {
|
for cached_file in files.into_iter() {
|
||||||
if cached_file.covered_span.contains(span.start) {
|
if cached_file.covered_span.contains(span.start) {
|
||||||
let path = Path::new(&*cached_file.name);
|
let path = Path::new(&*cached_file.name);
|
||||||
if !(path.exists() && path.is_file()) {
|
if !path.is_file() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let target_uri = path_to_uri(path);
|
let target_uri = path_to_uri(path);
|
||||||
@ -294,11 +344,13 @@ impl LanguageServer {
|
|||||||
.text_document
|
.text_document
|
||||||
.uri
|
.uri
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let (block, file_offset, working_set, file) =
|
let (working_set, id, _, _, _) = self
|
||||||
self.parse_file(&mut engine_state, &path_uri, false)?;
|
.parse_and_find(
|
||||||
let location =
|
&mut engine_state,
|
||||||
file.offset_at(params.text_document_position_params.position) as usize + file_offset;
|
&path_uri,
|
||||||
let id = find_id(&block, &working_set, &location)?;
|
params.text_document_position_params.position,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
match id {
|
match id {
|
||||||
Id::Variable(var_id) => {
|
Id::Variable(var_id) => {
|
||||||
@ -543,7 +595,9 @@ mod tests {
|
|||||||
use nu_test_support::fs::fixtures;
|
use nu_test_support::fs::fixtures;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
|
|
||||||
pub fn initialize_language_server() -> (Connection, Receiver<Result<()>>) {
|
pub fn initialize_language_server(
|
||||||
|
params: Option<InitializeParams>,
|
||||||
|
) -> (Connection, Receiver<Result<()>>) {
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
let (client_connection, server_connection) = Connection::memory();
|
let (client_connection, server_connection) = Connection::memory();
|
||||||
let engine_state = nu_cmd_lang::create_default_context();
|
let engine_state = nu_cmd_lang::create_default_context();
|
||||||
@ -559,13 +613,7 @@ mod tests {
|
|||||||
.send(Message::Request(lsp_server::Request {
|
.send(Message::Request(lsp_server::Request {
|
||||||
id: 1.into(),
|
id: 1.into(),
|
||||||
method: Initialize::METHOD.to_string(),
|
method: Initialize::METHOD.to_string(),
|
||||||
params: serde_json::to_value(InitializeParams {
|
params: serde_json::to_value(params.unwrap_or_default()).unwrap(),
|
||||||
capabilities: lsp_types::ClientCapabilities {
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
client_connection
|
client_connection
|
||||||
@ -586,7 +634,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shutdown_on_request() {
|
fn shutdown_on_request() {
|
||||||
let (client_connection, recv) = initialize_language_server();
|
let (client_connection, recv) = initialize_language_server(None);
|
||||||
|
|
||||||
client_connection
|
client_connection
|
||||||
.sender
|
.sender
|
||||||
@ -717,7 +765,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_variable() {
|
fn hover_on_variable() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -744,7 +792,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_custom_command() {
|
fn hover_on_custom_command() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -774,7 +822,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_str_join() {
|
fn hover_on_str_join() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -834,7 +882,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_on_variable() {
|
fn complete_on_variable() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -871,7 +919,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_command_with_space() {
|
fn complete_command_with_space() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -909,7 +957,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_command_with_line() {
|
fn complete_command_with_line() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -947,7 +995,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_keyword() {
|
fn complete_keyword() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
use lsp_server::{Message, RequestId, Response, ResponseError};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
notification::{
|
notification::{
|
||||||
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification,
|
DidChangeTextDocument, DidChangeWorkspaceFolders, DidCloseTextDocument,
|
||||||
|
DidOpenTextDocument, Notification, Progress,
|
||||||
},
|
},
|
||||||
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Uri,
|
DidChangeTextDocumentParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||||
|
DidOpenTextDocumentParams, ProgressParams, ProgressParamsValue, ProgressToken, Uri,
|
||||||
|
WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::LanguageServer;
|
use crate::LanguageServer;
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
|
||||||
impl LanguageServer {
|
impl LanguageServer {
|
||||||
pub(crate) fn handle_lsp_notification(
|
pub(crate) fn handle_lsp_notification(
|
||||||
@ -36,9 +41,88 @@ impl LanguageServer {
|
|||||||
self.inlay_hints.remove(&uri);
|
self.inlay_hints.remove(&uri);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
DidChangeWorkspaceFolders::METHOD => {
|
||||||
|
let params: DidChangeWorkspaceFoldersParams =
|
||||||
|
serde_json::from_value(notification.params.clone())
|
||||||
|
.expect("Expect receive DidChangeWorkspaceFoldersParams");
|
||||||
|
for added in params.event.added {
|
||||||
|
self.workspace_folders.insert(added.name.clone(), added);
|
||||||
|
}
|
||||||
|
for removed in params.event.removed {
|
||||||
|
self.workspace_folders.remove(&removed.name);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_progress_notification(
|
||||||
|
&self,
|
||||||
|
token: ProgressToken,
|
||||||
|
value: WorkDoneProgress,
|
||||||
|
) -> Result<()> {
|
||||||
|
let progress_params = ProgressParams {
|
||||||
|
token,
|
||||||
|
value: ProgressParamsValue::WorkDone(value),
|
||||||
|
};
|
||||||
|
let notification =
|
||||||
|
lsp_server::Notification::new(Progress::METHOD.to_string(), progress_params);
|
||||||
|
self.connection
|
||||||
|
.sender
|
||||||
|
.send(lsp_server::Message::Notification(notification))
|
||||||
|
.into_diagnostic()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_progress_begin(&self, token: ProgressToken, title: &str) -> Result<()> {
|
||||||
|
self.send_progress_notification(
|
||||||
|
token,
|
||||||
|
WorkDoneProgress::Begin(WorkDoneProgressBegin {
|
||||||
|
title: title.to_string(),
|
||||||
|
percentage: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_progress_report(
|
||||||
|
&self,
|
||||||
|
token: ProgressToken,
|
||||||
|
percentage: u32,
|
||||||
|
message: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.send_progress_notification(
|
||||||
|
token,
|
||||||
|
WorkDoneProgress::Report(WorkDoneProgressReport {
|
||||||
|
message,
|
||||||
|
percentage: Some(percentage),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_progress_end(&self, token: ProgressToken, message: Option<String>) -> Result<()> {
|
||||||
|
self.send_progress_notification(
|
||||||
|
token,
|
||||||
|
WorkDoneProgress::End(WorkDoneProgressEnd { message }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_error_message(&self, id: RequestId, code: i32, message: String) -> Result<()> {
|
||||||
|
self.connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Response(Response {
|
||||||
|
id,
|
||||||
|
result: None,
|
||||||
|
error: Some(ResponseError {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
data: None,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.into_diagnostic()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -55,7 +139,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_correct_documentation_on_let() {
|
fn hover_correct_documentation_on_let() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -85,7 +169,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_command_after_full_content_change() {
|
fn hover_on_command_after_full_content_change() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -126,7 +210,7 @@ hello"#,
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_command_after_partial_content_change() {
|
fn hover_on_command_after_partial_content_change() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -171,7 +255,7 @@ hello"#,
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_document_with_utf_char() {
|
fn open_document_with_utf_char() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
|
@ -198,7 +198,7 @@ impl SymbolCache {
|
|||||||
);
|
);
|
||||||
for cached_file in working_set.files() {
|
for cached_file in working_set.files() {
|
||||||
let path = Path::new(&*cached_file.name);
|
let path = Path::new(&*cached_file.name);
|
||||||
if !(path.exists() && path.is_file()) {
|
if !path.is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let target_uri = path_to_uri(path);
|
let target_uri = path_to_uri(path);
|
||||||
@ -342,7 +342,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
// for variable `$in/$it`, should not appear in symbols
|
// for variable `$in/$it`, should not appear in symbols
|
||||||
fn document_symbol_special_variables() {
|
fn document_symbol_special_variables() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -364,7 +364,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn document_symbol_basic() {
|
fn document_symbol_basic() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -412,7 +412,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn document_symbol_update() {
|
fn document_symbol_update() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -464,7 +464,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_symbol_current() {
|
fn workspace_symbol_current() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
@ -530,7 +530,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_symbol_other() {
|
fn workspace_symbol_other() {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let (client_connection, _recv) = initialize_language_server(None);
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
|
569
crates/nu-lsp/src/workspace.rs
Normal file
569
crates/nu-lsp/src/workspace.rs
Normal file
@ -0,0 +1,569 @@
|
|||||||
|
use lsp_textdocument::FullTextDocument;
|
||||||
|
use nu_parser::parse;
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ast::find_reference_by_id, path_to_uri, span_to_range, uri_to_path, Id, LanguageServer,
|
||||||
|
};
|
||||||
|
use lsp_server::{Message, Request, Response};
|
||||||
|
use lsp_types::{
|
||||||
|
Location, PrepareRenameResponse, ProgressToken, Range, ReferenceParams, RenameParams,
|
||||||
|
TextDocumentPositionParams, TextEdit, Uri, WorkspaceEdit, WorkspaceFolder,
|
||||||
|
};
|
||||||
|
use miette::{miette, IntoDiagnostic, Result};
|
||||||
|
use nu_glob::{glob, Paths};
|
||||||
|
use nu_protocol::{engine::StateWorkingSet, Span};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
impl LanguageServer {
|
||||||
|
/// get initial workspace folders from initialization response
|
||||||
|
pub fn initialize_workspace_folders(&mut self, init_params: Value) -> Result<()> {
|
||||||
|
if let Some(array) = init_params.get("workspaceFolders") {
|
||||||
|
let folders: Vec<WorkspaceFolder> =
|
||||||
|
serde_json::from_value(array.clone()).into_diagnostic()?;
|
||||||
|
for folder in folders {
|
||||||
|
self.workspace_folders.insert(folder.name.clone(), folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rename(&mut self, params: &RenameParams) -> Option<WorkspaceEdit> {
|
||||||
|
let new_name = params.new_name.to_owned();
|
||||||
|
// changes in WorkspaceEdit have mutable key
|
||||||
|
#[allow(clippy::mutable_key_type)]
|
||||||
|
let changes: HashMap<Uri, Vec<TextEdit>> = self
|
||||||
|
.occurrences
|
||||||
|
.iter()
|
||||||
|
.map(|(uri, ranges)| {
|
||||||
|
(
|
||||||
|
uri.clone(),
|
||||||
|
ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| TextEdit {
|
||||||
|
range: *range,
|
||||||
|
new_text: new_name.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Some(WorkspaceEdit {
|
||||||
|
changes: Some(changes),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Goto references response
|
||||||
|
/// TODO: WorkDoneProgress -> PartialResults
|
||||||
|
pub fn references(&mut self, params: &ReferenceParams) -> Option<Vec<Location>> {
|
||||||
|
self.occurrences = BTreeMap::new();
|
||||||
|
let mut engine_state = self.new_engine_state();
|
||||||
|
let path_uri = params.text_document_position.text_document.uri.to_owned();
|
||||||
|
let (mut working_set, id, span, _, _) = self
|
||||||
|
.parse_and_find(
|
||||||
|
&mut engine_state,
|
||||||
|
&path_uri,
|
||||||
|
params.text_document_position.position,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
self.find_reference_in_workspace(
|
||||||
|
&mut working_set,
|
||||||
|
&path_uri,
|
||||||
|
id,
|
||||||
|
span,
|
||||||
|
params
|
||||||
|
.work_done_progress_params
|
||||||
|
.work_done_token
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or(ProgressToken::Number(1)),
|
||||||
|
"Finding references ...",
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
Some(
|
||||||
|
self.occurrences
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(uri, ranges)| {
|
||||||
|
ranges.iter().map(|range| Location {
|
||||||
|
uri: uri.clone(),
|
||||||
|
range: *range,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 1. Parse current file to find the content at the cursor that is suitable for a workspace wide renaming
|
||||||
|
/// 2. Parse all nu scripts in the same workspace folder, with the variable/command name in it.
|
||||||
|
/// 3. Store the results in `self.occurrences` for later rename quest
|
||||||
|
pub fn prepare_rename(&mut self, request: Request) -> Result<()> {
|
||||||
|
let params: TextDocumentPositionParams =
|
||||||
|
serde_json::from_value(request.params).into_diagnostic()?;
|
||||||
|
self.occurrences = BTreeMap::new();
|
||||||
|
|
||||||
|
let mut engine_state = self.new_engine_state();
|
||||||
|
let path_uri = params.text_document.uri.to_owned();
|
||||||
|
|
||||||
|
let (mut working_set, id, span, file_offset, file) =
|
||||||
|
self.parse_and_find(&mut engine_state, &path_uri, params.position)?;
|
||||||
|
|
||||||
|
if let Id::Value(_) = id {
|
||||||
|
return Err(miette!("\nRename only works for variable/command."));
|
||||||
|
}
|
||||||
|
if Self::find_definition_span_by_id(&working_set, &id).is_none() {
|
||||||
|
return Err(miette!(
|
||||||
|
"\nDefinition not found.\nNot allowed to rename built-ins."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let range = span_to_range(&span, file, file_offset);
|
||||||
|
let response = PrepareRenameResponse::Range(range);
|
||||||
|
self.connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Response(Response {
|
||||||
|
id: request.id,
|
||||||
|
result: serde_json::to_value(response).ok(),
|
||||||
|
error: None,
|
||||||
|
}))
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
// now continue parsing on other files in the workspace
|
||||||
|
self.find_reference_in_workspace(
|
||||||
|
&mut working_set,
|
||||||
|
&path_uri,
|
||||||
|
id,
|
||||||
|
span,
|
||||||
|
ProgressToken::Number(0),
|
||||||
|
"Preparing rename ...",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_reference_in_workspace(
|
||||||
|
&mut self,
|
||||||
|
working_set: &mut StateWorkingSet,
|
||||||
|
current_uri: &Uri,
|
||||||
|
id: Id,
|
||||||
|
span: Span,
|
||||||
|
token: ProgressToken,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let current_workspace_folder = self
|
||||||
|
.get_workspace_folder_by_uri(current_uri)
|
||||||
|
.ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?;
|
||||||
|
let scripts: Vec<PathBuf> = Self::find_nu_scripts_in_folder(¤t_workspace_folder.uri)?
|
||||||
|
.filter_map(|p| p.ok())
|
||||||
|
.collect();
|
||||||
|
let len = scripts.len();
|
||||||
|
|
||||||
|
self.send_progress_begin(token.clone(), message)?;
|
||||||
|
for (i, fp) in scripts.iter().enumerate() {
|
||||||
|
let uri = path_to_uri(fp);
|
||||||
|
if let Some(file) = self.docs.get_document(&uri) {
|
||||||
|
Self::find_reference_in_file(working_set, file, fp, &id)
|
||||||
|
} else {
|
||||||
|
let bytes = fs::read(fp).into_diagnostic()?;
|
||||||
|
// skip if the file does not contain what we're looking for
|
||||||
|
let content_string = String::from_utf8(bytes).into_diagnostic()?;
|
||||||
|
let text_to_search =
|
||||||
|
String::from_utf8(working_set.get_span_contents(span).to_vec())
|
||||||
|
.into_diagnostic()?;
|
||||||
|
if !content_string.contains(&text_to_search) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let temp_file = FullTextDocument::new("nu".to_string(), 0, content_string);
|
||||||
|
Self::find_reference_in_file(working_set, &temp_file, fp, &id)
|
||||||
|
}
|
||||||
|
.and_then(|range| self.occurrences.insert(uri, range));
|
||||||
|
self.send_progress_report(token.clone(), (i * 100 / len) as u32, None)?
|
||||||
|
}
|
||||||
|
self.send_progress_end(token.clone(), Some("Done".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_reference_in_file(
|
||||||
|
working_set: &mut StateWorkingSet,
|
||||||
|
file: &FullTextDocument,
|
||||||
|
fp: &Path,
|
||||||
|
id: &Id,
|
||||||
|
) -> Option<Vec<Range>> {
|
||||||
|
let fp_str = fp.to_str()?;
|
||||||
|
let block = parse(
|
||||||
|
working_set,
|
||||||
|
Some(fp_str),
|
||||||
|
file.get_content(None).as_bytes(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let file_span = working_set.get_span_for_filename(fp_str)?;
|
||||||
|
let offset = file_span.start;
|
||||||
|
let mut references: Vec<Span> = find_reference_by_id(&block, working_set, id);
|
||||||
|
|
||||||
|
// NOTE: for arguments whose declaration is in a signature
|
||||||
|
// which is not covered in the AST
|
||||||
|
if let Id::Variable(vid) = id {
|
||||||
|
let decl_span = working_set.get_variable(*vid).declaration_span;
|
||||||
|
if file_span.contains_span(decl_span)
|
||||||
|
&& decl_span.end > decl_span.start
|
||||||
|
&& !references.contains(&decl_span)
|
||||||
|
{
|
||||||
|
references.push(decl_span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let occurs: Vec<Range> = references
|
||||||
|
.iter()
|
||||||
|
.map(|span| span_to_range(span, file, offset))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// add_block to avoid repeated parsing
|
||||||
|
working_set.add_block(block);
|
||||||
|
(!occurs.is_empty()).then_some(occurs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_workspace_folder_by_uri(&self, uri: &Uri) -> Option<WorkspaceFolder> {
|
||||||
|
let uri_string = uri.to_string();
|
||||||
|
self.workspace_folders
|
||||||
|
.iter()
|
||||||
|
.find_map(|(_, folder)| {
|
||||||
|
uri_string
|
||||||
|
.starts_with(&folder.uri.to_string())
|
||||||
|
.then_some(folder)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result<Paths> {
|
||||||
|
let path = uri_to_path(folder_uri);
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(miette!("\nworkspace folder does not exist."));
|
||||||
|
}
|
||||||
|
let pattern = format!("{}/**/*.nu", path.to_string_lossy());
|
||||||
|
glob(&pattern).into_diagnostic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use assert_json_diff::assert_json_eq;
|
||||||
|
use lsp_server::{Connection, Message};
|
||||||
|
use lsp_types::RenameParams;
|
||||||
|
use lsp_types::{
|
||||||
|
request, request::Request, InitializeParams, PartialResultParams, Position,
|
||||||
|
ReferenceContext, ReferenceParams, TextDocumentIdentifier, TextDocumentPositionParams, Uri,
|
||||||
|
WorkDoneProgressParams, WorkspaceFolder,
|
||||||
|
};
|
||||||
|
use nu_test_support::fs::fixtures;
|
||||||
|
|
||||||
|
use crate::path_to_uri;
|
||||||
|
use crate::tests::{initialize_language_server, open_unchecked};
|
||||||
|
|
||||||
|
fn send_reference_request(
|
||||||
|
client_connection: &Connection,
|
||||||
|
uri: Uri,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
num: usize,
|
||||||
|
) -> Vec<Message> {
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 1.into(),
|
||||||
|
method: request::References::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(ReferenceParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position { line, character },
|
||||||
|
},
|
||||||
|
context: ReferenceContext {
|
||||||
|
include_declaration: true,
|
||||||
|
},
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(0..num)
|
||||||
|
.map(|_| {
|
||||||
|
client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_rename_prepare_request(
|
||||||
|
client_connection: &Connection,
|
||||||
|
uri: Uri,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
num: usize,
|
||||||
|
) -> Vec<Message> {
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 1.into(),
|
||||||
|
method: request::PrepareRenameRequest::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri: uri.clone() },
|
||||||
|
position: Position { line, character },
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(0..num)
|
||||||
|
.map(|_| {
|
||||||
|
client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_rename_request(
|
||||||
|
client_connection: &Connection,
|
||||||
|
uri: Uri,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
) -> Message {
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 1.into(),
|
||||||
|
method: request::Rename::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(RenameParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position { line, character },
|
||||||
|
},
|
||||||
|
new_name: "new".to_string(),
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_reference_in_workspace() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("workspace");
|
||||||
|
let (client_connection, _recv) = initialize_language_server(Some(InitializeParams {
|
||||||
|
workspace_folders: Some(vec![WorkspaceFolder {
|
||||||
|
uri: path_to_uri(&script),
|
||||||
|
name: "random name".to_string(),
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
script.push("foo.nu");
|
||||||
|
let script = path_to_uri(&script);
|
||||||
|
|
||||||
|
open_unchecked(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let message_num = 5;
|
||||||
|
let messages =
|
||||||
|
send_reference_request(&client_connection, script.clone(), 0, 12, message_num);
|
||||||
|
assert_eq!(messages.len(), message_num);
|
||||||
|
for message in messages {
|
||||||
|
match message {
|
||||||
|
Message::Notification(n) => assert_eq!(n.method, "$/progress"),
|
||||||
|
Message::Response(r) => {
|
||||||
|
let result = r.result.unwrap();
|
||||||
|
let array = result.as_array().unwrap();
|
||||||
|
assert!(array.contains(&serde_json::json!(
|
||||||
|
{
|
||||||
|
"uri": script.to_string().replace("foo", "bar"),
|
||||||
|
"range": { "start": { "line": 4, "character": 2 }, "end": { "line": 4, "character": 7 } }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
));
|
||||||
|
assert!(array.contains(&serde_json::json!(
|
||||||
|
{
|
||||||
|
"uri": script.to_string(),
|
||||||
|
"range": { "start": { "line": 0, "character": 11 }, "end": { "line": 0, "character": 16 } }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => panic!("unexpected message type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quoted_command_reference_in_workspace() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("workspace");
|
||||||
|
let (client_connection, _recv) = initialize_language_server(Some(InitializeParams {
|
||||||
|
workspace_folders: Some(vec![WorkspaceFolder {
|
||||||
|
uri: path_to_uri(&script),
|
||||||
|
name: "random name".to_string(),
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
script.push("foo.nu");
|
||||||
|
let script = path_to_uri(&script);
|
||||||
|
|
||||||
|
open_unchecked(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let message_num = 5;
|
||||||
|
let messages =
|
||||||
|
send_reference_request(&client_connection, script.clone(), 6, 11, message_num);
|
||||||
|
assert_eq!(messages.len(), message_num);
|
||||||
|
for message in messages {
|
||||||
|
match message {
|
||||||
|
Message::Notification(n) => assert_eq!(n.method, "$/progress"),
|
||||||
|
Message::Response(r) => {
|
||||||
|
let result = r.result.unwrap();
|
||||||
|
let array = result.as_array().unwrap();
|
||||||
|
assert!(array.contains(&serde_json::json!(
|
||||||
|
{
|
||||||
|
"uri": script.to_string().replace("foo", "bar"),
|
||||||
|
"range": { "start": { "line": 5, "character": 4 }, "end": { "line": 5, "character": 11 } }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
));
|
||||||
|
assert!(array.contains(&serde_json::json!(
|
||||||
|
{
|
||||||
|
"uri": script.to_string(),
|
||||||
|
"range": { "start": { "line": 6, "character": 12 }, "end": { "line": 6, "character": 19 } }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => panic!("unexpected message type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_quoted_command() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("workspace");
|
||||||
|
let (client_connection, _recv) = initialize_language_server(Some(InitializeParams {
|
||||||
|
workspace_folders: Some(vec![WorkspaceFolder {
|
||||||
|
uri: path_to_uri(&script),
|
||||||
|
name: "random name".to_string(),
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
script.push("foo.nu");
|
||||||
|
let script = path_to_uri(&script);
|
||||||
|
|
||||||
|
open_unchecked(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let message_num = 5;
|
||||||
|
let messages =
|
||||||
|
send_rename_prepare_request(&client_connection, script.clone(), 6, 11, message_num);
|
||||||
|
assert_eq!(messages.len(), message_num);
|
||||||
|
for message in messages {
|
||||||
|
match message {
|
||||||
|
Message::Notification(n) => assert_eq!(n.method, "$/progress"),
|
||||||
|
Message::Response(r) => assert_json_eq!(
|
||||||
|
r.result,
|
||||||
|
serde_json::json!({
|
||||||
|
"start": { "line": 6, "character": 12 },
|
||||||
|
"end": { "line": 6, "character": 19 }
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => panic!("unexpected message type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 6, 11)
|
||||||
|
{
|
||||||
|
let changes = r.result.unwrap()["changes"].clone();
|
||||||
|
assert_json_eq!(
|
||||||
|
changes[script.to_string()],
|
||||||
|
serde_json::json!([
|
||||||
|
{
|
||||||
|
"range": { "start": { "line": 6, "character": 12 }, "end": { "line": 6, "character": 19 } },
|
||||||
|
"newText": "new"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_json_eq!(
|
||||||
|
changes[script.to_string().replace("foo", "bar")],
|
||||||
|
serde_json::json!([
|
||||||
|
{
|
||||||
|
"range": { "start": { "line": 5, "character": 4 }, "end": { "line": 5, "character": 11 } },
|
||||||
|
"newText": "new"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_command_argument() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("workspace");
|
||||||
|
let (client_connection, _recv) = initialize_language_server(Some(InitializeParams {
|
||||||
|
workspace_folders: Some(vec![WorkspaceFolder {
|
||||||
|
uri: path_to_uri(&script),
|
||||||
|
name: "random name".to_string(),
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
script.push("foo.nu");
|
||||||
|
let script = path_to_uri(&script);
|
||||||
|
|
||||||
|
open_unchecked(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let message_num = 4;
|
||||||
|
let messages =
|
||||||
|
send_rename_prepare_request(&client_connection, script.clone(), 3, 5, message_num);
|
||||||
|
assert_eq!(messages.len(), message_num);
|
||||||
|
for message in messages {
|
||||||
|
match message {
|
||||||
|
Message::Notification(n) => assert_eq!(n.method, "$/progress"),
|
||||||
|
Message::Response(r) => assert_json_eq!(
|
||||||
|
r.result,
|
||||||
|
serde_json::json!({
|
||||||
|
"start": { "line": 3, "character": 3 },
|
||||||
|
"end": { "line": 3, "character": 8 }
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => panic!("unexpected message type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Message::Response(r) = send_rename_request(&client_connection, script.clone(), 3, 5)
|
||||||
|
{
|
||||||
|
assert_json_eq!(
|
||||||
|
r.result,
|
||||||
|
serde_json::json!({
|
||||||
|
"changes": {
|
||||||
|
script.to_string(): [
|
||||||
|
{
|
||||||
|
"range": { "start": { "line": 3, "character": 3 }, "end": { "line": 3, "character": 8 } },
|
||||||
|
"newText": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } },
|
||||||
|
"newText": "new"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
tests/fixtures/lsp/workspace/bar.nu
vendored
Normal file
7
tests/fixtures/lsp/workspace/bar.nu
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use foo.nu [ foooo "foo str" ]
|
||||||
|
|
||||||
|
export def "bar str" [
|
||||||
|
] {
|
||||||
|
foooo 3
|
||||||
|
| foo str
|
||||||
|
}
|
7
tests/fixtures/lsp/workspace/foo.nu
vendored
Normal file
7
tests/fixtures/lsp/workspace/foo.nu
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export def foooo [
|
||||||
|
param: int
|
||||||
|
] {
|
||||||
|
$param
|
||||||
|
}
|
||||||
|
|
||||||
|
export def "foo str" [] { "foo" }
|
Loading…
Reference in New Issue
Block a user