From e5337b50a92129a0f18c9e90e0229ba77073a8c8 Mon Sep 17 00:00:00 2001 From: zc he Date: Mon, 13 Jan 2025 21:00:01 +0800 Subject: [PATCH] fix(lsp): goto definition on variables in `match guard` (#14818) # Description This PR fixes a corner case of goto definition in lsp server. ```nushell let foo = 1 match $foo { _ if $foo == 1 => 1 # |_______________ goto definition does not work here _ => 2 } ``` Since `match_pattern.guard` is not handled in this function (which could be another issue). https://github.com/nushell/nushell/blob/23dc1b600a0e09279cef75f0dbf47505c37edeca/crates/nu-parser/src/flatten.rs#L604-L658 In this PR, however, finding leaf expression at the cursor is done with the new AST traversing helper functions. Theoretically, this is faster as the flattening and filtering are combined in a single scan; the difference could be negligible though. # User-Facing Changes # Tests + Formatting 3 new test cases added, will add more if new issues found. # After Submitting --- crates/nu-lsp/src/ast.rs | 210 +++++++++++++++++ crates/nu-lsp/src/goto.rs | 344 ++++++++++++++++++++++++++++ crates/nu-lsp/src/hints.rs | 161 +------------ crates/nu-lsp/src/lib.rs | 351 +++-------------------------- tests/fixtures/lsp/goto/collect.nu | 2 + tests/fixtures/lsp/goto/else.nu | 2 + tests/fixtures/lsp/goto/match.nu | 5 + 7 files changed, 601 insertions(+), 474 deletions(-) create mode 100644 crates/nu-lsp/src/ast.rs create mode 100644 crates/nu-lsp/src/goto.rs create mode 100644 tests/fixtures/lsp/goto/collect.nu create mode 100644 tests/fixtures/lsp/goto/else.nu create mode 100644 tests/fixtures/lsp/goto/match.nu diff --git a/crates/nu-lsp/src/ast.rs b/crates/nu-lsp/src/ast.rs new file mode 100644 index 0000000000..44ea202e8a --- /dev/null +++ b/crates/nu-lsp/src/ast.rs @@ -0,0 +1,210 @@ +use std::sync::Arc; + +use nu_protocol::{ + ast::{ + Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Pattern, + PipelineRedirection, RecordItem, + }, + engine::StateWorkingSet, +}; + +use crate::Id; + +/// similar to flatten_block, but allows extra map function +pub fn ast_flat_map( + ast: &Arc, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + ast.pipelines + .iter() + .flat_map(|pipeline| { + pipeline.elements.iter().flat_map(|element| { + expr_flat_map(&element.expr, working_set, extra_args, f_special) + .into_iter() + .chain( + element + .redirection + .as_ref() + .map(|redir| { + redirect_flat_map(redir, working_set, extra_args, f_special) + }) + .unwrap_or_default(), + ) + }) + }) + .collect() +} + +/// generic function that do flat_map on an expression +/// concats all recursive results on sub-expressions +/// +/// # Arguments +/// * `f_special` - function that overrides the default behavior +pub fn expr_flat_map( + expr: &Expression, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + // behavior overridden by f_special + if let Some(vec) = f_special(expr, working_set, extra_args) { + return vec; + } + let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special); + match &expr.expr { + Expr::RowCondition(block_id) + | Expr::Subexpression(block_id) + | Expr::Block(block_id) + | Expr::Closure(block_id) => { + let block = working_set.get_block(block_id.to_owned()); + ast_flat_map(block, working_set, extra_args, f_special) + } + Expr::Range(range) => [&range.from, &range.next, &range.to] + .iter() + .filter_map(|e| e.as_ref()) + .flat_map(recur) + .collect(), + Expr::Call(call) => call + .arguments + .iter() + .filter_map(|arg| arg.expr()) + .flat_map(recur) + .collect(), + Expr::ExternalCall(head, args) => recur(head) + .into_iter() + .chain(args.iter().flat_map(|arg| match arg { + ExternalArgument::Regular(e) | ExternalArgument::Spread(e) => recur(e), + })) + .collect(), + Expr::UnaryNot(expr) | Expr::Collect(_, expr) => recur(expr), + Expr::BinaryOp(lhs, op, rhs) => recur(lhs) + .into_iter() + .chain(recur(op)) + .chain(recur(rhs)) + .collect(), + Expr::MatchBlock(matches) => matches + .iter() + .flat_map(|(pattern, expr)| { + match_pattern_flat_map(pattern, working_set, extra_args, f_special) + .into_iter() + .chain(recur(expr)) + }) + .collect(), + Expr::List(items) => items + .iter() + .flat_map(|item| match item { + ListItem::Item(expr) | ListItem::Spread(_, expr) => recur(expr), + }) + .collect(), + Expr::Record(items) => items + .iter() + .flat_map(|item| match item { + RecordItem::Spread(_, expr) => recur(expr), + RecordItem::Pair(key, val) => [key, val].into_iter().flat_map(recur).collect(), + }) + .collect(), + Expr::Table(table) => table + .columns + .iter() + .flat_map(recur) + .chain(table.rows.iter().flat_map(|row| row.iter().flat_map(recur))) + .collect(), + Expr::ValueWithUnit(vu) => recur(&vu.expr), + Expr::FullCellPath(fcp) => recur(&fcp.head), + Expr::Keyword(kw) => recur(&kw.expr), + Expr::StringInterpolation(vec) | Expr::GlobInterpolation(vec, _) => { + vec.iter().flat_map(recur).collect() + } + + _ => Vec::new(), + } +} + +/// flat_map on match patterns +fn match_pattern_flat_map( + pattern: &MatchPattern, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special); + let recur_match = |p| match_pattern_flat_map(p, working_set, extra_args, f_special); + match &pattern.pattern { + Pattern::Expression(expr) => recur(expr), + Pattern::List(patterns) | Pattern::Or(patterns) => { + patterns.iter().flat_map(recur_match).collect() + } + Pattern::Record(entries) => entries.iter().flat_map(|(_, p)| recur_match(p)).collect(), + _ => Vec::new(), + } + .into_iter() + .chain(pattern.guard.as_ref().map(|g| recur(g)).unwrap_or_default()) + .collect() +} + +/// flat_map on redirections +fn redirect_flat_map( + redir: &PipelineRedirection, + working_set: &StateWorkingSet, + extra_args: &E, + f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, +) -> Vec { + let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special); + match redir { + PipelineRedirection::Single { target, .. } => target.expr().map(recur).unwrap_or_default(), + PipelineRedirection::Separate { out, err } => [out, err] + .iter() + .filter_map(|t| t.expr()) + .flat_map(recur) + .collect(), + } +} + +fn find_id_in_expr(expr: &Expression, _: &StateWorkingSet, location: &usize) -> Option> { + // skip the entire expression if the location is not in it + if !expr.span.contains(*location) { + // TODO: the span of Keyword does not include its subsidiary expression + // resort to `expr_flat_map` if location found in its expr + if let Expr::Keyword(kw) = &expr.expr { + if kw.expr.span.contains(*location) { + return None; + } + } + return Some(Vec::new()); + } + match &expr.expr { + Expr::Var(var_id) | Expr::VarDecl(var_id) => Some(vec![Id::Variable(*var_id)]), + Expr::Call(call) => { + if call.head.contains(*location) { + Some(vec![Id::Declaration(call.decl_id)]) + } else { + None + } + } + Expr::Overlay(Some(module_id)) => Some(vec![Id::Module(*module_id)]), + // terminal value expressions + Expr::Bool(_) + | Expr::Binary(_) + | Expr::DateTime(_) + | Expr::Directory(_, _) + | Expr::Filepath(_, _) + | Expr::Float(_) + | Expr::Garbage + | Expr::GlobPattern(_, _) + | Expr::Int(_) + | Expr::Nothing + | Expr::RawString(_) + | Expr::Signature(_) + | Expr::String(_) => Some(vec![Id::Value(expr.ty.clone())]), + _ => None, + } +} + +/// find the leaf node at the given location from ast +pub fn find_id(ast: &Arc, working_set: &StateWorkingSet, location: &usize) -> Option { + ast_flat_map(ast, working_set, location, find_id_in_expr) + .first() + .cloned() +} diff --git a/crates/nu-lsp/src/goto.rs b/crates/nu-lsp/src/goto.rs new file mode 100644 index 0000000000..7589323cab --- /dev/null +++ b/crates/nu-lsp/src/goto.rs @@ -0,0 +1,344 @@ +use crate::ast::find_id; +use crate::{Id, LanguageServer}; +use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse}; + +impl LanguageServer { + pub fn goto_definition( + &mut self, + params: &GotoDefinitionParams, + ) -> Option { + let mut engine_state = self.new_engine_state(); + + let path_uri = params + .text_document_position_params + .text_document + .uri + .to_owned(); + let (block, file_offset, working_set, file) = + self.parse_file(&mut engine_state, &path_uri, false)?; + let location = + file.offset_at(params.text_document_position_params.position) as usize + file_offset; + let id = find_id(&block, &working_set, &location)?; + + let 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, + }?; + Some(GotoDefinitionResponse::Scalar( + self.get_location_by_span(working_set.files(), &span)?, + )) + } +} + +#[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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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 } + } + }) + ); + } +} diff --git a/crates/nu-lsp/src/hints.rs b/crates/nu-lsp/src/hints.rs index eb659f8812..136fca0371 100644 --- a/crates/nu-lsp/src/hints.rs +++ b/crates/nu-lsp/src/hints.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::ast::{ast_flat_map, expr_flat_map}; use crate::{span_to_range, LanguageServer}; use lsp_textdocument::FullTextDocument; use lsp_types::{ @@ -7,166 +8,11 @@ use lsp_types::{ MarkupKind, Position, Range, }; use nu_protocol::{ - ast::{ - Argument, Block, Expr, Expression, ExternalArgument, ListItem, MatchPattern, Operator, - Pattern, PipelineRedirection, RecordItem, - }, + ast::{Argument, Block, Expr, Expression, Operator}, engine::StateWorkingSet, Type, }; -/// similar to flatten_block, but allows extra map function -fn ast_flat_map( - ast: &Arc, - working_set: &StateWorkingSet, - extra_args: &E, - f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, -) -> Vec { - ast.pipelines - .iter() - .flat_map(|pipeline| { - pipeline.elements.iter().flat_map(|element| { - expr_flat_map(&element.expr, working_set, extra_args, f_special) - .into_iter() - .chain( - element - .redirection - .as_ref() - .map(|redir| { - redirect_flat_map(redir, working_set, extra_args, f_special) - }) - .unwrap_or_default(), - ) - }) - }) - .collect() -} - -/// generic function that do flat_map on an expression -/// concats all recursive results on sub-expressions -/// -/// # Arguments -/// * `f_special` - function that overrides the default behavior -fn expr_flat_map( - expr: &Expression, - working_set: &StateWorkingSet, - extra_args: &E, - f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, -) -> Vec { - // behavior overridden by f_special - if let Some(vec) = f_special(expr, working_set, extra_args) { - return vec; - } - let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special); - match &expr.expr { - Expr::RowCondition(block_id) - | Expr::Subexpression(block_id) - | Expr::Block(block_id) - | Expr::Closure(block_id) => { - let block = working_set.get_block(block_id.to_owned()); - ast_flat_map(block, working_set, extra_args, f_special) - } - Expr::Range(range) => [&range.from, &range.next, &range.to] - .iter() - .filter_map(|e| e.as_ref()) - .flat_map(recur) - .collect(), - Expr::Call(call) => call - .arguments - .iter() - .filter_map(|arg| arg.expr()) - .flat_map(recur) - .collect(), - Expr::ExternalCall(head, args) => recur(head) - .into_iter() - .chain(args.iter().flat_map(|arg| match arg { - ExternalArgument::Regular(e) | ExternalArgument::Spread(e) => recur(e), - })) - .collect(), - Expr::UnaryNot(expr) | Expr::Collect(_, expr) => recur(expr), - Expr::BinaryOp(lhs, op, rhs) => recur(lhs) - .into_iter() - .chain(recur(op)) - .chain(recur(rhs)) - .collect(), - Expr::MatchBlock(matches) => matches - .iter() - .flat_map(|(pattern, expr)| { - match_pattern_flat_map(pattern, working_set, extra_args, f_special) - .into_iter() - .chain(recur(expr)) - }) - .collect(), - Expr::List(items) => items - .iter() - .flat_map(|item| match item { - ListItem::Item(expr) | ListItem::Spread(_, expr) => recur(expr), - }) - .collect(), - Expr::Record(items) => items - .iter() - .flat_map(|item| match item { - RecordItem::Spread(_, expr) => recur(expr), - RecordItem::Pair(key, val) => [key, val].into_iter().flat_map(recur).collect(), - }) - .collect(), - Expr::Table(table) => table - .columns - .iter() - .flat_map(recur) - .chain(table.rows.iter().flat_map(|row| row.iter().flat_map(recur))) - .collect(), - Expr::ValueWithUnit(vu) => recur(&vu.expr), - Expr::FullCellPath(fcp) => recur(&fcp.head), - Expr::Keyword(kw) => recur(&kw.expr), - Expr::StringInterpolation(vec) | Expr::GlobInterpolation(vec, _) => { - vec.iter().flat_map(recur).collect() - } - - _ => Vec::new(), - } -} - -/// flat_map on match patterns -fn match_pattern_flat_map( - pattern: &MatchPattern, - working_set: &StateWorkingSet, - extra_args: &E, - f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, -) -> Vec { - let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special); - let recur_match = |p| match_pattern_flat_map(p, working_set, extra_args, f_special); - match &pattern.pattern { - Pattern::Expression(expr) => recur(expr), - Pattern::List(patterns) | Pattern::Or(patterns) => { - patterns.iter().flat_map(recur_match).collect() - } - Pattern::Record(entries) => entries.iter().flat_map(|(_, p)| recur_match(p)).collect(), - _ => Vec::new(), - } - .into_iter() - .chain(pattern.guard.as_ref().map(|g| recur(g)).unwrap_or_default()) - .collect() -} - -/// flat_map on redirections -fn redirect_flat_map( - redir: &PipelineRedirection, - working_set: &StateWorkingSet, - extra_args: &E, - f_special: fn(&Expression, &StateWorkingSet, &E) -> Option>, -) -> Vec { - let recur = |expr| expr_flat_map(expr, working_set, extra_args, f_special); - match redir { - PipelineRedirection::Single { target, .. } => target.expr().map(recur).unwrap_or_default(), - PipelineRedirection::Separate { out, err } => [out, err] - .iter() - .filter_map(|t| t.expr()) - .flat_map(recur) - .collect(), - } -} - fn type_short_name(t: &Type) -> String { match t { Type::Custom(_) => String::from("custom"), @@ -182,7 +28,6 @@ fn extract_inlay_hints_from_expression( working_set: &StateWorkingSet, extra_args: &(usize, &FullTextDocument), ) -> Option> { - let span = expr.span; let (offset, file) = extra_args; let recur = |expr| { expr_flat_map( @@ -219,7 +64,7 @@ fn extract_inlay_hints_from_expression( Some(hints) } Expr::VarDecl(var_id) => { - let position = span_to_range(&span, file, *offset).end; + let position = span_to_range(&expr.span, file, *offset).end; // skip if the type is already specified in code if file .get_content(Some(Range { diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 0bf83e6dfe..4013e3a19a 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1,23 +1,20 @@ #![doc = include_str!("../README.md")] +use ast::find_id; use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ - request::{ - Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, InlayHintRequest, Request, - WorkspaceSymbolRequest, - }, - CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit, - GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, InlayHint, - Location, MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, - TextEdit, Uri, + request, request::Request, CompletionItem, CompletionItemKind, CompletionParams, + CompletionResponse, CompletionTextEdit, Hover, HoverContents, HoverParams, InlayHint, Location, + MarkupContent, MarkupKind, OneOf, Range, RenameOptions, ServerCapabilities, + TextDocumentSyncKind, TextEdit, Uri, WorkDoneProgressOptions, }; use miette::{IntoDiagnostic, Result}; use nu_cli::{NuCompleter, SuggestionKind}; -use nu_parser::{flatten_block, parse, FlatShape}; +use nu_parser::parse; use nu_protocol::{ ast::Block, engine::{CachedFile, EngineState, Stack, StateWorkingSet}, - DeclId, ModuleId, Span, Value, VarId, + DeclId, ModuleId, Span, Type, Value, VarId, }; use std::collections::BTreeMap; use std::{ @@ -29,16 +26,18 @@ use std::{ use symbols::SymbolCache; use url::Url; +mod ast; mod diagnostics; +mod goto; mod hints; mod notification; mod symbols; -#[derive(Debug)] +#[derive(Debug, Clone)] enum Id { Variable(VarId), Declaration(DeclId), - Value(FlatShape), + Value(Type), Module(ModuleId), } @@ -105,10 +104,16 @@ impl LanguageServer { document_symbol_provider: Some(OneOf::Left(true)), workspace_symbol_provider: Some(OneOf::Left(true)), inlay_hint_provider: Some(OneOf::Left(true)), + rename_provider: Some(OneOf::Right(RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: Some(true), + }, + })), + references_provider: Some(OneOf::Left(true)), ..Default::default() }) .expect("Must be serializable"); - let _ = self .connection .initialize_while(server_capabilities, || { @@ -140,24 +145,24 @@ impl LanguageServer { } let resp = match request.method.as_str() { - GotoDefinition::METHOD => { + request::GotoDefinition::METHOD => { Self::handle_lsp_request(request, |params| self.goto_definition(params)) } - HoverRequest::METHOD => { + request::HoverRequest::METHOD => { Self::handle_lsp_request(request, |params| self.hover(params)) } - Completion::METHOD => { + request::Completion::METHOD => { Self::handle_lsp_request(request, |params| self.complete(params)) } - DocumentSymbolRequest::METHOD => { + request::DocumentSymbolRequest::METHOD => { Self::handle_lsp_request(request, |params| self.document_symbol(params)) } - WorkspaceSymbolRequest::METHOD => { + request::WorkspaceSymbolRequest::METHOD => { Self::handle_lsp_request(request, |params| { self.workspace_symbol(params) }) } - InlayHintRequest::METHOD => { + request::InlayHintRequest::METHOD => { Self::handle_lsp_request(request, |params| self.get_inlay_hints(params)) } _ => { @@ -281,66 +286,6 @@ impl LanguageServer { } } - fn find_id( - flattened: Vec<(Span, FlatShape)>, - location: usize, - offset: usize, - ) -> Option<(Id, usize, Span)> { - let location = location + offset; - - for (span, shape) in flattened { - if location >= span.start && location < span.end { - match &shape { - FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => { - return Some((Id::Variable(*var_id), offset, span)); - } - FlatShape::InternalCall(decl_id) | FlatShape::Custom(decl_id) => { - return Some((Id::Declaration(*decl_id), offset, span)); - } - _ => return Some((Id::Value(shape), offset, span)), - } - } - } - None - } - - fn goto_definition(&mut self, params: &GotoDefinitionParams) -> Option { - let mut engine_state = self.new_engine_state(); - - let path_uri = params - .text_document_position_params - .text_document - .uri - .to_owned(); - let (block, file_offset, working_set, file) = - self.parse_file(&mut engine_state, &path_uri, false)?; - let flattened = flatten_block(&working_set, &block); - let (id, _, _) = Self::find_id( - flattened, - file.offset_at(params.text_document_position_params.position) as usize, - file_offset, - )?; - - let 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, - }?; - Some(GotoDefinitionResponse::Scalar( - self.get_location_by_span(working_set.files(), &span)?, - )) - } - fn hover(&mut self, params: &HoverParams) -> Option { let mut engine_state = self.new_engine_state(); @@ -351,12 +296,9 @@ impl LanguageServer { .to_owned(); let (block, file_offset, working_set, file) = self.parse_file(&mut engine_state, &path_uri, false)?; - let flattened = flatten_block(&working_set, &block); - let (id, _, _) = Self::find_id( - flattened, - file.offset_at(params.text_document_position_params.position) as usize, - file_offset, - )?; + let location = + file.offset_at(params.text_document_position_params.position) as usize + file_offset; + let id = find_id(&block, &working_set, &location)?; match id { Id::Variable(var_id) => { @@ -518,37 +460,9 @@ impl LanguageServer { range: None, }) } - Id::Value(shape) => { - let hover = String::from(match shape { - FlatShape::Binary => "binary", - FlatShape::Block => "block", - FlatShape::Bool => "bool", - FlatShape::Closure => "closure", - FlatShape::DateTime => "datetime", - FlatShape::Directory => "directory", - FlatShape::External => "external", - FlatShape::ExternalArg => "external arg", - FlatShape::Filepath => "file path", - FlatShape::Flag => "flag", - FlatShape::Float => "float", - FlatShape::GlobPattern => "glob pattern", - FlatShape::Int => "int", - FlatShape::Keyword => "keyword", - FlatShape::List => "list", - FlatShape::MatchPattern => "match-pattern", - FlatShape::Nothing => "nothing", - FlatShape::Range => "range", - FlatShape::Record => "record", - FlatShape::String => "string", - FlatShape::StringInterpolation => "string interpolation", - FlatShape::Table => "table", - _ => { - return None; - } - }); - + Id::Value(t) => { Some(Hover { - contents: HoverContents::Scalar(lsp_types::MarkedString::String(hover)), + contents: HoverContents::Scalar(lsp_types::MarkedString::String(t.to_string())), // TODO range: None, }) @@ -620,13 +534,13 @@ mod tests { notification::{ DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification, }, - request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown}, - CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - GotoDefinitionParams, InitializeParams, InitializedParams, PartialResultParams, Position, - TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, - TextDocumentPositionParams, WorkDoneProgressParams, + request::{Completion, HoverRequest, Initialize, Request, Shutdown}, + CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, + InitializedParams, PartialResultParams, Position, TextDocumentContentChangeEvent, + TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, + WorkDoneProgressParams, }; - use nu_test_support::fs::{fixtures, root}; + use nu_test_support::fs::fixtures; use std::sync::mpsc::Receiver; pub fn initialize_language_server() -> (Connection, Receiver>) { @@ -696,48 +610,6 @@ mod tests { .is_ok()); } - #[test] - fn goto_definition_for_none_existing_file() { - let (client_connection, _recv) = initialize_language_server(); - - 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)); - } - pub fn open_unchecked(client_connection: &Connection, uri: Uri) -> lsp_server::Notification { open(client_connection, uri).unwrap() } @@ -815,159 +687,6 @@ mod tests { } } - 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_of_variable() { - let (client_connection, _recv) = initialize_language_server(); - - 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(); - - 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(); - - 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(); - - 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 } - } - }) - ); - } - pub fn send_hover_request( client_connection: &Connection, uri: Uri, diff --git a/tests/fixtures/lsp/goto/collect.nu b/tests/fixtures/lsp/goto/collect.nu new file mode 100644 index 0000000000..bfd14fcb52 --- /dev/null +++ b/tests/fixtures/lsp/goto/collect.nu @@ -0,0 +1,2 @@ +let foo = 1 +..2 | each { $foo } diff --git a/tests/fixtures/lsp/goto/else.nu b/tests/fixtures/lsp/goto/else.nu new file mode 100644 index 0000000000..04018b111c --- /dev/null +++ b/tests/fixtures/lsp/goto/else.nu @@ -0,0 +1,2 @@ +let foo = 1 +if true { } else { $foo } diff --git a/tests/fixtures/lsp/goto/match.nu b/tests/fixtures/lsp/goto/match.nu new file mode 100644 index 0000000000..52836f2209 --- /dev/null +++ b/tests/fixtures/lsp/goto/match.nu @@ -0,0 +1,5 @@ +let foo = 1 +match $foo { + _ if $foo == 1 => 1 + _ => 2 +}