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 ::{ Result , project:: canonical_uri, wire:: RpcError }; 2use serde:: Deserialize ; 3use serde_json::{ Value , json}; 4use std::{ 5 collections:: BTreeMap , 6 sync:: atomic::{ AtomicU64 , Ordering }, 7}; 8use url:: Url ; 9 10static GENERATION : AtomicU64 =AtomicU64 :: new ( 1 ); 11const MAX_ITEMS : usize =8192 ; 12 13# [ derive ( Deserialize )] 14pub ( crate ) struct Capabilities { 15pub version : u32 , 16pub capabilities : Value , 17pub methods : Vec < String >, 18} 19 20struct Document { 21native : String , 22read_only : bool , 23} 24struct Item { 25family : &' static str , 26project : String , 27value : Value , 28} 29 30/// Only document identities and opaque continuations cross this boundary. TypeScript 31/// owns language semantics; the host owns source acquisition and write policy. 32pub ( crate ) struct Bridge { 33generation : u64 , 34next : u64 , 35items : BTreeMap < u64 , Item >, 36documents : BTreeMap < String , Document >, 37editors : BTreeMap < String , String >, 38display_paths : Vec <( String , String )>, 39} 40impl Bridge { 41pub fn new () ->Self { 42Self { 43generation : GENERATION . fetch_add ( 1 , Ordering :: Relaxed ), 44next : 0 , 45items : BTreeMap :: new (), 46documents : BTreeMap :: new (), 47editors : BTreeMap :: new (), 48display_paths : Vec :: new (), 49} 50} 51pub fn insert ( & mut self , uri : String , file : & str , read_only : bool ) ->Result <()> { 52let native =Url :: from_file_path ( file) 53. map_err ( |() |"Invalid compiler path" ) ? 54. to_string (); 55self . editors . insert ( native. clone (), uri. clone ()); 56self . display_paths . push (( native. clone (), uri. clone ())); 57self . display_paths . push (( file. into (), uri. clone ())); 58if let Some (( directory, _)) = file. rsplit_once ( "/source" ) { 59self . display_paths 60. push (( format! ( "{directory}/source" ), uri. clone ())); 61} 62self . documents . insert ( uri, Document { native, read_only}); 63Ok (()) 64} 65pub fn native_uri ( & self , uri : & str ) ->Result < String > { 66let uri =canonical_uri ( uri) ?; 67if let Some ( document) =self . documents . get ( & uri) { 68return Ok ( document. native . clone ()); 69} 70if let Some ( name) = uri. strip_prefix ( "avril-builtin:/typescript/" ) 71 && name. starts_with ( "lib." ) 72 && name. ends_with ( ".d.ts" ) 73 && name 74. bytes () 75. all ( |b| b. is_ascii_alphanumeric () || b ==b'.' || b ==b'-' ) 76{ 77return Ok ( format! ( "bundled:///libs/{name}" )); 78} 79Err ( RpcError :: invalid ( format! ( "Document not in this project: {uri}" )). into ()) 80} 81pub fn contains ( & self , uri : & str ) ->bool { 82self . native_uri ( uri). is_ok () 83} 84fn editor_uri ( & self , native : & str ) ->Result < String > { 85if let Some ( uri) =self . editors . get ( native) { 86return Ok ( uri. clone ()); 87} 88let name = native. strip_prefix ( "bundled:///libs/" ); 89if let Some ( name) = name. filter ( |name|{ 90 name. starts_with ( "lib." ) 91 && name. ends_with ( ".d.ts" ) 92 && name 93. bytes () 94. all ( |b| b. is_ascii_alphanumeric () || b ==b'.' || b ==b'-' ) 95}) { 96return Ok ( format! ( "avril-builtin:/typescript/{name}" )); 97} 98Err ( format! ( "TypeScript returned an unknown document: {native}" ). into ()) 99} 100pub fn writable ( & self , uri : & str ) ->Result <()> { 101if self . documents . get ( uri). is_some_and ( |d| !d. read_only ) { 102Ok (()) 103} else { 104Err ( 105RpcError :: failed ( format! ( "Cannot edit read-only or unknown document: {uri}" )) 106. into (), 107) 108} 109} 110pub fn input ( & self , value : & mut Value ) ->Result <()> { 111match value{ 112Value :: Array ( values) =>{ 113for valuein values{ 114self . input ( value) ?; 115} 116} 117Value :: Object ( object) =>{ 118for ( key, value) in object{ 119if matches! ( key. as_str (), "uri" |"targetUri" |"oldUri" |"newUri" ) { 120let uri = value 121. as_str () 122. ok_or_else ( ||RpcError :: invalid ( "Invalid document URI" )) ?; 123* value =json! ( self . native_uri ( uri) ?); 124} else if key !="data" { 125self . input ( value) ?; 126} 127} 128} 129 _ =>{} 130} 131Ok (()) 132} 133pub fn restore ( & self , method : & str , params : & mut Value ) ->Result < Option < String >> { 134let family =match method{ 135"completionItem/resolve" =>"completion" , 136"codeAction/resolve" =>"action" , 137"codeLens/resolve" =>"lens" , 138"callHierarchy/incomingCalls" |"callHierarchy/outgoingCalls" =>"call" , 139 _ =>return Ok ( None ), 140}; 141let value =if family =="call" { 142 params 143. get_mut ( "item" ) 144. ok_or_else ( ||RpcError :: invalid ( "Missing hierarchy item" )) ? 145} else { 146 params 147}; 148# [ derive ( Deserialize )] 149struct Token { 150generation : u64 , 151id : u64 , 152} 153let token: Token = serde_json:: from_value ( value[ "data" ][ "avril" ]. clone ()) 154. map_err ( |_|RpcError :: modified ()) ?; 155let item =self 156. items 157. get ( & token. id ) 158. filter ( |item| token. generation ==self . generation && item. family == family) 159. ok_or_else ( RpcError :: modified) ?; 160* value = item. value . clone (); 161Ok ( Some ( item. project . clone ())) 162} 163fn remember ( & mut self , family : &' static str , project : & str , value : & mut Value ) { 164self . next +=1 ; 165self . items . insert ( 166self . next , 167Item { 168 family, 169project : project. into (), 170value : value. clone (), 171}, 172); 173if self . items . len () >MAX_ITEMS { 174self . items . pop_first (); 175} 176 value[ "data" ] =json! ({ "avril" : { "generation" : self . generation , "id" : self . next }}); 177} 178pub fn output ( & mut self , method : & str , project : & str , value : & mut Value ) ->Result <()> { 179match method{ 180"completionItem/resolve" |"codeAction/resolve" |"codeLens/resolve" 181if !value. is_null () => 182{ 183let family =match method{ 184"completionItem/resolve" =>"completion" , 185"codeAction/resolve" =>"action" , 186 _ =>"lens" , 187}; 188self . remember ( family, project, value); 189} 190"textDocument/completion" =>{ 191let items =if value. is_array () { 192 value. as_array_mut () 193} else { 194 value. get_mut ( "items" ). and_then ( Value :: as_array_mut) 195}; 196if let Some ( items) = items{ 197for itemin items{ 198self . remember ( "completion" , project, item); 199} 200} 201} 202"textDocument/codeAction" 203 |"textDocument/codeLens" 204 |"textDocument/prepareCallHierarchy" =>{ 205let family =match method{ 206"textDocument/codeAction" =>"action" , 207"textDocument/codeLens" =>"lens" , 208 _ =>"call" , 209}; 210if let Some ( items) = value. as_array_mut () { 211for itemin items{ 212self . remember ( family, project, item); 213} 214} 215} 216"textDocument/inlayHint" |"workspace/symbol" =>{ 217if let Some ( items) = value. as_array_mut () { 218for itemin items{ 219if let Some ( object) = item. as_object_mut () { 220 object. remove ( "data" ); 221} 222} 223} 224} 225"callHierarchy/incomingCalls" |"callHierarchy/outgoingCalls" =>{ 226if let Some ( items) = value. as_array_mut () { 227let key =if method =="callHierarchy/incomingCalls" { 228"from" 229} else { 230"to" 231}; 232for itemin items{ 233self . remember ( "call" , project, & mut item[ key]); 234} 235} 236} 237 _ =>{} 238} 239self . translate ( value) ?; 240self . check_edits ( value) 241} 242fn translate ( & mut self , value : & mut Value ) ->Result <()> { 243match value{ 244Value :: Array ( values) =>{ 245for valuein values{ 246self . translate ( value) ?; 247} 248} 249Value :: Object ( object) =>{ 250for ( key, value) in object{ 251match key. as_str () { 252"data" =>{} 253"uri" |"targetUri" |"oldUri" |"newUri" =>{ 254* value =json! ( 255self . editor_uri ( value. as_str (). ok_or ( "Invalid native URI" ) ?) ? 256); 257} 258"changes" if value. is_object () =>{ 259let mut changes = serde_json:: Map :: new (); 260for ( uri, mut edits) in std:: mem:: take ( value. as_object_mut (). unwrap ()) { 261self . translate ( & mut edits) ?; 262 changes. insert ( self . editor_uri ( & uri) ?, edits); 263} 264* value =Value :: Object ( changes); 265} 266"arguments" =>{ 267if let Some ( arguments) = value. as_array_mut () { 268for argumentin arguments{ 269if let Some ( uri) = argument. as_str (). filter ( |s|{ 270 s. starts_with ( "file:" ) || s. starts_with ( "bundled:" ) 271}) { 272* argument =json! ( self . editor_uri ( uri) ?); 273} else { 274self . translate ( argument) ?; 275} 276} 277} 278} 279"label" |"name" |"containerName" if value. is_string () =>{ 280self . display ( value) 281} 282"contents" |"documentation" |"tooltip" |"detail" |"message" 283 |"labelDetails" =>self . display ( value), 284 _ =>self . translate ( value) ?, 285} 286} 287} 288 _ =>{} 289} 290Ok (()) 291} 292pub fn display ( & self , value : & mut Value ) { 293match value{ 294Value :: String ( text) =>{ 295if !text. contains ( "/avril-lsp/" ) && !text. contains ( "bundled:" ) { 296return ; 297} 298for ( path, uri) in & self . display_paths { 299* text = text. replace ( path, uri); 300} 301* text = text. replace ( "bundled:///libs/" , "avril-builtin:/typescript/" ); 302} 303Value :: Array ( values) =>{ 304for valuein values{ 305self . display ( value); 306} 307} 308Value :: Object ( object) =>{ 309for valuein object. values_mut () { 310self . display ( value); 311} 312} 313 _ =>{} 314} 315} 316fn check_edits ( & self , value : & Value ) ->Result <()> { 317match value{ 318Value :: Array ( values) =>{ 319for valuein values{ 320self . check_edits ( value) ?; 321} 322} 323Value :: Object ( object) =>{ 324if let Some ( changes) = object. get ( "changes" ). and_then ( Value :: as_object) { 325for uriin changes. keys () { 326self . writable ( uri) ?; 327} 328} 329if object. contains_key ( "edits" ) 330 &&let Some ( uri) = value. pointer ( "/textDocument/uri" ). and_then ( Value :: as_str) 331{ 332self . writable ( uri) ?; 333} 334if matches! ( value[ "kind" ]. as_str (), Some ( "create" |"rename" |"delete" )) { 335return Err ( 336RpcError :: failed ( "TypeScript file operations are not authorized" ). into (), 337); 338} 339for ( key, value) in object{ 340if key !="data" { 341self . check_edits ( value) ?; 342} 343} 344} 345 _ =>{} 346} 347Ok (()) 348} 349} 350 351# [ cfg ( test )] 352mod tests{ 353use super :: * ; 354fn bridge () ->Bridge { 355let mut bridge =Bridge :: new (); 356 bridge 357. insert ( 358"file:///app/main.ts" . into (), 359"/avril-lsp/live/0/source.ts" , 360false , 361) 362. unwrap (); 363 bridge 364. insert ( 365"avril-https://example.test/mod.ts" . into (), 366"/avril-lsp/live/1/source.ts" , 367true , 368) 369. unwrap (); 370 bridge 371} 372# [ test ] 373fn locations_hierarchy_and_documentation_use_editor_identities () { 374let mut bridge =bridge (); 375let mut result =json! ({ "from" : { "uri" : "file : ///avril-lsp/live/1/source.ts" }, 376"documentation" :{ "kind" : "markdown" , "value" : " [ link ]( file : ///avril-lsp/live/0/source.ts#L2)" }}); 377 bridge. output ( "test" , "p" , & mut result). unwrap (); 378assert_eq! ( result[ "from" ][ "uri" ], "avril-https://example.test/mod.ts" ); 379assert_eq! ( 380 result[ "documentation" ][ "value" ], 381"[link](file:///app/main.ts#L2)" 382); 383} 384# [ test ] 385fn empty_completions_and_malformed_hierarchy_params_do_not_mutate_or_panic () { 386let mut bridge =bridge (); 387let mut completion =Value :: Null ; 388 bridge 389. output ( "textDocument/completion" , "p" , & mut completion) 390. unwrap (); 391assert_eq! ( completion, Value :: Null ); 392for mut paramsin [ json! ([]), json! ( 42 ), json! ( "bad" ), Value :: Null , json! ({})] { 393let error = bridge 394. restore ( "callHierarchy/incomingCalls" , & mut params) 395. unwrap_err (); 396assert_eq! ( error. downcast_ref ::< RpcError >(). unwrap (). code , -32602 ); 397} 398let mut tokens =json! ({ "data" :[ 0 , 0 , 4 , 1 , 0 ]}); 399 bridge 400. output ( "textDocument/semanticTokens/full" , "p" , & mut tokens) 401. unwrap (); 402assert_eq! ( tokens[ "data" ], json! ([ 0 , 0 , 4 , 1 , 0 ])); 403} 404 405# [ test ] 406fn display_names_and_diagnostic_quotes_hide_compiler_paths () { 407let mut bridge =bridge (); 408let mut result =json! ({ 409"name" : "/avril-lsp/live/ 0 /source . ts " , 410"containerName" : "/avril-lsp/live/ 1 /source" , 411"message" : " Module ' \"/avril-lsp/live/ 0 /source\" ' has no default export . " , 412"documentation" : " import ( \"/avril-lsp/live/ 1 /source\" ) " 413}); 414 bridge. output ( "test" , "p" , & mut result). unwrap (); 415assert_eq! ( result[ "name" ], "file:///app/main.ts" ); 416assert_eq! ( result[ "containerName" ], "avril-https://example.test/mod.ts" ); 417assert! ( !result. to_string (). contains ( "/avril-lsp/" )); 418} 419 420# [ test ] 421fn non_resolvable_results_do_not_expose_native_data_or_evict_completions () { 422let mut bridge =bridge (); 423let mut completion = 424json! ([{ "label" : "x" , "data" :{ "fileName" : "/avril-lsp/live/ 0 /source . ts " }}]); 425 bridge 426. output ( "textDocument/completion" , "p" , & mut completion) 427. unwrap (); 428let mut hints =json! ( vec! [ 429json! ({ "label" : "number" , "data" :{ "internal" : "private" }}); 430MAX_ITEMS +1 431]); 432 bridge 433. output ( "textDocument/inlayHint" , "p" , & mut hints) 434. unwrap (); 435assert! ( hints[ 0 ]. get ( "data" ). is_none ()); 436assert! ( 437 bridge 438. restore ( "completionItem/resolve" , & mut completion[ 0 ]) 439. is_ok () 440); 441} 442 443# [ test ] 444fn opaque_data_is_bound_to_generation_and_method_and_not_client_controlled () { 445let mut bridge =bridge (); 446let original = 447json! ({ "label" : "x" , "data" :{ "fileName" : "/avril-lsp/live/ 0 /source . ts " , "position" : 4 }}); 448let mut result =json! ({ "items" :[ original]}); 449 bridge 450. output ( "textDocument/completion" , "p" , & mut result) 451. unwrap (); 452let mut item = result[ "items" ][ 0 ]. clone (); 453let mut stale = item. clone (); 454 item[ "label" ] =json! ( "changed by client" ); 455assert_eq! ( 456 bridge 457. restore ( "completionItem/resolve" , & mut item) 458. unwrap () 459. as_deref (), 460Some ( "p" ) 461); 462assert_eq! ( item, original); 463assert! ( bridge. restore ( "codeAction/resolve" , & mut stale). is_err ()); 464assert! ( 465super :: Bridge :: new () 466. restore ( "completionItem/resolve" , & mut stale) 467. is_err () 468); 469} 470# [ test ] 471fn readonly_edits_are_rejected_atomically_in_both_workspace_edit_forms () { 472for mut resultin [ 473json! ({ "changes" :{ "file : ///avril-lsp/live/0/source.ts" :[], "file:///avril-lsp/live/1/source.ts" :[]}}), 474json! ({ "documentChanges" :[{ "textDocument" :{ "uri" : "file : ///avril-lsp/live/1/source.ts" , "version" : null }, "edits" :[]}]}), 475] { 476assert! ( 477bridge () 478. output ( "textDocument/rename" , "p" , & mut result) 479. is_err () 480); 481} 482} 483# [ test ] 484fn library_locations_are_readonly_and_unknown_files_never_escape () { 485let mut bridge =bridge (); 486let mut result =json! ({ "uri" : "bundled : ///libs/lib.es5.d.ts" }); 487 bridge. output ( "test" , "p" , & mut result). unwrap (); 488assert_eq! ( result[ "uri" ], "avril-builtin:/typescript/lib.es5.d.ts" ); 489assert_eq! ( 490 bridge. native_uri ( result[ "uri" ]. as_str (). unwrap ()). unwrap (), 491"bundled:///libs/lib.es5.d.ts" 492); 493assert! ( bridge. writable ( result[ "uri" ]. as_str (). unwrap ()). is_err ()); 494assert! ( 495 bridge 496. output ( "test" , "p" , & mut json! ({ "uri" : "file : ///etc/passwd" })) 497. is_err () 498); 499} 500}