2024-07-14 10:10:41 +02:00
#![ doc = include_str!( " ../README.md " ) ]
2023-11-02 16:18:57 +01:00
use lsp_server ::{ Connection , IoThreads , Message , Response , ResponseError } ;
use lsp_types ::{
request ::{ Completion , GotoDefinition , HoverRequest , Request } ,
2024-03-25 02:14:12 +01:00
CompletionItem , CompletionItemKind , CompletionParams , CompletionResponse , CompletionTextEdit ,
GotoDefinitionParams , GotoDefinitionResponse , Hover , HoverContents , HoverParams , Location ,
MarkupContent , MarkupKind , OneOf , Range , ServerCapabilities , TextDocumentSyncKind , TextEdit ,
Url ,
2023-11-02 16:18:57 +01:00
} ;
use miette ::{ IntoDiagnostic , Result } ;
2024-03-25 02:14:12 +01:00
use nu_cli ::{ NuCompleter , SuggestionKind } ;
2023-11-02 16:18:57 +01:00
use nu_parser ::{ flatten_block , parse , FlatShape } ;
use nu_protocol ::{
engine ::{ EngineState , Stack , StateWorkingSet } ,
DeclId , Span , Value , VarId ,
} ;
use ropey ::Rope ;
Add `command_prelude` module (#12291)
# Description
When implementing a `Command`, one must also import all the types
present in the function signatures for `Command`. This makes it so that
we often import the same set of types in each command implementation
file. E.g., something like this:
```rust
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
ShellError, Signature, Span, Type, Value,
};
```
This PR adds the `nu_engine::command_prelude` module which contains the
necessary and commonly used types to implement a `Command`:
```rust
// command_prelude.rs
pub use crate::CallExt;
pub use nu_protocol::{
ast::{Call, CellPath},
engine::{Command, EngineState, Stack},
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned,
PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
};
```
This should reduce the boilerplate needed to implement a command and
also gives us a place to track the breadth of the `Command` API. I tried
to be conservative with what went into the prelude modules, since it
might be hard/annoying to remove items from the prelude in the future.
Let me know if something should be included or excluded.
2024-03-26 22:17:30 +01:00
use std ::{
collections ::BTreeMap ,
path ::{ Path , PathBuf } ,
2024-07-08 00:29:01 +02:00
sync ::Arc ,
Add `command_prelude` module (#12291)
# Description
When implementing a `Command`, one must also import all the types
present in the function signatures for `Command`. This makes it so that
we often import the same set of types in each command implementation
file. E.g., something like this:
```rust
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
ShellError, Signature, Span, Type, Value,
};
```
This PR adds the `nu_engine::command_prelude` module which contains the
necessary and commonly used types to implement a `Command`:
```rust
// command_prelude.rs
pub use crate::CallExt;
pub use nu_protocol::{
ast::{Call, CellPath},
engine::{Command, EngineState, Stack},
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned,
PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
};
```
This should reduce the boilerplate needed to implement a command and
also gives us a place to track the breadth of the `Command` API. I tried
to be conservative with what went into the prelude modules, since it
might be hard/annoying to remove items from the prelude in the future.
Let me know if something should be included or excluded.
2024-03-26 22:17:30 +01:00
time ::Duration ,
} ;
2023-11-02 16:18:57 +01:00
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
} )
}
2024-07-08 00:29:01 +02:00
pub fn serve_requests ( mut self , engine_state : EngineState ) -> Result < ( ) > {
2023-11-17 16:15:55 +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
2024-07-08 00:29:01 +02:00
. initialize_while ( server_capabilities , | | {
! engine_state . signals ( ) . interrupted ( )
} )
2023-11-02 16:18:57 +01:00
. into_diagnostic ( ) ? ;
2024-07-08 00:29:01 +02:00
while ! engine_state . signals ( ) . interrupted ( ) {
2023-11-16 00:35:48 +01:00
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 {
2024-01-11 22:24:49 +01:00
let line = rope_of_file . byte_to_line ( span . start - offset ) ;
2023-11-02 16:18:57 +01:00
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 ,
} ;
2024-01-11 22:24:49 +01:00
let line = rope_of_file . byte_to_line ( span . end - offset ) ;
2023-11-02 16:18:57 +01:00
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
2024-04-19 08:38:08 +02:00
engine_state . file = Some ( path . to_owned ( ) ) ;
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 ) = > {
2024-05-19 01:37:31 +02:00
if let Some ( block_id ) = working_set . get_decl ( decl_id ) . block_id ( ) {
2023-11-02 16:18:57 +01:00
let block = working_set . get_block ( block_id ) ;
if let Some ( span ) = & block . span {
2024-03-20 19:43:50 +01:00
for cached_file in working_set . files ( ) {
if cached_file . covered_span . contains ( span . start ) {
2023-11-02 16:18:57 +01:00
return Some ( GotoDefinitionResponse ::Scalar ( Location {
2024-03-20 19:43:50 +01:00
uri : Url ::from_file_path ( & * cached_file . name ) . ok ( ) ? ,
range : Self ::span_to_range (
span ,
file ,
cached_file . covered_span . start ,
) ,
2023-11-02 16:18:57 +01:00
} ) ) ;
}
}
}
}
}
Id ::Variable ( var_id ) = > {
let var = working_set . get_variable ( var_id ) ;
2024-03-20 19:43:50 +01:00
for cached_file in working_set . files ( ) {
if cached_file
. covered_span
. contains ( var . declaration_span . start )
2023-11-02 16:18:57 +01:00
{
return Some ( GotoDefinitionResponse ::Scalar ( Location {
uri : params
. text_document_position_params
. text_document
. uri
. clone ( ) ,
2024-03-20 19:43:50 +01:00
range : Self ::span_to_range (
& var . declaration_span ,
file ,
cached_file . covered_span . start ,
) ,
2023-11-02 16:18:57 +01:00
} ) ) ;
}
}
}
2023-12-15 18:39:19 +01:00
Id ::Value ( _ ) = > { }
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 ) ;
2023-12-15 18:39:19 +01:00
let mut description = String ::new ( ) ;
// First description
2024-08-22 12:02:08 +02:00
description . push_str ( & format! ( " {} \n " , decl . description ( ) . replace ( '\r' , " " ) ) ) ;
2023-12-15 18:39:19 +01:00
// Additional description
2024-08-22 12:02:08 +02:00
if ! decl . extra_description ( ) . is_empty ( ) {
description . push_str ( & format! ( " \n {} \n " , decl . extra_description ( ) ) ) ;
2023-12-15 18:39:19 +01:00
}
// Usage
2024-04-20 14:16:38 +02:00
description . push_str ( " ### Usage \n ```nu \n " ) ;
2023-11-02 16:18:57 +01:00
let signature = decl . signature ( ) ;
description . push_str ( & format! ( " {} " , signature . name ) ) ;
if ! signature . named . is_empty ( ) {
2023-12-15 18:39:19 +01:00
description . push_str ( " {flags} " ) ;
2023-11-02 16:18:57 +01:00
}
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 " ) ;
2023-12-15 18:39:19 +01:00
// Flags
if ! signature . named . is_empty ( ) {
description . push_str ( " \n ### Flags \n \n " ) ;
let mut first = true ;
for named in & signature . named {
if first {
first = false ;
} else {
description . push ( '\n' ) ;
}
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' ) ;
}
description . push ( '\n' ) ;
}
// Parameters
2023-11-02 16:18:57 +01:00
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 {
2023-12-15 18:39:19 +01:00
if first {
2023-11-02 16:18:57 +01:00
first = false ;
2023-12-15 18:39:19 +01:00
} else {
description . push ( '\n' ) ;
2023-11-02 16:18:57 +01:00
}
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 {
2023-12-15 18:39:19 +01:00
if first {
2023-11-02 16:18:57 +01:00
first = false ;
2023-12-15 18:39:19 +01:00
} else {
description . push ( '\n' ) ;
2023-11-02 16:18:57 +01:00
}
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 {
2023-12-08 19:30:13 +01:00
description . push ( '\n' ) ;
2023-11-02 16:18:57 +01:00
}
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' ) ;
}
2023-12-15 18:39:19 +01:00
// Input/output types
2023-11-02 16:18:57 +01:00
if ! signature . input_output_types . is_empty ( ) {
2023-12-15 18:39:19 +01:00
description . push_str ( " \n ### Input/output types \n " ) ;
2024-04-20 14:16:38 +02:00
description . push_str ( " \n ```nu \n " ) ;
2023-11-02 16:18:57 +01:00
for input_output in & signature . input_output_types {
description
2023-12-08 19:30:13 +01:00
. push_str ( & format! ( " {} | {} \n " , input_output . 0 , input_output . 1 ) ) ;
2023-11-02 16:18:57 +01:00
}
description . push_str ( " \n ``` \n " ) ;
}
2023-12-15 18:39:19 +01:00
// Examples
2023-11-02 16:18:57 +01:00
if ! decl . examples ( ) . is_empty ( ) {
2023-12-08 19:30:13 +01:00
description . push_str ( " ### Example(s) \n " ) ;
2023-11-02 16:18:57 +01:00
for example in decl . examples ( ) {
description . push_str ( & format! (
2024-04-20 14:16:38 +02:00
" {} \n ```nu \n {} \n ``` \n " ,
2023-11-02 16:18:57 +01:00
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
2024-05-09 07:38:24 +02:00
let mut completer =
NuCompleter ::new ( Arc ::new ( engine_state . clone ( ) ) , Arc ::new ( Stack ::new ( ) ) ) ;
2023-11-02 16:18:57 +01:00
let location =
2023-11-16 00:35:48 +01:00
Self ::lsp_position_to_location ( & params . text_document_position . position , rope_of_file ) ;
2024-03-25 02:14:12 +01:00
let results =
completer . fetch_completions_at ( & rope_of_file . to_string ( ) [ .. location ] , location ) ;
2023-11-02 16:18:57 +01:00
if results . is_empty ( ) {
None
} else {
Some ( CompletionResponse ::Array (
results
. into_iter ( )
. map ( | r | {
let mut start = params . text_document_position . position ;
2024-03-25 02:14:12 +01:00
start . character - = ( r . suggestion . span . end - r . suggestion . span . start ) as u32 ;
2023-11-02 16:18:57 +01:00
CompletionItem {
2024-03-25 02:14:12 +01:00
label : r . suggestion . value . clone ( ) ,
detail : r . suggestion . description ,
kind : Self ::lsp_completion_item_kind ( r . kind ) ,
2023-11-02 16:18:57 +01:00
text_edit : Some ( CompletionTextEdit ::Edit ( TextEdit {
range : Range {
start ,
end : params . text_document_position . position ,
} ,
2024-03-25 02:14:12 +01:00
new_text : r . suggestion . value ,
2023-11-02 16:18:57 +01:00
} ) ) ,
.. Default ::default ( )
}
} )
. collect ( ) ,
) )
}
}
2024-03-25 02:14:12 +01:00
fn lsp_completion_item_kind (
suggestion_kind : Option < SuggestionKind > ,
) -> Option < CompletionItemKind > {
suggestion_kind . and_then ( | suggestion_kind | match suggestion_kind {
SuggestionKind ::Type ( t ) = > match t {
nu_protocol ::Type ::String = > Some ( CompletionItemKind ::VARIABLE ) ,
_ = > None ,
} ,
SuggestionKind ::Command ( c ) = > match c {
nu_protocol ::engine ::CommandType ::Keyword = > Some ( CompletionItemKind ::KEYWORD ) ,
nu_protocol ::engine ::CommandType ::Builtin = > Some ( CompletionItemKind ::FUNCTION ) ,
_ = > None ,
} ,
} )
}
2023-11-02 16:18:57 +01:00
}
#[ cfg(test) ]
mod tests {
use super ::* ;
2024-03-25 02:14:12 +01:00
use assert_json_diff ::{ assert_json_eq , assert_json_include } ;
2023-11-02 16:18:57 +01:00
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 ,
2023-12-15 18:39:19 +01:00
GotoDefinitionParams , InitializeParams , InitializedParams , PartialResultParams ,
TextDocumentContentChangeEvent , TextDocumentIdentifier , TextDocumentItem ,
TextDocumentPositionParams , Url , WorkDoneProgressParams ,
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 ) ;
2024-07-08 00:29:01 +02:00
send . send ( lsp_server . serve_requests ( engine_state ) )
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 ,
} ,
} ,
2023-12-15 18:39:19 +01:00
work_done_progress_params : WorkDoneProgressParams ::default ( ) ,
partial_result_params : PartialResultParams ::default ( ) ,
2023-11-02 16:18:57 +01:00
} )
. 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
}
2024-01-11 22:24:49 +01:00
pub fn open_unchecked ( client_connection : & Connection , uri : Url ) -> lsp_server ::Notification {
open ( client_connection , uri ) . unwrap ( )
}
pub fn open (
client_connection : & Connection ,
uri : Url ,
) -> Result < lsp_server ::Notification , String > {
let text =
std ::fs ::read_to_string ( uri . to_file_path ( ) . unwrap ( ) ) . map_err ( | e | e . to_string ( ) ) ? ;
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 ( ) ,
} ) )
2024-01-11 22:24:49 +01:00
. map_err ( | e | e . to_string ( ) ) ? ;
2023-11-16 00:35:48 +01:00
let notification = client_connection
. receiver
. recv_timeout ( Duration ::from_secs ( 2 ) )
2024-01-11 22:24:49 +01:00
. map_err ( | e | e . to_string ( ) ) ? ;
2023-11-16 00:35:48 +01:00
if let Message ::Notification ( n ) = notification {
2024-01-11 22:24:49 +01:00
Ok ( n )
2023-11-16 00:35:48 +01:00
} else {
2024-01-11 22:24:49 +01:00
Err ( String ::from ( " Did not receive a notification from server " ) )
2023-11-16 00:35:48 +01:00
}
}
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 } ,
} ,
2023-12-15 18:39:19 +01:00
work_done_progress_params : WorkDoneProgressParams ::default ( ) ,
partial_result_params : PartialResultParams ::default ( ) ,
2023-11-02 16:18:57 +01:00
} )
. 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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 } ,
} ,
2023-12-15 18:39:19 +01:00
work_done_progress_params : WorkDoneProgressParams ::default ( ) ,
2023-11-02 16:18:57 +01:00
} )
. 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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 ]
2024-03-24 20:15:01 +01:00
fn hover_on_custom_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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 " ,
2024-04-20 14:16:38 +02:00
" value " : " Renders some greeting message \n ### Usage \n ```nu \n hello {flags} \n ``` \n \n ### Flags \n \n `-h`, `--help` - Display the help message for this command \n \n "
2023-11-02 16:18:57 +01:00
}
} )
) ;
}
2024-03-24 20:15:01 +01:00
#[ test ]
fn hover_on_str_join ( ) {
let ( client_connection , _recv ) = initialize_language_server ( ) ;
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " hover " ) ;
script . push ( " command.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
open_unchecked ( & client_connection , script . clone ( ) ) ;
let resp = hover ( & client_connection , script . clone ( ) , 5 , 8 ) ;
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_eq! (
result ,
serde_json ::json! ( {
" contents " : {
" kind " : " markdown " ,
2024-04-20 14:16:38 +02:00
" value " : " Concatenate multiple strings into a single string, with an optional separator between each. \n ### Usage \n ```nu \n str join {flags} <separator?> \n ``` \n \n ### Flags \n \n `-h`, `--help` - Display the help message for this command \n \n \n ### Parameters \n \n `separator: string` - Optional separator to use when creating string. \n \n \n ### Input/output types \n \n ```nu \n list<any> | string \n string | string \n \n ``` \n ### Example(s) \n Create a string from input \n ```nu \n ['nu', 'shell'] | str join \n ``` \n Create a string from input with a separator \n ```nu \n ['nu', 'shell'] | str join '-' \n ``` \n "
2024-03-24 20:15:01 +01:00
}
} )
) ;
}
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 } ,
} ,
2023-12-15 18:39:19 +01:00
work_done_progress_params : WorkDoneProgressParams ::default ( ) ,
partial_result_params : PartialResultParams ::default ( ) ,
2023-11-02 16:18:57 +01:00
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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 }
}
2024-03-25 02:14:12 +01:00
} ,
" kind " : 6
2023-11-02 16:18:57 +01:00
}
] )
) ;
}
#[ 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 ( ) ;
2024-01-11 22:24:49 +01:00
open_unchecked ( & client_connection , script . clone ( ) ) ;
2023-11-16 00:35:48 +01:00
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 "
2024-03-25 02:14:12 +01:00
} ,
" kind " : 3
2023-11-02 16:18:57 +01:00
}
] )
) ;
}
2024-01-11 22:24:49 +01:00
#[ test ]
fn complete_command_with_utf_line ( ) {
let ( client_connection , _recv ) = initialize_language_server ( ) ;
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " completion " ) ;
script . push ( " utf_pipeline.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
open_unchecked ( & client_connection , script . clone ( ) ) ;
let resp = complete ( & client_connection , script , 0 , 14 ) ;
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_eq! (
result ,
serde_json ::json! ( [
{
" label " : " str trim " ,
" detail " : " Trim whitespace or specific character. " ,
" textEdit " : {
" range " : {
" start " : { " line " : 0 , " character " : 9 } ,
" end " : { " line " : 0 , " character " : 14 } ,
} ,
" newText " : " str trim "
2024-03-25 02:14:12 +01:00
} ,
" kind " : 3
}
] )
) ;
}
#[ test ]
fn complete_keyword ( ) {
let ( client_connection , _recv ) = initialize_language_server ( ) ;
let mut script = fixtures ( ) ;
script . push ( " lsp " ) ;
script . push ( " completion " ) ;
script . push ( " keyword.nu " ) ;
let script = Url ::from_file_path ( script ) . unwrap ( ) ;
open_unchecked ( & client_connection , script . clone ( ) ) ;
let resp = complete ( & client_connection , script , 0 , 2 ) ;
let result = if let Message ::Response ( response ) = resp {
response . result
} else {
panic! ( )
} ;
assert_json_include! (
actual : result ,
expected : serde_json ::json! ( [
2024-08-06 02:30:10 +02:00
{
" label " : " overlay " ,
" textEdit " : {
" newText " : " overlay " ,
" range " : {
" start " : { " character " : 0 , " line " : 0 } ,
" end " : { " character " : 2 , " line " : 0 }
}
} ,
" kind " : 14
} ,
2024-01-11 22:24:49 +01:00
] )
) ;
}
2023-11-02 16:18:57 +01:00
}