char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c7a9569
main
1use crate ::{ 2Documents , ProjectService , Result , 3 project::{ Project , canonical_uri}, 4 wire, 5}; 6use serde_json::{ Value , json}; 7use std::{ 8 collections::{ BTreeMap , BTreeSet , HashSet }, 9 path:: PathBuf , 10 sync::{ Arc , Mutex }, 11}; 12use tokio::{ 13 io::{ AsyncRead , AsyncWrite , BufReader }, 14 sync::{ Notify , mpsc}, 15}; 16use tokio_util:: sync:: CancellationToken ; 17 18struct Session { 19service : Box < dyn ProjectService >, 20documents : Documents , 21failure : Option < String >, 22binary : PathBuf , 23project : Option < Project >, 24initialized : bool , 25shutdown : bool , 26client : Value , 27preferences : Value , 28capabilities : Value , 29methods : BTreeSet < String >, 30open : BTreeMap < String , i64 >, 31published : BTreeSet < String >, 32dirty : bool , 33refresh_request : u64 , 34} 35impl Session { 36async fn rebuild ( & mut self ) ->Result <()> { 37self . service . changed (); 38self . dirty =false ; 39if let Some ( project) =& mut self . project { 40// Teardown still kills/reaps a failed backend; allow the next generation 41// to recover rather than retaining a permanently failed connection. 42if let Err ( error) = project. close (). await { 43eprintln! ( "LSP backend cleanup: {error}" ); 44} 45} 46self . project =None ; 47let result =async { 48let input =self . service . refresh ( self . documents . clone ()). await ?; 49let project =Project :: create ( & self . binary , input). await ?; 50let capabilities = project. capabilities ( & self . client ). await ?; 51let methods: BTreeSet < _ > = capabilities. methods . into_iter (). collect (); 52if self . initialized 53 &&( capabilities. capabilities !=self . capabilities || methods !=self . methods ) 54{ 55return Err ( "TypeScript backend protocol changed during session" . into ()); 56} 57if !self . initialized { 58self . capabilities = capabilities. capabilities ; 59self . methods = methods; 60} 61Ok ::< _ , crate :: Error >( project) 62} 63. await ; 64match result{ 65Ok ( project) =>{ 66self . project =Some ( project); 67self . failure =None ; 68Ok (()) 69} 70Err ( error) =>{ 71self . failure =Some ( error. to_string ()); 72Err ( error) 73} 74} 75} 76async fn publish ( & mut self , writer : & mut ( impl AsyncWrite +Unpin )) ->Result <()> { 77let mut current: BTreeSet < String > =self . open . keys (). cloned (). collect (); 78if let Some ( project) =& self . project { 79 current. extend ( project. error_uris (). cloned ()); 80} 81for uriin current. union ( & self . published ) { 82let diagnostics =if !current. contains ( uri) { 83json! ([]) 84} else if let Some ( project) =& mut self . project { 85match project 86. diagnostics ( uri, & self . client , & self . preferences ) 87. await 88{ 89Ok ( diagnostics) => diagnostics, 90Err ( error) =>{ 91self . failure =Some ( error. to_string ()); 92json! ([{ "range" :{ "start" :{ "line" : 0 , "character" : 0 }, "end" :{ "line" : 0 , "character" : 0 }}, 93"severity" : 1 , "source" : "avril" , "message" : error. to_string ()}]) 94} 95} 96} else { 97json! ([{ "range" :{ "start" :{ "line" : 0 , "character" : 0 }, "end" :{ "line" : 0 , "character" : 0 }}, 98"severity" : 1 , "source" : "avril" , "message" : self . failure . as_deref (). unwrap_or ( "Project unavailable" )}]) 99}; 100let mut params =json! ({ "uri" : uri, "diagnostics" : diagnostics}); 101if let Some ( version) =self . open . get ( uri) { 102 params[ "version" ] =json! ( version); 103} 104 wire:: write ( 105 writer, 106json! ({ "method" : "textDocument/publishDiagnostics" , "params" : params}), 107) 108. await ?; 109} 110self . published = current; 111for featurein [ "codeLens" , "inlayHint" , "semanticTokens" ] { 112if self 113. client 114. pointer ( & format! ( "/workspace/{feature}/refreshSupport" )) 115. and_then ( Value :: as_bool) 116 ==Some ( true ) 117{ 118self . refresh_request +=1 ; 119 wire:: write ( 120 writer, 121json! ({ 122"id" : format! ( "avril-refresh-{}" , self . refresh_request ), 123"method" : format! ( "workspace/{feature}/refresh" ), 124}), 125) 126. await ?; 127} 128} 129Ok (()) 130} 131async fn handle ( 132& mut self , 133method : & str , 134params : Value , 135writer : & mut ( impl AsyncWrite +Unpin ), 136) ->Result < Value > { 137match method{ 138"initialize" =>{ 139if self . initialized { 140return Err ( "Already initialized" . into ()); 141} 142self . client = params. get ( "capabilities" ). cloned (). unwrap_or ( json! ({})); 143self . preferences = params 144. pointer ( "/initializationOptions/preferences" ) 145. cloned () 146. unwrap_or ( json! ({})); 147if !self . preferences . is_object () { 148return Err ( wire:: RpcError :: invalid ( "preferences must be an object" ). into ()); 149} 150// Resolve data is bound independently for every completion item. 151if let Some ( list) =self 152. client 153. pointer_mut ( "/textDocument/completion/completionList" ) 154. and_then ( Value :: as_object_mut) 155{ 156 list. remove ( "itemDefaults" ); 157} 158self . rebuild (). await ?; 159self . initialized =true ; 160let mut capabilities =self . capabilities . clone (); 161 capabilities[ "positionEncoding" ] =json! ( "utf-16" ); 162 capabilities[ "textDocumentSync" ] =json! ({ "openClose" : true , "change" : 1 }); 163return Ok ( json! ({ 164"capabilities" : capabilities, 165"serverInfo" : { "name" : "avril-lsp" , "version" : env! ( "CARGO_PKG_VERSION" )} 166})); 167} 168"initialized" =>return Ok ( Value :: Null ), 169"workspace/didChangeWatchedFiles" |"workspace/didChangeConfiguration" =>{ 170if method =="workspace/didChangeConfiguration" 171 &&let Some ( preferences) = params. pointer ( "/settings/preferences" ) 172{ 173if !preferences. is_object () { 174return Err ( wire:: RpcError :: invalid ( "preferences must be an object" ). into ()); 175} 176self . preferences = preferences. clone (); 177} 178if self . initialized && !self . shutdown { 179self . dirty =true ; 180} 181return Ok ( Value :: Null ); 182} 183"shutdown" =>{ 184self . shutdown =true ; 185let cleanup =if let Some ( project) =& mut self . project { 186 project. close (). await 187} else { 188Ok (()) 189}; 190self . project =None ; 191 cleanup?; 192return Ok ( Value :: Null ); 193} 194 _ =>{} 195} 196let uri = params 197. pointer ( "/textDocument/uri" ) 198. and_then ( Value :: as_str) 199. map ( canonical_uri) 200. transpose () ?; 201match method{ 202"textDocument/didOpen" =>{ 203let uri = uri. ok_or ( "Missing document URI" ) ?; 204if !uri. starts_with ( "file:" ) 205 && !self . project . as_ref (). is_some_and ( |p| p. contains ( & uri)) 206{ 207return Ok ( Value :: Null ); 208} 209let text = params[ "textDocument" ][ "text" ] 210. as_str () 211. ok_or ( "Invalid didOpen text" ) ?; 212let version = params[ "textDocument" ][ "version" ] 213. as_i64 () 214. ok_or ( "Invalid document version" ) ?; 215if self . open . contains_key ( & uri) { 216return Err ( "Document already open" . into ()); 217} 218self . open . insert ( uri. clone (), version); 219if uri. starts_with ( "file:" ) 220 && !self . project . as_ref (). is_some_and ( |p| p. is_read_only ( & uri)) 221{ 222self . documents . insert ( uri, text. into ()); 223self . dirty =true ; 224} 225Ok ( Value :: Null ) 226} 227"textDocument/didChange" =>{ 228let uri = uri. ok_or ( "Missing document URI" ) ?; 229let version = params[ "textDocument" ][ "version" ] 230. as_i64 () 231. ok_or ( "Invalid document version" ) ?; 232if self . open . get ( & uri). is_none_or ( |old| version <=* old) { 233return Ok ( Value :: Null ); 234} 235let changes = params[ "contentChanges" ] 236. as_array () 237. ok_or ( "Invalid document changes" ) ?; 238if changes. is_empty () 239 || changes 240. iter () 241. any ( |c| c. get ( "range" ). is_some () || !c[ "text" ]. is_string ()) 242{ 243return Err ( "Only full-document changes are supported" . into ()); 244} 245let text = changes 246. last () 247. and_then ( |c| c[ "text" ]. as_str ()) 248. ok_or ( "Missing change text" ) ?; 249self . open . insert ( uri. clone (), version); 250if uri. starts_with ( "file:" ) 251 && !self . project . as_ref (). is_some_and ( |p| p. is_read_only ( & uri)) 252{ 253self . documents . insert ( uri, text. into ()); 254self . dirty =true ; 255} 256Ok ( Value :: Null ) 257} 258"textDocument/didClose" =>{ 259let uri = uri. ok_or ( "Missing document URI" ) ?; 260if self . open . remove ( & uri). is_none () { 261return Ok ( Value :: Null ); 262} 263 wire:: write ( 264 writer, 265json! ({ 266"method" : "textDocument/publishDiagnostics" , 267"params" : { "uri" : uri, "diagnostics" : []} 268}), 269) 270. await ?; 271if self . documents . remove ( & uri). is_some () { 272self . dirty =true ; 273} 274Ok ( Value :: Null ) 275} 276"textDocument/diagnostic" =>{ 277let uri = uri. ok_or ( "Missing document URI" ) ?; 278let items =self 279. project 280. as_mut () 281. ok_or ( "Project unavailable" ) ? 282. diagnostics ( & uri, & self . client , & self . preferences ) 283. await ?; 284Ok ( json! ({ "kind" : "full" , "items" : items})) 285} 286"avril/readDocument" =>{ 287let project =self . project . as_mut (). ok_or ( "Project unavailable" ) ?; 288let uri =canonical_uri ( params[ "uri" ]. as_str (). ok_or ( "Missing document URI" ) ?) ?; 289if uri. starts_with ( "avril-builtin:/typescript/" ) { 290 project 291. language_service ( 292 method, 293json! ({ "textDocument" :{ "uri" : uri}}), 294& self . client , 295& self . preferences , 296) 297. await 298} else { 299Ok ( json! ( project. read_document ( & uri) ?)) 300} 301} 302 _if self . methods . contains ( method) =>{ 303self . project 304. as_mut () 305. ok_or ( "Project unavailable" ) ? 306. language_service ( method, params, & self . client , & self . preferences ) 307. await 308} 309 _ =>Err ( format! ( "Unsupported method: {method}" ). into ()), 310} 311} 312} 313 314/// Owns a workspace session and its native backend. Acquisition and resolution 315/// remain in the host's project service; no guest code is executed here. 316pub async fn serve < R , W >( 317reader : R , 318mut writer : W , 319binary : PathBuf , 320service : Box < dyn ProjectService >, 321) ->Result <()> 322where 323R : AsyncRead +Unpin +Send +' static , 324W : AsyncWrite +Unpin , 325{ 326let mut session =Session { 327 service, 328documents : Documents :: new (), 329failure : None , 330 binary, 331project : None , 332initialized : false , 333shutdown : false , 334client : json! ({}), 335preferences : json! ({}), 336capabilities : json! ({}), 337methods : BTreeSet :: new (), 338open : BTreeMap :: new (), 339published : BTreeSet :: new (), 340dirty : false , 341refresh_request : 0 , 342}; 343# [ cfg ( unix )] 344let mut terminate = tokio:: signal:: unix:: signal ( tokio:: signal:: unix:: SignalKind :: terminate ()) ?; 345let interrupt =async { 346# [ cfg ( unix )] 347{ 348 tokio:: select!{ 349 result = tokio:: signal:: ctrl_c () => result, 350 _ = terminate. recv () =>Ok (()), 351} 352} 353# [ cfg ( not ( unix ))] 354{ 355 tokio:: signal:: ctrl_c (). await 356} 357}; 358 tokio:: pin!( interrupt); 359let disconnected =CancellationToken :: new (); 360let pending =Arc :: new ( Mutex :: new ( HashSet ::< String >:: new ())); 361let cancelled =Arc :: new ( Mutex :: new ( HashSet ::< String >:: new ())); 362let cancellation =Arc :: new ( Notify :: new ()); 363let ( tx, mut rx) = mpsc:: channel ( 64 ); 364let reader_task = tokio:: spawn ({ 365let disconnected = disconnected. clone (); 366let pending = pending. clone (); 367let cancelled = cancelled. clone (); 368let cancellation = cancellation. clone (); 369async move { 370let mut reader =BufReader :: new ( reader); 371loop { 372let message =match wire:: read ( & mut reader). await { 373Ok ( Some ( message)) => message, 374Ok ( None ) =>{ 375 disconnected. cancel (); 376return ; 377} 378Err ( error) =>{ 379let _ = tx. send ( Err ( error)). await ; 380 disconnected. cancel (); 381return ; 382} 383}; 384if message[ "method" ] =="$/cancelRequest" { 385let key = message[ "params" ][ "id" ]. to_string (); 386if pending. lock (). unwrap (). contains ( & key) { 387 cancelled. lock (). unwrap (). insert ( key); 388 cancellation. notify_one (); 389} 390continue ; 391} 392if message[ "method" ]. is_string () 393 &&let Some ( id) = message. get ( "id" ) 394{ 395 pending. lock (). unwrap (). insert ( id. to_string ()); 396} 397let exit = message[ "method" ] =="exit" ; 398if tx. send ( Ok ( message)). await . is_err () { 399return ; 400} 401// Do not start a blocking stdin read after an explicit exit. 402if exit{ 403return ; 404} 405} 406} 407}); 408let mut polling = tokio:: time:: interval ( std:: time:: Duration :: from_millis ( 100 )); 409 polling. set_missed_tick_behavior ( tokio:: time:: MissedTickBehavior :: Skip ); 410let debounce = tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 100 )); 411 tokio:: pin!( debounce); 412let result: Result <()> =async { 413loop { 414let message = tokio:: select!{ 415 biased; 416 message = rx. recv () =>match message{ 417Some ( message) => message?, 418None =>return if session. shutdown { Ok (()) } else { 419Err ( "Editor connection closed without shutdown" . into ()) 420} 421}, 422 _ =& mut debounce, if session. dirty && session. initialized && !session. shutdown =>{ 423let rebuilt = session. rebuild (). await ; 424 session. publish ( & mut writer). await ?; 425if let Err ( error) = rebuilt{ 426 wire:: write ( & mut writer, json! ({ "method" : "window/ logMessage " , "params" :{ " type " : 1 , " message " : error. to_string ()}})). await ?; 427} 428continue ; 429}, 430 _ = polling. tick (), if session. initialized && !session. shutdown =>{ 431if session. service . changed () { 432 session. dirty =true ; 433 debounce. as_mut (). reset ( tokio:: time:: Instant :: now () + std:: time:: Duration :: from_millis ( 100 )); 434} 435continue ; 436}, 437 _ = disconnected. cancelled () =>return Err ( "Editor disconnected" . into ()), 438 _ =& mut interrupt =>return Err ( "LSP interrupted" . into ()), 439}; 440let Some ( method) = message[ "method" ]. as_str () else { 441continue ; 442}; 443if message. get ( "id" ). is_none () &&matches! ( method, "$/setTrace" |"textDocument/didSave" ) { 444continue ; 445} 446if method =="exit" { 447return if session. shutdown { 448Ok (()) 449} else { 450Err ( "LSP exit without shutdown" . into ()) 451}; 452} 453let id = message. get ( "id" ). cloned (); 454let key = id. as_ref (). map ( Value :: to_string); 455let cancel = ||{ 456 key. as_ref () 457. is_some_and ( |k| cancelled. lock (). unwrap (). contains ( k)) 458}; 459let mut error_code = -32603 ; 460let query = id. is_some () && session. initialized && !session. shutdown && session. methods . contains ( method); 461let recovery =if query 462 && !cancel () 463 &&( session. dirty 464 || session. project . as_ref (). is_none_or ( Project :: failed)) 465{ 466let result = session. rebuild (). await ; 467if result. is_ok () { 468 session. publish ( & mut writer). await ?; 469} 470Some ( result) 471} else { 472None 473}; 474let response =if let Some ( Err ( error)) = recovery{ 475Err ( error) 476} else if cancel () { 477 error_code = -32800 ; 478Err ( "Request cancelled" . into ()) 479} else if ( !session. initialized && method !="initialize" ) || session. shutdown { 480 error_code =if session. shutdown { -32600 } else { -32002 }; 481Err ( "Server not initialized or has shut down" . into ()) 482} else { 483if !matches! ( 484 method, 485"initialize" 486 |"initialized" 487 |"shutdown" 488 |"textDocument/didOpen" 489 |"textDocument/didChange" 490 |"textDocument/didClose" 491 |"avril/readDocument" 492 |"workspace/didChangeWatchedFiles" 493 |"workspace/didChangeConfiguration" 494) && !session. methods . contains ( method) 495{ 496 error_code = -32601 ; 497} 498 tokio:: select!{ 499 result = session. handle ( method, message[ "params" ]. clone (), & mut writer) => result, 500 _ =async { 501loop { 502let notified = cancellation. notified (); 503if cancel () { break ; } 504 notified. await ; 505} 506}, if session. methods . contains ( method) =>{ 507 error_code = -32800 ; 508Err ( "Request cancelled" . into ()) 509}, 510 _ = disconnected. cancelled () =>return Err ( "Editor disconnected" . into ()), 511 _ =& mut interrupt =>return Err ( "LSP interrupted" . into ()), 512} 513}; 514if let Some ( id) = id{ 515let response =if cancel () { 516json! ({ "id" : id, "error" : { "code" : -32800 , "message" : "Request cancelled" }}) 517} else { 518match response{ 519Ok ( value) =>json! ({ "id" : id, "result" : value}), 520Err ( error) =>{ 521let native = error. downcast_ref ::< wire:: RpcError >(); 522json! ({ "id" : id, "error" : { 523"code" : native. map_or ( error_code, |e| e. code ), 524"message" : error. to_string (), 525"data" : native. and_then ( |e| e. data . as_ref ()), 526}}) 527} 528} 529}; 530 wire:: write ( & mut writer, response). await ?; 531} else if let Err ( error) = response{ 532 wire:: write ( 533& mut writer, 534json! ({ 535"method" : "window/logMessage" , 536"params" : { " type " : 1 , "message" : error. to_string ()} 537}), 538) 539. await ?; 540} 541if session. dirty { 542 debounce. as_mut (). reset ( tokio:: time:: Instant :: now () + std:: time:: Duration :: from_millis ( 100 )); 543} 544if let Some ( key) = key{ 545 pending. lock (). unwrap (). remove ( & key); 546 cancelled. lock (). unwrap (). remove ( & key); 547} 548} 549} 550. await ; 551 reader_task. abort (); 552let _ = reader_task. await ; 553let cleanup =if let Some ( mut project) = session. project { 554if result. is_err () { 555 project. terminate (). await 556} else { 557 project. close (). await 558} 559} else { 560Ok (()) 561}; 562 result. and ( cleanup) 563}