char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c7a9569
main
1// Synchronous LSP client harness for integration tests, replacing the old 2// TypeScript test driver. Tests that need a native TypeScript backend set 3// AVRIL_TEST_TYPESCRIPT to its absolute path and are skipped otherwise. 4use std:: fs; 5use std:: io::{ BufRead , BufReader , Read , Write }; 6use std:: path::{ Path , PathBuf }; 7use std:: process::{ Child , ChildStdin , Command , ExitStatus , Stdio }; 8use std:: sync:: mpsc::{ Receiver , RecvTimeoutError , channel}; 9use std:: sync::{ Arc , Mutex }; 10use std:: time::{ Duration , Instant }; 11 12use serde_json::{ Value , json}; 13 14pub fn backend () ->Option < PathBuf > { 15let path = std:: env:: var_os ( "AVRIL_TEST_TYPESCRIPT" ) ?; 16if path. is_empty () { 17return None ; 18} 19Some ( PathBuf :: from ( path)) 20} 21 22pub fn avril () ->PathBuf { 23PathBuf :: from ( env! ( "CARGO_BIN_EXE_avril" )) 24} 25 26pub fn write_frame ( writer : & mut impl Write , message : & Value ) -> std:: io:: Result <()> { 27let body = serde_json:: to_vec ( message). unwrap (); 28write! ( writer, "Content-Length: {}\r\n\r\n" , body. len ()) ?; 29 writer. write_all ( & body) ?; 30 writer. flush () 31} 32 33pub fn read_frame ( reader : & mut impl BufRead ) -> std:: io:: Result < Option < Value >> { 34let mut length =None ; 35loop { 36let mut line =String :: new (); 37if reader. read_line ( & mut line) ? ==0 { 38return Ok ( None ); 39} 40let line = line. trim_end_matches ([ '\r' , '\n' ]); 41if line. is_empty () { 42break ; 43} 44if let Some (( name, value)) = line. split_once ( ':' ) 45 && name. trim (). eq_ignore_ascii_case ( "Content-Length" ) 46{ 47 length =Some ( value. trim (). parse ::< usize >(). map_err ( |_|{ 48 std:: io:: Error :: new ( std:: io:: ErrorKind :: InvalidData , "Invalid Content-Length" ) 49}) ?); 50} 51} 52let length = length. ok_or_else ( ||{ 53 std:: io:: Error :: new ( std:: io:: ErrorKind :: InvalidData , "Missing Content-Length" ) 54}) ?; 55if length >64 * 1024 * 1024 { 56return Err ( std:: io:: Error :: new ( 57 std:: io:: ErrorKind :: InvalidData , 58"RPC frame exceeds test transport limit" , 59)); 60} 61let mut body =vec! [ 0 ; length]; 62 reader. read_exact ( & mut body) ?; 63Ok ( Some ( serde_json:: from_slice ( & body). unwrap ())) 64} 65 66# [ derive ( Debug )] 67pub struct RpcError { 68pub code : i64 , 69pub message : String , 70} 71 72impl std:: fmt:: Display for RpcError { 73fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ >) -> std:: fmt:: Result { 74write! ( f, "RPC {}: {}" , self . code , self . message ) 75} 76} 77 78pub struct Server { 79child : Child , 80stdin : Option < ChildStdin >, 81messages : Receiver < Value >, 82pub notifications : Vec < Value >, 83next_id : u64 , 84root : PathBuf , 85stderr : Arc < Mutex < Vec < u8 >>>, 86} 87 88impl Server { 89pub fn start ( root : & Path , backend : & Path , options : & [ & str ]) ->Server { 90let mut child =Command :: new ( avril ()) 91. current_dir ( root) 92. args ([ "lsp" , & format! ( "--typescript={}" , backend. display ())]) 93. arg ( format! ( "--project={}" , root. display ())) 94. args ( options) 95. stdin ( Stdio :: piped ()) 96. stdout ( Stdio :: piped ()) 97. stderr ( Stdio :: piped ()) 98. spawn () 99. unwrap (); 100let stdin = child. stdin . take (). unwrap (); 101let mut stdout =BufReader :: new ( child. stdout . take (). unwrap ()); 102let ( tx, messages) =channel (); 103 std:: thread:: spawn ( move ||{ 104while let Ok ( Some ( message)) =read_frame ( & mut stdout) { 105if tx. send ( message). is_err () { 106return ; 107} 108} 109}); 110let stderr =Arc :: new ( Mutex :: new ( Vec :: new ())); 111let mut pipe = child. stderr . take (). unwrap (); 112 std:: thread:: spawn ({ 113let stderr = stderr. clone (); 114move ||{ 115let mut chunk =[ 0 ; 8192 ]; 116while let Ok ( count) = pipe. read ( & mut chunk) { 117if count ==0 { 118break ; 119} 120 stderr. lock (). unwrap (). extend_from_slice ( & chunk[ ..count]); 121} 122} 123}); 124Server { 125 child, 126stdin : Some ( stdin), 127 messages, 128notifications : Vec :: new (), 129next_id : 0 , 130root : root. to_owned (), 131 stderr, 132} 133} 134 135pub fn pid ( & self ) ->u32 { 136self . child . id () 137} 138 139pub fn stderr ( & self ) ->String { 140String :: from_utf8_lossy ( & self . stderr . lock (). unwrap ()). into_owned () 141} 142 143fn send ( & mut self , message : & Value ) { 144write_frame ( self . stdin . as_mut (). unwrap (), message). unwrap (); 145} 146 147pub fn notify ( & mut self , method : & str , params : Value ) { 148self . send ( & json! ({ "jsonrpc" : "2.0" , "method" : method, "params" : params})); 149} 150 151/// Sends a request without waiting, for tests that must act (cancel, 152/// signal, close) while it is in flight. 153pub fn request_start ( & mut self , method : & str , params : Value ) ->u64 { 154self . next_id +=1 ; 155let id =self . next_id ; 156self . send ( & json! ({ "jsonrpc" : "2.0" , "id" : id, "method" : method, "params" : params})); 157 id 158} 159 160pub fn request_wait ( & mut self , id : u64 ) ->Value { 161let deadline =Instant :: now () +Duration :: from_secs ( 15 ); 162loop { 163let remaining = deadline. saturating_duration_since ( Instant :: now ()); 164assert! ( 165 !remaining. is_zero (), 166"Timeout waiting for response {id}\n{}" , 167self . stderr () 168); 169match self . messages . recv_timeout ( remaining) { 170Ok ( message) =>{ 171if message. get ( "method" ). is_some () { 172self . notifications . push ( message); 173} else if message[ "id" ]. as_u64 () ==Some ( id) { 174return message; 175} 176} 177Err ( RecvTimeoutError :: Timeout ) =>{ 178panic! ( "Timeout waiting for response {id}\n{}" , self . stderr ()) 179} 180Err ( RecvTimeoutError :: Disconnected ) =>{ 181panic! ( 182"Unexpected LSP EOF waiting for response {id}\n{}" , 183self . stderr () 184) 185} 186} 187} 188} 189 190/// Sends a request and returns the full response message, so tests can 191/// inspect either `result` or `error`. 192pub fn request ( & mut self , method : & str , params : Value ) ->Value { 193let id =self . request_start ( method, params); 194self . request_wait ( id) 195} 196 197pub fn call ( & mut self , method : & str , params : Value ) ->Result < Value , RpcError > { 198let response =self . request ( method, params); 199if let Some ( error) = response. get ( "error" ) { 200Err ( RpcError { 201code : error[ "code" ]. as_i64 (). unwrap_or_default (), 202message : error[ "message" ]. as_str (). unwrap_or_default (). to_owned (), 203}) 204} else { 205Ok ( response[ "result" ]. clone ()) 206} 207} 208 209pub fn write_raw ( & mut self , bytes : & [ u8 ]) { 210let stdin =self . stdin . as_mut (). unwrap (); 211 stdin. write_all ( bytes). unwrap (); 212 stdin. flush (). unwrap (); 213} 214 215pub fn close_stdin ( & mut self ) { 216self . stdin . take (); 217} 218 219pub fn wait_status ( & mut self , timeout : Duration ) ->ExitStatus { 220let deadline =Instant :: now () + timeout; 221loop { 222if let Some ( status) =self . child . try_wait (). unwrap () { 223return status; 224} 225assert! ( 226Instant :: now () < deadline, 227"Server did not exit\n{}" , 228self . stderr () 229); 230 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 231} 232} 233 234pub fn kill ( & mut self ) { 235let _ =self . child . kill (); 236let _ =self . child . wait (); 237} 238 239pub fn uri ( & self , path : & str ) ->String { 240 url:: Url :: from_file_path ( self . root . join ( path)) 241. unwrap () 242. to_string () 243} 244 245pub fn initialize ( & mut self ) { 246self . initialize_with ( json! ({ "textDocument" : { "definition" : { "linkSupport" : true }}})); 247} 248 249pub fn initialize_with ( & mut self , capabilities : Value ) ->Value { 250self . initialize_params ( json! ({ "capabilities" : capabilities})) 251} 252 253pub fn initialize_params ( & mut self , params : Value ) ->Value { 254let result =self . call ( "initialize" , params). unwrap (); 255self . notify ( "initialized" , json! ({})); 256 result 257} 258 259pub fn open ( & mut self , path : & str , text : & str ) { 260self . open_as ( path, text, 1 , "typescript" ); 261} 262 263pub fn open_version ( & mut self , path : & str , text : & str , version : i64 ) { 264self . open_as ( path, text, version, "typescript" ); 265} 266 267pub fn open_as ( & mut self , path : & str , text : & str , version : i64 , language : & str ) { 268self . notify ( 269"textDocument/didOpen" , 270json! ({ 271"textDocument" : { 272"uri" : self . uri ( path), "text" : text, "version" : version, 273"languageId" : language 274} 275}), 276); 277} 278 279pub fn change ( & mut self , path : & str , text : & str , version : i64 ) { 280self . notify ( 281"textDocument/didChange" , 282json! ({ 283"textDocument" : { "uri" : self . uri ( path), "version" : version}, 284"contentChanges" : [{ "text" : text}] 285}), 286); 287} 288 289fn query ( & self , path : & str , text : & str , word : & str ) ->Value { 290Self :: query_uri ( & self . uri ( path), text, word) 291} 292 293fn query_uri ( uri : & str , text : & str , word : & str ) ->Value { 294let offset = text. rfind ( word). unwrap () +1 ; 295json! ({ 296"textDocument" : { "uri" : uri}, 297"position" : position_at ( text, offset) 298}) 299} 300 301pub fn hover_uri ( & mut self , uri : & str , text : & str , word : & str ) ->String { 302let result =self 303. call ( "textDocument/hover" , Self :: query_uri ( uri, text, word)) 304. unwrap (); 305hover_text ( & result) 306} 307 308pub fn hover ( & mut self , path : & str , text : & str , word : & str ) ->String { 309let result =self 310. call ( "textDocument/hover" , self . query ( path, text, word)) 311. unwrap (); 312hover_text ( & result) 313} 314 315pub fn definitions ( & mut self , path : & str , text : & str , word : & str ) ->Vec < Value > { 316self . call ( "textDocument/definition" , self . query ( path, text, word)) 317. unwrap () 318. as_array () 319. cloned () 320. unwrap_or_default () 321} 322 323pub fn complete ( 324& mut self , 325path : & str , 326text : & str , 327offset : usize , 328trigger_character : Option < & str >, 329) ->Value { 330let mut params =json! ({ 331"textDocument" : { "uri" : self . uri ( path)}, 332"position" : position_at ( text, offset) 333}); 334if let Some ( trigger) = trigger_character{ 335 params[ "context" ] =json! ({ "triggerKind" : 2 , "triggerCharacter" : trigger}); 336} 337self . call ( "textDocument/completion" , params). unwrap () 338} 339 340/// The most recently published diagnostics for a project-relative path. 341pub fn diagnostics ( & self , path : & str ) ->Vec < Value > { 342self . diagnostics_uri ( & self . uri ( path)) 343} 344 345pub fn diagnostics_uri ( & self , uri : & str ) ->Vec < Value > { 346self . notifications 347. iter () 348. rfind ( |message|{ 349 message[ "method" ] =="textDocument/publishDiagnostics" 350 && message[ "params" ][ "uri" ] == uri 351}) 352. map ( |message|{ 353 message[ "params" ][ "diagnostics" ] 354. as_array () 355. cloned () 356. unwrap_or_default () 357}) 358. unwrap_or_default () 359} 360 361pub fn eventually ( & mut self , mut check : impl FnMut ( & mut Server ) ->bool ) { 362for _in 0 ..80 { 363if check ( self ) { 364return ; 365} 366 std:: thread:: sleep ( Duration :: from_millis ( 100 )); 367} 368panic! ( "Live project did not update\n{}" , self . stderr ()); 369} 370 371pub fn close ( & mut self ) { 372self . call ( "shutdown" , json! ({})). unwrap (); 373self . notify ( "exit" , json! ({})); 374self . close_stdin (); 375let status =self . wait_status ( Duration :: from_secs ( 8 )); 376assert! ( status. success (), "Server exit {status}\n{}" , self . stderr ()); 377} 378} 379 380impl Drop for Server { 381fn drop ( & mut self ) { 382let _ =self . child . kill (); 383let _ =self . child . wait (); 384} 385} 386 387pub fn files ( root : & Path , entries : & [( & str , & str )]) { 388for ( name, text) in entries{ 389let path = root. join ( name); 390 fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 391 fs:: write ( path, text). unwrap (); 392} 393} 394 395/// LSP position (UTF-16 code units) for a byte offset into `text`. 396pub fn position_at ( text : & str , byte_offset : usize ) ->Value { 397assert! ( text. is_char_boundary ( byte_offset)); 398let prefix =& text[ ..byte_offset]; 399let line = prefix. matches ([ '\n' , '\r' ]). count () - prefix. matches ( "\r\n" ). count (); 400let line_start = prefix 401. rfind ([ '\n' , '\r' ]) 402. map ( |index| index + prefix[ index..]. chars (). next (). unwrap (). len_utf8 ()) 403. unwrap_or ( 0 ); 404let character = text[ line_start..byte_offset]. encode_utf16 (). count (); 405json! ({ "line" : line, "character" : character}) 406} 407 408/// Byte offset for an LSP position in `text`, clamping to the line end. 409pub fn offset_at ( text : & str , position : & Value ) ->usize { 410let line = position[ "line" ]. as_u64 (). unwrap () as usize ; 411let character = position[ "character" ]. as_u64 (). unwrap () as usize ; 412let mut offset =0 ; 413for ( current, part) in text. split_inclusive ( '\n' ). enumerate () { 414let content = part. trim_end_matches ([ '\r' , '\n' ]); 415if current == line{ 416let mut units =0 ; 417for ( index, ch) in content. char_indices () { 418if units >= character{ 419return offset + index; 420} 421 units += ch. len_utf16 (); 422} 423return offset + content. len (); 424} 425 offset += part. len (); 426} 427 text. len () 428} 429 430/// The source text covered by an LSP range value. 431pub fn selected ( text : & str , range : & Value ) ->String { 432 text[ offset_at ( text, & range[ "start" ]) ..offset_at ( text, & range[ "end" ])]. to_owned () 433} 434 435pub fn hover_text ( hover : & Value ) ->String { 436fn append ( content : & Value , text : & mut String ) { 437match content{ 438Value :: String ( value) => text. push_str ( value), 439Value :: Array ( contents) =>{ 440for ( index, content) in contents. iter (). enumerate () { 441if index >0 { 442 text. push ( '\n' ); 443} 444append ( content, text); 445} 446} 447Value :: Object ( object) =>{ 448if let Some ( value) = object. get ( "value" ). and_then ( Value :: as_str) { 449 text. push_str ( value); 450} 451} 452 _ =>{} 453} 454} 455let mut text =String :: new (); 456if let Some ( contents) = hover. get ( "contents" ) { 457append ( contents, & mut text); 458} 459 text 460}