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 +}