2023-11-16 00:35:48 +01:00
use std ::{
collections ::BTreeMap ,
path ::{ Path , PathBuf } ,
sync ::{
atomic ::{ AtomicBool , Ordering } ,
Arc ,
} ,
time ::Duration ,
} ;
2023-11-02 16:18:57 +01:00
use lsp_server ::{ Connection , IoThreads , Message , Response , ResponseError } ;
use lsp_types ::{
request ::{ Completion , GotoDefinition , HoverRequest , Request } ,
CompletionItem , CompletionParams , CompletionResponse , CompletionTextEdit , GotoDefinitionParams ,
GotoDefinitionResponse , Hover , HoverContents , HoverParams , Location , MarkupContent , MarkupKind ,
2023-11-16 00:35:48 +01:00
OneOf , Range , ServerCapabilities , TextDocumentSyncKind , TextEdit , Url ,
2023-11-02 16:18:57 +01:00
} ;
use miette ::{ IntoDiagnostic , Result } ;
use nu_cli ::NuCompleter ;
use nu_parser ::{ flatten_block , parse , FlatShape } ;
use nu_protocol ::{
engine ::{ EngineState , Stack , StateWorkingSet } ,
DeclId , Span , Value , VarId ,
} ;
use reedline ::Completer ;
use ropey ::Rope ;
2023-11-16 00:35:48 +01:00
mod diagnostics ;
mod notification ;
2023-11-02 16:18:57 +01:00
#[ derive(Debug) ]
enum Id {
Variable ( VarId ) ,
Declaration ( DeclId ) ,
Value ( FlatShape ) ,
}
pub struct LanguageServer {
connection : Connection ,
io_threads : Option < IoThreads > ,
2023-11-16 00:35:48 +01:00
ropes : BTreeMap < PathBuf , Rope > ,
2023-11-02 16:18:57 +01:00
}
impl LanguageServer {
pub fn initialize_stdio_connection ( ) -> Result < Self > {
let ( connection , io_threads ) = Connection ::stdio ( ) ;
Self ::initialize_connection ( connection , Some ( io_threads ) )
}
fn initialize_connection (
connection : Connection ,
io_threads : Option < IoThreads > ,
) -> Result < Self > {
Ok ( Self {
connection ,
io_threads ,
2023-11-16 00:35:48 +01:00
ropes : BTreeMap ::new ( ) ,
2023-11-02 16:18:57 +01:00
} )
}
2023-11-16 00:35:48 +01:00
pub fn serve_requests (
mut self ,
engine_state : EngineState ,
ctrlc : Arc < AtomicBool > ,
) -> Result < ( ) > {
2023-11-02 16:18:57 +01:00
let server_capabilities = serde_json ::to_value ( & ServerCapabilities {
2023-11-16 00:35:48 +01:00
text_document_sync : Some ( lsp_types ::TextDocumentSyncCapability ::Kind (
TextDocumentSyncKind ::INCREMENTAL ,
) ) ,
2023-11-02 16:18:57 +01:00
definition_provider : Some ( OneOf ::Left ( true ) ) ,
hover_provider : Some ( lsp_types ::HoverProviderCapability ::Simple ( true ) ) ,
completion_provider : Some ( lsp_types ::CompletionOptions ::default ( ) ) ,
.. Default ::default ( )
} )
. expect ( " Must be serializable " ) ;
let _initialization_params = self
. connection
2023-11-16 00:35:48 +01:00
. initialize_while ( server_capabilities , | | ! ctrlc . load ( Ordering ::SeqCst ) )
2023-11-02 16:18:57 +01:00
. into_diagnostic ( ) ? ;
2023-11-16 00:35:48 +01:00
while ! ctrlc . load ( Ordering ::SeqCst ) {
let msg = match self
. connection
. receiver
. recv_timeout ( Duration ::from_secs ( 1 ) )
{
Ok ( msg ) = > msg ,
Err ( crossbeam_channel ::RecvTimeoutError ::Timeout ) = > {
continue ;
}
Err ( _ ) = > break ,
} ;
2023-11-02 16:18:57 +01:00
match msg {
Message ::Request ( request ) = > {
if self
. connection
. handle_shutdown ( & request )
. into_diagnostic ( ) ?
{
return Ok ( ( ) ) ;
}
let mut engine_state = engine_state . clone ( ) ;
2023-11-16 00:35:48 +01:00
let resp = match request . method . as_str ( ) {
GotoDefinition ::METHOD = > Self ::handle_lsp_request (
& mut engine_state ,
request ,
| engine_state , params | self . goto_definition ( engine_state , params ) ,
) ,
HoverRequest ::METHOD = > Self ::handle_lsp_request (
& mut engine_state ,
request ,
| engine_state , params | self . hover ( engine_state , params ) ,
) ,
Completion ::METHOD = > Self ::handle_lsp_request (
& mut engine_state ,
request ,
| engine_state , params | self . complete ( engine_state , params ) ,
) ,
_ = > {
continue ;
2023-11-02 16:18:57 +01:00
}
2023-11-16 00:35:48 +01:00
} ;
self . connection
. sender
. send ( Message ::Response ( resp ) )
. into_diagnostic ( ) ? ;
2023-11-02 16:18:57 +01:00
}
Message ::Response ( _ ) = > { }
2023-11-16 00:35:48 +01:00
Message ::Notification ( notification ) = > {
if let Some ( updated_file ) = self . handle_lsp_notification ( notification ) {
let mut engine_state = engine_state . clone ( ) ;
self . publish_diagnostics_for_file ( updated_file , & mut engine_state ) ? ;
}
}
2023-11-02 16:18:57 +01:00
}
}
if let Some ( io_threads ) = self . io_threads {
io_threads . join ( ) . into_diagnostic ( ) ? ;
}
Ok ( ( ) )
}
fn handle_lsp_request < P , H , R > (
engine_state : & mut EngineState ,
req : lsp_server ::Request ,
2023-11-16 00:35:48 +01:00
mut param_handler : H ,
) -> Response
2023-11-02 16:18:57 +01:00
where
P : serde ::de ::DeserializeOwned ,
2023-11-16 00:35:48 +01:00
H : FnMut ( & mut EngineState , & P ) -> Option < R > ,
2023-11-02 16:18:57 +01:00
R : serde ::ser ::Serialize ,
{
2023-11-16 00:35:48 +01:00
match serde_json ::from_value ::< P > ( req . params ) {
Ok ( params ) = > Response {
id : req . id ,
result : Some (
param_handler ( engine_state , & params )
. and_then ( | response | serde_json ::to_value ( response ) . ok ( ) )
. unwrap_or ( serde_json ::Value ::Null ) ,
) ,
error : None ,
} ,
Err ( err ) = > Response {
id : req . id ,
result : None ,
error : Some ( ResponseError {
code : 1 ,
message : err . to_string ( ) ,
data : None ,
} ) ,
} ,
}
2023-11-02 16:18:57 +01:00
}
fn span_to_range ( span : & Span , rope_of_file : & Rope , offset : usize ) -> lsp_types ::Range {
let line = rope_of_file . char_to_line ( span . start - offset ) ;
let character = span . start - offset - rope_of_file . line_to_char ( line ) ;
let start = lsp_types ::Position {
line : line as u32 ,
character : character as u32 ,
} ;
let line = rope_of_file . char_to_line ( span . end - offset ) ;
let character = span . end - offset - rope_of_file . line_to_char ( line ) ;
let end = lsp_types ::Position {
line : line as u32 ,
character : character as u32 ,
} ;
lsp_types ::Range { start , end }
}
2023-11-16 00:35:48 +01:00
pub fn lsp_position_to_location ( position : & lsp_types ::Position , rope_of_file : & Rope ) -> usize {
2023-11-02 16:18:57 +01:00
let line_idx = rope_of_file . line_to_char ( position . line as usize ) ;
line_idx + position . character as usize
}
fn find_id (
working_set : & mut StateWorkingSet ,
2023-11-16 00:35:48 +01:00
path : & Path ,
file : & Rope ,
2023-11-02 16:18:57 +01:00
location : usize ,
) -> Option < ( Id , usize , Span ) > {
2023-11-16 00:35:48 +01:00
let file_path = path . to_string_lossy ( ) ;
// TODO: think about passing down the rope into the working_set
let contents = file . bytes ( ) . collect ::< Vec < u8 > > ( ) ;
let block = parse ( working_set , Some ( & file_path ) , & contents , false ) ;
2023-11-02 16:18:57 +01:00
let flattened = flatten_block ( working_set , & block ) ;
2023-11-16 00:35:48 +01:00
let offset = working_set . get_span_for_filename ( & file_path ) ? . start ;
2023-11-02 16:18:57 +01:00
let location = location + offset ;
2023-11-16 00:35:48 +01:00
for ( span , shape ) in flattened {
if location > = span . start & & location < span . end {
match & shape {
2023-11-02 16:18:57 +01:00
FlatShape ::Variable ( var_id ) | FlatShape ::VarDecl ( var_id ) = > {
2023-11-16 00:35:48 +01:00
return Some ( ( Id ::Variable ( * var_id ) , offset , span ) ) ;
2023-11-02 16:18:57 +01:00
}
FlatShape ::InternalCall ( decl_id ) = > {
2023-11-16 00:35:48 +01:00
return Some ( ( Id ::Declaration ( * decl_id ) , offset , span ) ) ;
2023-11-02 16:18:57 +01:00
}
2023-11-16 00:35:48 +01:00
_ = > return Some ( ( Id ::Value ( shape ) , offset , span ) ) ,
2023-11-02 16:18:57 +01:00
}
}
}
None
}
2023-11-16 00:35:48 +01:00
fn rope < ' a , ' b : ' a > ( & ' b self , file_url : & Url ) -> Option < ( & ' a Rope , & ' a PathBuf ) > {
let file_path = file_url . to_file_path ( ) . ok ( ) ? ;
self . ropes
. get_key_value ( & file_path )
. map ( | ( path , rope ) | ( rope , path ) )
}
2023-11-02 16:18:57 +01:00
fn read_in_file < ' a > (
2023-11-16 00:35:48 +01:00
& mut self ,
2023-11-02 16:18:57 +01:00
engine_state : & ' a mut EngineState ,
2023-11-16 00:35:48 +01:00
file_url : & Url ,
) -> Option < ( & Rope , & PathBuf , StateWorkingSet < ' a > ) > {
let ( file , path ) = self . rope ( file_url ) ? ;
2023-11-02 16:18:57 +01:00
2023-11-16 00:35:48 +01:00
// TODO: AsPath thingy
engine_state . start_in_file ( Some ( & path . to_string_lossy ( ) ) ) ;
2023-11-02 16:18:57 +01:00
let working_set = StateWorkingSet ::new ( engine_state ) ;
2023-11-16 00:35:48 +01:00
Some ( ( file , path , working_set ) )
2023-11-02 16:18:57 +01:00
}
fn goto_definition (
2023-11-16 00:35:48 +01:00
& mut self ,
2023-11-02 16:18:57 +01:00
engine_state : & mut EngineState ,
params : & GotoDefinitionParams ,
) -> Option < GotoDefinitionResponse > {
let cwd = std ::env ::current_dir ( ) . expect ( " Could not get current working directory. " ) ;
engine_state . add_env_var ( " PWD " . into ( ) , Value ::test_string ( cwd . to_string_lossy ( ) ) ) ;
2023-11-16 00:35:48 +01:00
let ( file , path , mut working_set ) = self . read_in_file (
engine_state ,
& params . text_document_position_params . text_document . uri ,
) ? ;
2023-11-02 16:18:57 +01:00
let ( id , _ , _ ) = Self ::find_id (
& mut working_set ,
2023-11-16 00:35:48 +01:00
path ,
file ,
Self ::lsp_position_to_location ( & params . text_document_position_params . position , file ) ,
2023-11-02 16:18:57 +01:00
) ? ;
match id {
Id ::Declaration ( decl_id ) = > {
if let Some ( block_id ) = working_set . get_decl ( decl_id ) . get_block_id ( ) {
let block = working_set . get_block ( block_id ) ;
if let Some ( span ) = & block . span {
for ( file_path , file_start , file_end ) in working_set . files ( ) {
if span . start > = * file_start & & span . start < * file_end {
return Some ( GotoDefinitionResponse ::Scalar ( Location {
uri : Url ::from_file_path ( file_path ) . ok ( ) ? ,
2023-11-16 00:35:48 +01:00
range : Self ::span_to_range ( span , file , * file_start ) ,
2023-11-02 16:18:57 +01:00
} ) ) ;
}
}
}
}
}
Id ::Variable ( var_id ) = > {
let var = working_set . get_variable ( var_id ) ;
for ( _ , file_start , file_end ) in working_set . files ( ) {
if var . declaration_span . start > = * file_start
& & var . declaration_span . start < * file_end
{
return Some ( GotoDefinitionResponse ::Scalar ( Location {
uri : params
. text_document_position_params
. text_document
. uri
. clone ( ) ,
2023-11-16 00:35:48 +01:00
range : Self ::span_to_range ( & var . declaration_span , file , * file_start ) ,
2023-11-02 16:18:57 +01:00
} ) ) ;
}
}
}
_ = > { }
}
None
}
2023-11-16 00:35:48 +01:00
fn hover ( & mut self , engine_state : & mut EngineState , params : & HoverParams ) -> Option < Hover > {
2023-11-02 16:18:57 +01:00
let cwd = std ::env ::current_dir ( ) . expect ( " Could not get current working directory. " ) ;
engine_state . add_env_var ( " PWD " . into ( ) , Value ::test_string ( cwd . to_string_lossy ( ) ) ) ;
2023-11-16 00:35:48 +01:00
let ( file , path , mut working_set ) = self . read_in_file (
engine_state ,
& params . text_document_position_params . text_document . uri ,
) ? ;
2023-11-02 16:18:57 +01:00
let ( id , _ , _ ) = Self ::find_id (
& mut working_set ,
2023-11-16 00:35:48 +01:00
path ,
file ,
Self ::lsp_position_to_location ( & params . text_document_position_params . position , file ) ,
2023-11-02 16:18:57 +01:00
) ? ;
match id {
Id ::Variable ( var_id ) = > {
let var = working_set . get_variable ( var_id ) ;
let contents = format! ( " {} {} " , if var . mutable { " mutable " } else { " " } , var . ty ) ;
Some ( Hover {
contents : HoverContents ::Scalar ( lsp_types ::MarkedString ::String ( contents ) ) ,
// TODO
range : None ,
} )
}
Id ::Declaration ( decl_id ) = > {
let decl = working_set . get_decl ( decl_id ) ;
let mut description = " ``` \n ### Signature \n ``` \n " . to_string ( ) ;
let signature = decl . signature ( ) ;
description . push_str ( & format! ( " {} " , signature . name ) ) ;
if ! signature . named . is_empty ( ) {
description . push_str ( " {flags} " )
}
for required_arg in & signature . required_positional {
description . push_str ( & format! ( " < {} > " , required_arg . name ) ) ;
}
for optional_arg in & signature . optional_positional {
description . push_str ( & format! ( " < {} ?> " , optional_arg . name ) ) ;
}
if let Some ( arg ) = & signature . rest_positional {
description . push_str ( & format! ( " <... {} > " , arg . name ) ) ;
}
description . push_str ( " \n ``` \n " ) ;
if ! signature . required_positional . is_empty ( )
| | ! signature . optional_positional . is_empty ( )
| | signature . rest_positional . is_some ( )
{
description . push_str ( " \n ### Parameters \n \n " ) ;
let mut first = true ;
for required_arg in & signature . required_positional {
if ! first {
description . push_str ( " \\ \n " ) ;
} else {
first = false ;
}
description . push_str ( & format! (
" `{}: {}` " ,
required_arg . name ,
required_arg . shape . to_type ( )
) ) ;
if ! required_arg . desc . is_empty ( ) {
description . push_str ( & format! ( " - {} " , required_arg . desc ) ) ;
}
description . push ( '\n' ) ;
}
for optional_arg in & signature . optional_positional {
if ! first {
description . push_str ( " \\ \n " ) ;
} else {
first = false ;
}
description . push_str ( & format! (
" `{}: {}` " ,
optional_arg . name ,
optional_arg . shape . to_type ( )
) ) ;
if ! optional_arg . desc . is_empty ( ) {
description . push_str ( & format! ( " - {} " , optional_arg . desc ) ) ;
}
description . push ( '\n' ) ;
}
if let Some ( arg ) = & signature . rest_positional {
if ! first {
description . push_str ( " \\ \n " ) ;
}
description . push_str ( & format! (
" `...{}: {}` " ,
arg . name ,
arg . shape . to_type ( )
) ) ;
if ! arg . desc . is_empty ( ) {
description . push_str ( & format! ( " - {} " , arg . desc ) ) ;
}
description . push ( '\n' ) ;
}
description . push ( '\n' ) ;
}
if ! signature . named . is_empty ( ) {
description . push_str ( " \n ### Flags \n \n " ) ;
let mut first = true ;
for named in & signature . named {
if ! first {
description . push_str ( " \\ \n " ) ;
} else {
first = false ;
}
description . push_str ( " " ) ;
if let Some ( short_flag ) = & named . short {
description . push_str ( & format! ( " `- {} ` " , short_flag ) ) ;
}
if ! named . long . is_empty ( ) {
if named . short . is_some ( ) {
description . push_str ( " , " )
}
description . push_str ( & format! ( " `-- {} ` " , named . long ) ) ;
}
if let Some ( arg ) = & named . arg {
description . push_str ( & format! ( " `< {} >` " , arg . to_type ( ) ) )
}
if ! named . desc . is_empty ( ) {
description . push_str ( & format! ( " - {} " , named . desc ) ) ;
}
}
description . push ( '\n' ) ;
}
if ! signature . input_output_types . is_empty ( ) {
description . push_str ( " \n ### Input/output \n " ) ;
description . push_str ( " \n ``` \n " ) ;
for input_output in & signature . input_output_types {
description
. push_str ( & format! ( " {} | {} \n " , input_output . 0 , input_output . 1 ) ) ;
}
description . push_str ( " \n ``` \n " ) ;
}
description . push_str ( & format! (
" ### Usage \n {} \n " ,
decl . usage ( ) . replace ( '\r' , " " )
) ) ;
if ! decl . extra_usage ( ) . is_empty ( ) {
description
. push_str ( & format! ( " \n ### Extra usage: \n {} \n " , decl . extra_usage ( ) ) ) ;
}
if ! decl . examples ( ) . is_empty ( ) {
description . push_str ( " ### Example(s) \n ``` \n " ) ;
for example in decl . examples ( ) {
description . push_str ( & format! (
" ``` \n {} \n ``` \n {} \n \n " ,
example . description , example . example
) ) ;
}
}
Some ( Hover {
contents : HoverContents ::Markup ( MarkupContent {
kind : MarkupKind ::Markdown ,
value : description ,
} ) ,
// TODO
range : None ,
} )
}
Id ::Value ( shape ) = > {
let hover = String ::from ( match shape {
FlatShape ::And = > " and " ,
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 ;
}
} ) ;
Some ( Hover {
contents : HoverContents ::Scalar ( lsp_types ::MarkedString ::String ( hover ) ) ,
// TODO
range : None ,
} )
}
}
}
fn complete (
2023-11-16 00:35:48 +01:00
& mut self ,
2023-11-02 16:18:57 +01:00
engine_state : & mut EngineState ,
params : & CompletionParams ,
) -> Option < CompletionResponse > {
let cwd = std ::env ::current_dir ( ) . expect ( " Could not get current working directory. " ) ;
engine_state . add_env_var ( " PWD " . into ( ) , Value ::test_string ( cwd . to_string_lossy ( ) ) ) ;
2023-11-16 00:35:48 +01:00
let ( rope_of_file , _ , _ ) = self . read_in_file (
engine_state ,
& params . text_document_position . text_document . uri ,
) ? ;
2023-11-02 16:18:57 +01:00
let stack = Stack ::new ( ) ;
let mut completer = NuCompleter ::new ( Arc ::new ( engine_state . clone ( ) ) , stack ) ;
let location =
2023-11-16 00:35:48 +01:00
Self ::lsp_position_to_location ( & params . text_document_position . position , rope_of_file ) ;
2023-11-02 16:18:57 +01:00
let results = completer . complete ( & rope_of_file . to_string ( ) , location ) ;
if results . is_empty ( ) {
None
} else {
Some ( CompletionResponse ::Array (
results
. into_iter ( )
. map ( | r | {
let mut start = params . text_document_position . position ;
start . character - = ( r . span . end - r . span . start ) as u32 ;
CompletionItem {
label : r . value . clone ( ) ,
detail : r . description ,
text_edit : Some ( CompletionTextEdit ::Edit ( TextEdit {
range : Range {
start ,
end : params . text_document_position . position ,
} ,
new_text : r . value ,
} ) ) ,
.. Default ::default ( )
}
} )
. collect ( ) ,
) )
}
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use assert_json_diff ::assert_json_eq ;
use lsp_types ::{
2023-11-16 00:35:48 +01:00
notification ::{
DidChangeTextDocument , DidOpenTextDocument , Exit , Initialized , Notification ,
} ,
2023-11-02 16:18:57 +01:00
request ::{ Completion , GotoDefinition , HoverRequest , Initialize , Request , Shutdown } ,
2023-11-16 00:35:48 +01:00
CompletionParams , DidChangeTextDocumentParams , DidOpenTextDocumentParams ,
GotoDefinitionParams , InitializeParams , InitializedParams , TextDocumentContentChangeEvent ,
TextDocumentIdentifier , TextDocumentItem , TextDocumentPositionParams , Url ,
2023-11-02 16:18:57 +01:00
} ;
use nu_test_support ::fs ::{ fixtures , root } ;
use std ::sync ::mpsc ::Receiver ;
2023-11-16 00:35:48 +01:00
pub fn initialize_language_server ( ) -> ( Connection , Receiver < Result < ( ) > > ) {
2023-11-02 16:18:57 +01:00
use std ::sync ::mpsc ;
let ( client_connection , server_connection ) = Connection ::memory ( ) ;
let lsp_server = LanguageServer ::initialize_connection ( server_connection , None ) . unwrap ( ) ;
let ( send , recv ) = mpsc ::channel ( ) ;
std ::thread ::spawn ( move | | {
let engine_state = nu_cmd_lang ::create_default_context ( ) ;
let engine_state = nu_command ::add_shell_command_context ( engine_state ) ;
2023-11-16 00:35:48 +01:00
send . send ( lsp_server . serve_requests ( engine_state , Arc ::new ( AtomicBool ::new ( false ) ) ) )
2023-11-02 16:18:57 +01:00
} ) ;
client_connection
. sender
. send ( Message ::Request ( lsp_server ::Request {
id : 1. into ( ) ,
method : Initialize ::METHOD . to_string ( ) ,
params : serde_json ::to_value ( InitializeParams {
.. Default ::default ( )
} )
. unwrap ( ) ,
} ) )
. unwrap ( ) ;
client_connection
. sender
. send ( Message ::Notification ( lsp_server ::Notification {
method : Initialized ::METHOD . to_string ( ) ,
params : serde_json ::to_value ( InitializedParams { } ) . unwrap ( ) ,
} ) )
. unwrap ( ) ;
let _initialize_response = client_connection
. receiver
. recv_timeout ( std ::time ::Duration ::from_secs ( 2 ) )
. unwrap ( ) ;
( client_connection , recv )
}
#[ test ]
fn shutdown_on_request ( ) {
let ( client_connection , recv ) = initialize_language_server ( ) ;
client_connection
. sender
. send ( Message ::Request ( lsp_server ::Request {
id : 2. into ( ) ,
method : Shutdown ::METHOD . to_string ( ) ,
params : serde_json ::Value ::Null ,
} ) )
. unwrap ( ) ;
client_connection
. sender
. send ( Message ::Notification ( lsp_server ::Notification {
method : Exit ::METHOD . to_string ( ) ,
params : serde_json ::Value ::Null ,
} ) )
. unwrap ( ) ;
assert! ( recv
. recv_timeout ( std ::time ::Duration ::from_secs ( 2 ) )
. unwrap ( )
. 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 : Url ::from_file_path ( none_existent_path ) . unwrap ( ) ,
} ,
position : lsp_types ::Position {
line : 0 ,
character : 0 ,
} ,
} ,
work_done_progress_params : Default ::default ( ) ,
partial_result_params : Default ::default ( ) ,
} )
. unwrap ( ) ,
} ) )
. unwrap ( ) ;
let resp = client_connection
. receiver
. recv_timeout ( std ::time ::Duration ::from_secs ( 2 ) )
. unwrap ( ) ;
2023-11-16 00:35:48 +01:00
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
2023-11-02 16:18:57 +01:00
2023-11-16 00:35:48 +01:00
assert_json_eq! ( result , serde_json ::json! ( null ) ) ;
2023-11-02 16:18:57 +01:00
}
2023-11-16 00:35:48 +01:00
pub fn open ( client_connection : & Connection , uri : Url ) -> lsp_server ::Notification {
let text = std ::fs ::read_to_string ( uri . to_file_path ( ) . unwrap ( ) ) . unwrap ( ) ;
2023-11-02 16:18:57 +01:00
2023-11-16 00:35:48 +01:00
client_connection
. sender
. send ( Message ::Notification ( lsp_server ::Notification {
method : DidOpenTextDocument ::METHOD . to_string ( ) ,
params : serde_json ::to_value ( DidOpenTextDocumentParams {
text_document : TextDocumentItem {
uri ,
language_id : String ::from ( " nu " ) ,
version : 1 ,
text ,
} ,
} )
. unwrap ( ) ,
} ) )
. unwrap ( ) ;
let notification = client_connection
. receiver
. recv_timeout ( Duration ::from_secs ( 2 ) )
. unwrap ( ) ;
if let Message ::Notification ( n ) = notification {
n
} else {
panic! ( ) ;
}
}
pub fn update (
client_connection : & Connection ,
uri : Url ,
text : String ,
range : Option < Range > ,
) -> lsp_server ::Notification {
client_connection
. sender
. send ( lsp_server ::Message ::Notification (
lsp_server ::Notification {
method : DidChangeTextDocument ::METHOD . to_string ( ) ,
params : serde_json ::to_value ( DidChangeTextDocumentParams {
text_document : lsp_types ::VersionedTextDocumentIdentifier {
uri ,
version : 2 ,
} ,
content_changes : vec ! [ TextDocumentContentChangeEvent {
range ,
range_length : None ,
text ,
} ] ,
} )
. unwrap ( ) ,
} ,
) )
. unwrap ( ) ;
let notification = client_connection
. receiver
. recv_timeout ( Duration ::from_secs ( 2 ) )
. unwrap ( ) ;
if let Message ::Notification ( n ) = notification {
n
} else {
panic! ( ) ;
}
}
fn goto_definition (
client_connection : & Connection ,
uri : Url ,
line : u32 ,
character : u32 ,
) -> Message {
2023-11-02 16:18:57 +01:00
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 : lsp_types ::Position { line , character } ,
} ,
work_done_progress_params : Default ::default ( ) ,
partial_result_params : Default ::default ( ) ,
} )
. unwrap ( ) ,
} ) )
. unwrap ( ) ;
client_connection
. receiver
. recv_timeout ( std ::time ::Duration ::from_secs ( 2 ) )
. unwrap ( )
}
#[ test ]
fn goto_definition_of_variable ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " goto " ) ;
script . push ( " var.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = goto_definition ( & client_connection , script . clone ( ) , 2 , 12 ) ;
2023-11-02 16:18:57 +01:00
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 ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " goto " ) ;
script . push ( " command.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = goto_definition ( & client_connection , script . clone ( ) , 4 , 1 ) ;
2023-11-02 16:18:57 +01:00
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_parameter ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " goto " ) ;
script . push ( " command.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = goto_definition ( & client_connection , script . clone ( ) , 1 , 14 ) ;
2023-11-02 16:18:57 +01:00
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 }
}
} )
) ;
}
2023-11-16 00:35:48 +01:00
pub fn hover ( client_connection : & Connection , uri : Url , line : u32 , character : u32 ) -> Message {
2023-11-02 16:18:57 +01:00
client_connection
. sender
. send ( Message ::Request ( lsp_server ::Request {
id : 2. into ( ) ,
method : HoverRequest ::METHOD . to_string ( ) ,
params : serde_json ::to_value ( HoverParams {
text_document_position_params : TextDocumentPositionParams {
text_document : TextDocumentIdentifier { uri } ,
position : lsp_types ::Position { line , character } ,
} ,
work_done_progress_params : Default ::default ( ) ,
} )
. unwrap ( ) ,
} ) )
. unwrap ( ) ;
client_connection
. receiver
. recv_timeout ( std ::time ::Duration ::from_secs ( 2 ) )
. unwrap ( )
}
#[ test ]
fn hover_on_variable ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " hover " ) ;
script . push ( " var.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = hover ( & client_connection , script . clone ( ) , 2 , 0 ) ;
2023-11-02 16:18:57 +01:00
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_eq! (
result ,
serde_json ::json! ( {
" contents " : " table "
} )
) ;
}
#[ test ]
fn hover_on_command ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " hover " ) ;
script . push ( " command.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = hover ( & client_connection , script . clone ( ) , 3 , 0 ) ;
2023-11-02 16:18:57 +01:00
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_eq! (
result ,
serde_json ::json! ( {
" contents " : {
" kind " : " markdown " ,
" value " : " ``` \n ### Signature \n ``` \n hello {flags} \n ``` \n \n ### Flags \n \n `-h`, `--help` - Display the help message for this command \n ### Usage \n Renders some greeting message \n "
}
} )
) ;
}
2023-11-16 00:35:48 +01:00
fn complete ( client_connection : & Connection , uri : Url , line : u32 , character : u32 ) -> Message {
2023-11-02 16:18:57 +01:00
client_connection
. sender
. send ( Message ::Request ( lsp_server ::Request {
id : 2. into ( ) ,
method : Completion ::METHOD . to_string ( ) ,
params : serde_json ::to_value ( CompletionParams {
text_document_position : TextDocumentPositionParams {
text_document : TextDocumentIdentifier { uri } ,
position : lsp_types ::Position { line , character } ,
} ,
work_done_progress_params : Default ::default ( ) ,
partial_result_params : Default ::default ( ) ,
context : None ,
} )
. unwrap ( ) ,
} ) )
. unwrap ( ) ;
client_connection
. receiver
. recv_timeout ( std ::time ::Duration ::from_secs ( 2 ) )
. unwrap ( )
}
#[ test ]
fn complete_on_variable ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " completion " ) ;
script . push ( " var.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = complete ( & client_connection , script , 2 , 9 ) ;
2023-11-02 16:18:57 +01:00
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_eq! (
result ,
serde_json ::json! ( [
{
" label " : " $greeting " ,
" textEdit " : {
" newText " : " $greeting " ,
" range " : {
" start " : { " character " : 5 , " line " : 2 } ,
" end " : { " character " : 9 , " line " : 2 }
}
}
}
] )
) ;
}
#[ test ]
fn complete_command_with_space ( ) {
2023-11-16 00:35:48 +01:00
let ( client_connection , _recv ) = initialize_language_server ( ) ;
2023-11-02 16:18:57 +01:00
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " completion " ) ;
script . push ( " command.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
2023-11-16 00:35:48 +01:00
open ( & client_connection , script . clone ( ) ) ;
let resp = complete ( & client_connection , script , 0 , 8 ) ;
2023-11-02 16:18:57 +01:00
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_eq! (
result ,
serde_json ::json! ( [
{
" label " : " config nu " ,
" detail " : " Edit nu configurations. " ,
" textEdit " : {
" range " : {
" start " : { " line " : 0 , " character " : 0 } ,
" end " : { " line " : 0 , " character " : 8 } ,
} ,
" newText " : " config nu "
}
}
] )
) ;
}
}