char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1c8ffed
main
1use crate ::{ 2Result , 3 backend::{ Backend , Callbacks }, 4 bridge::{ Bridge , Capabilities }, 5}; 6use percent_encoding::{ NON_ALPHANUMERIC , percent_decode_str, utf8_percent_encode}; 7use serde:: Deserialize ; 8use serde_json::{ Value , json}; 9use sha2::{ Digest , Sha256 }; 10use std::{ 11 collections::{ BTreeMap , BTreeSet }, 12 path:: Path , 13 sync:: Arc , 14}; 15use url:: Url ; 16 17/// Canonical module URIs, not compiler paths. Require falls back to import when 18/// no separate require edge was recorded. 19# [ derive ( Default )] 20pub struct ResolvedImport { 21pub import : Option < String >, 22pub require : Option < String >, 23} 24 25pub struct Source { 26pub uri : String , 27pub text : String , 28pub read_only : bool , 29} 30# [ derive ( Clone , Copy )] 31pub enum ScriptKind { 32JavaScript , 33TypeScript , 34} 35 36pub struct ProjectInput { 37pub sources : Vec < Source >, 38/// MIME-selected languages for modules whose URLs are not filenames. 39pub script_kinds : BTreeMap < String , ScriptKind >, 40pub compiler_options : Value , 41pub imports : BTreeMap <( String , String ), ResolvedImport >, 42/// Authorized additional import spellings (referrer URI, target URI, specifier). 43pub import_candidates : Vec <( String , String , String )>, 44pub per_file_options : BTreeMap < String , Value >, 45pub diagnostics : BTreeMap < String , Vec < String >>, 46pub type_references : BTreeMap <( String , String ), String >, 47pub path_references : BTreeMap <( String , String ), String >, 48pub globals : BTreeSet < String >, 49} 50 51# [ derive ( Clone , Debug , PartialEq , Eq )] 52pub struct Diagnostic { 53pub uri : String , 54pub line : usize , 55pub character : usize , 56pub end_line : usize , 57pub end_character : usize , 58pub severity : u8 , 59pub code : Option < u64 >, 60pub source : String , 61pub message : String , 62} 63 64fn type_package_name ( name : & str ) ->Result < String > { 65let valid_part = |part : & str |{ 66 !part. is_empty () 67 && !matches! ( part, "." |".." ) 68 && part 69. bytes () 70. all ( |byte| byte. is_ascii_alphanumeric () ||matches! ( byte, b'-' |b'_' |b'.' )) 71}; 72if let Some ( scope) = name. strip_prefix ( '@' ) { 73let Some (( scope, package)) = scope. split_once ( '/' ) else { 74return Err ( "Invalid scoped type reference name" . into ()); 75}; 76if !valid_part ( scope) || !valid_part ( package) || package. contains ( '/' ) { 77return Err ( "Invalid scoped type reference name" . into ()); 78} 79Ok ( format! ( "{scope}__{package}" )) 80} else if valid_part ( name) { 81Ok ( name. to_owned ()) 82} else { 83Err ( "Invalid type reference name" . into ()) 84} 85} 86 87fn insert_path_alias ( host : & mut Host , path : String , target : String ) ->Result <()> { 88if host 89. aliases 90. get ( & path) 91. is_some_and ( |previous| previous !=& target) 92{ 93return Err ( format! ( "Conflicting virtual path reference alias: {path}" ). into ()); 94} 95 host. aliases . insert ( path, target); 96Ok (()) 97} 98 99fn validate_virtual_paths ( host : & mut Host ) ->Result <()> { 100for ( path, target) in & host. aliases { 101if host. contents . contains_key ( path) && path != target{ 102return Err ( format! ( "Path reference shadows a canonical source: {path}" ). into ()); 103} 104} 105let virtual_files: BTreeSet < _ > = host 106. contents 107. keys () 108. chain ( host. aliases . keys ()) 109. map ( String :: as_str) 110. collect (); 111for filein & virtual_files{ 112for ancestorin Path :: new ( file). ancestors (). skip ( 1 ) { 113if virtual_files. contains ( ancestor. to_str (). unwrap ()) { 114return Err ( format! ( 115"Virtual file collides with a directory: {}" , 116 ancestor. display () 117) 118. into ()); 119} 120} 121} 122 host. aliases . retain ( |path, target| path != target); 123Ok (()) 124} 125 126fn lexical_path ( path : & Path ) ->String { 127use std:: path:: Component ; 128let mut parts =Vec :: new (); 129for componentin path. components () { 130match component{ 131Component :: RootDir => parts. clear (), 132Component :: CurDir =>{} 133Component :: ParentDir =>{ 134 parts. pop (); 135} 136Component :: Normal ( part) => parts. push ( part. to_string_lossy (). into_owned ()), 137Component :: Prefix ( _) =>{} 138} 139} 140format! ( "/{}" , parts. join ( "/" )) 141} 142 143const COMPONENT : & percent_encoding:: AsciiSet =& NON_ALPHANUMERIC 144. remove ( b'-' ) 145. remove ( b'_' ) 146. remove ( b'.' ) 147. remove ( b'~' ) 148. remove ( b'!' ) 149. remove ( b'\'' ) 150. remove ( b'(' ) 151. remove ( b')' ) 152. remove ( b'*' ); 153 154fn editor_uri ( uri : & str , extension : & str ) ->Result < String > { 155let uri =canonical_uri ( uri) ?; 156let mut url =Url :: parse ( & uri) ?; 157match url. scheme () { 158"https" |"http" =>{ 159let scheme = url. scheme (). to_owned (); 160// Custom-scheme query components must survive VS Code's URI codec 161// without collapsing an escaped delimiter into a real delimiter. 162if let Some ( query) = url. query () { 163let query =utf8_percent_encode ( query, COMPONENT ). to_string (); 164 url. set_query ( Some ( & query)); 165} 166canonical_uri ( & url. to_string (). replacen ( 167& format! ( "{scheme}:" ), 168& format! ( "avril-{scheme}:" ), 1691 , 170)) 171} 172"avril" =>Ok ( uri. replacen ( "avril:" , "avril-builtin:" , 1 )), 173"data" =>Ok ( format! ( 174"avril-builtin:/data/{:x}{extension}" , 175Sha256 :: digest ( uri. as_bytes ()) 176)), 177"file" =>Ok ( uri), 178 _ =>Err ( "Unsupported module URI scheme" . into ()), 179} 180} 181 182pub fn canonical_uri ( uri : & str ) ->Result < String > { 183let mut url =Url :: parse ( uri) ?; 184// Percent escapes in an opaque payload are source bytes, not path aliases. 185if url. scheme () =="data" { 186return Ok ( url. to_string ()); 187} 188let parts: Result < Vec < _ >> = url 189. path () 190. split ( '/' ) 191. map ( |s|{ 192Ok ( utf8_percent_encode ( & percent_decode_str ( s). decode_utf8 () ?, COMPONENT ). to_string ()) 193}) 194. collect (); 195 url. set_path ( & parts?. join ( "/" )); 196if matches! ( url. scheme (), "avril-http" |"avril-https" ) 197 &&let Some ( query) = url. query () 198{ 199let query = 200utf8_percent_encode ( & percent_decode_str ( query). decode_utf8 () ?, COMPONENT ). to_string (); 201 url. set_query ( Some ( & query)); 202} 203// VS Code removes empty authorities from custom-scheme URIs. 204if matches! ( 205 url. scheme (), 206"avril" |"avril-https" |"avril-http" |"avril-builtin" 207) && url. host (). is_none () 208{ 209Ok ( url. to_string (). replacen ( ":///" , ":/" , 1 )) 210} else { 211Ok ( url. to_string ()) 212} 213} 214 215struct Module { 216source : Source , 217file : String , 218} 219struct Host { 220contents : BTreeMap < String , String >, 221aliases : BTreeMap < String , String >, 222} 223impl Callbacks for Host { 224fn call ( & self , method : & str , params : Value ) ->Result < Value > { 225if !matches! ( 226 method, 227"readFile" |"fileExists" |"directoryExists" |"getAccessibleEntries" |"realpath" 228) { 229return Err ( format! ( "Unexpected backend callback: {method}" ). into ()); 230} 231let path = params. as_str (). ok_or ( "Invalid filesystem callback" ) ?; 232if method =="realpath" { 233return Ok ( 234json! ({ "kind" : "value" , "value" : self . aliases . get ( path). map_or ( path, String :: as_str)}), 235); 236} 237if let Some ( target) =self . aliases . get ( path) { 238return Ok ( match method{ 239"readFile" =>json! ({ "kind" : "value" , "value" : self . contents . get ( target)}), 240"fileExists" =>json! ({ "kind" : "value" , "value" : true }), 241 _ =>json! ({ "kind" : "value" , "value" : false }), 242}); 243} 244let prefix =format! ( "{path}/" ); 245let mut files =BTreeSet :: new (); 246let mut directories =BTreeSet :: new (); 247for entryin self 248. aliases 249. range ( prefix. clone () ..) 250. map_while ( |( alias, _) | alias. strip_prefix ( & prefix)) 251{ 252if let Some (( directory, _)) = entry. split_once ( '/' ) { 253 directories. insert ( directory); 254} else { 255 files. insert ( entry); 256} 257} 258if !files. is_empty () || !directories. is_empty () { 259return Ok ( match method{ 260"directoryExists" =>json! ({ "kind" : "value" , "value" : true }), 261"getAccessibleEntries" =>{ 262json! ({ "kind" : "value" , "value" :{ "files" : files, "directories" : directories}}) 263} 264 _ =>json! ({ "kind" : "value" , "value" : false }), 265}); 266} 267if path =="bundled:///libs" || path. starts_with ( "bundled:///libs/" ) { 268return Ok ( json! ({ "kind" : "useOS" })); 269} 270Ok ( match method{ 271"readFile" =>json! ({ "kind" : "missing" }), 272"getAccessibleEntries" =>{ 273json! ({ "kind" : "value" , "value" :{ "files" :[], "directories" :[]}}) 274} 275 _ =>json! ({ "kind" : "value" , "value" : false }), 276}) 277} 278} 279# [ derive ( Deserialize )] 280struct Snapshot { 281snapshot : u64 , 282# [ serde ( default )] 283operation : Operation , 284} 285# [ derive ( Default , Deserialize )] 286struct Operation { 287# [ serde ( default , rename = "createdPrograms" )] 288created_programs : Vec < String >, 289} 290pub ( crate ) struct Project { 291backend : Backend , 292modules : BTreeMap < String , Module >, 293bridge : Bridge , 294module_specifiers : Vec < Value >, 295snapshot : u64 , 296projects : BTreeMap < String , String >, 297diagnostics : BTreeMap < String , Vec < String >>, 298resolver : u64 , 299} 300impl Project { 301pub async fn create ( binary : & Path , input : ProjectInput ) ->Result < Self > { 302let binary = std:: fs:: canonicalize ( binary) ?; 303let globals = input 304. globals 305. into_iter () 306. map ( |uri|canonical_uri ( & uri)) 307. collect ::< Result < BTreeSet < _ >>>() ?; 308let script_kinds = input 309. script_kinds 310. into_iter () 311. map ( |( uri, kind) |Ok (( canonical_uri ( & uri) ?, kind))) 312. collect ::< Result < BTreeMap < _ , _ >>>() ?; 313let mut modules =BTreeMap :: new (); 314let mut bridge =Bridge :: new (); 315let mut files =BTreeMap :: new (); 316let mut host =Host { 317contents : BTreeMap :: new (), 318aliases : BTreeMap :: new (), 319}; 320for mut sourcein input. sources { 321 source. uri =canonical_uri ( & source. uri ) ?; 322let uri =Url :: parse ( & source. uri ) ?; 323let extension =match script_kinds. get ( & source. uri ) { 324Some ( ScriptKind :: JavaScript ) =>".js" , 325Some ( ScriptKind :: TypeScript ) =>".ts" , 326None =>[ 327".d.mts" , ".d.cts" , ".d.ts" , ".tsx" , ".jsx" , ".mts" , ".cts" , ".mjs" , ".cjs" , 328".ts" , ".js" , 329] 330. into_iter () 331. find ( |ext| uri. path (). ends_with ( ext)) 332. unwrap_or ( ".ts" ), 333}; 334let editor_uri =if source. read_only && uri. scheme () =="file" { 335canonical_uri ( & format! ( "avril-builtin:/dependencies{}" , uri. path ())) ? 336} else { 337editor_uri ( & source. uri , extension) ? 338}; 339let directory =format! ( "/avril-lsp/live/{}" , modules. len ()); 340let file =format! ( "{directory}/source{extension}" ); 341if files. insert ( source. uri . clone (), file. clone ()). is_some () { 342return Err ( "Duplicate module identity" . into ()); 343} 344 host. contents . insert ( file. clone (), source. text . clone ()); 345 bridge. insert ( editor_uri. clone (), & file, source. read_only ) ?; 346if modules 347. insert ( editor_uri. clone (), Module { source, file}) 348. is_some () 349{ 350return Err ( "Duplicate editor URI" . into ()); 351} 352} 353for ( kind, references) in [ 354( "type" , input. type_references ), 355( "path" , input. path_references ), 356] { 357for (( referrer, reference), target) in references{ 358let referrer =canonical_uri ( & referrer) ?; 359let target =canonical_uri ( & target) ?; 360let containing_file = files 361. get ( & referrer) 362. ok_or_else ( ||format! ( "Unknown {kind}-reference referrer: {referrer}" )) ?; 363let target_file = files 364. get ( & target) 365. ok_or_else ( ||format! ( "Unknown {kind}-reference target: {target}" )) ? 366. clone (); 367let directory =Path :: new ( containing_file) 368. parent () 369. ok_or ( "Invalid virtual source path" ) ?; 370let alias =if kind =="type" { 371 directory 372. join ( "node_modules/@types" ) 373. join ( type_package_name ( & reference) ?) 374. join ( "index.d.ts" ) 375} else { 376 directory. join ( reference) 377}; 378insert_path_alias ( & mut host, lexical_path ( & alias), target_file) ?; 379} 380} 381validate_virtual_paths ( & mut host) ?; 382let diagnostics = input 383. diagnostics 384. into_iter () 385. map ( |( uri, messages) |{ 386let canonical =canonical_uri ( & uri) ?; 387let editor = modules 388. iter () 389. find ( |( _, module) | module. source . uri == canonical) 390. map ( |( uri, _) | uri. clone ()) 391. map_or_else ( 392 ||{ 393if canonical. starts_with ( "data:" ) { 394Ok ( canonical) 395} else { 396editor_uri ( & uri, ".ts" ) 397} 398}, 399Ok , 400) ?; 401Ok (( editor, messages)) 402}) 403. collect ::< Result < _ >>() ?; 404let mut resolutions =Vec :: new (); 405let mut module_specifiers =Vec :: new (); 406// Keep existing import spellings ahead of require-only spellings for auto-imports. 407for modein [ None , Some ( 1 )] { 408for (( from, specifier), targets) in & input. imports { 409let target =if mode. is_some () { 410& targets. require 411} else { 412& targets. import 413}; 414let Some ( target) = targetelse { continue }; 415let from =canonical_uri ( from) ?; 416let file = files. get ( & from). ok_or ( "Unknown import referrer" ) ?; 417let directory =Path :: new ( file). parent (). ok_or ( "Invalid compiler path" ) ?; 418let target =canonical_uri ( target) ?; 419let target = files. get ( & target). ok_or ( "Unknown import target" ) ?; 420let mut entry =json! ({ 421"moduleName" : specifier, 422"containingDirectory" : directory, 423"result" : { "resolvedFileName" : target}, 424}); 425// TypeScript's CommonJS mode overrides the default import edge. 426if let Some ( mode) = mode{ 427 entry[ "resolutionMode" ] =json! ( mode); 428} 429 resolutions. push ( entry); 430 module_specifiers. push ( json! ({ "from" : file, "to" : target, "specifier" : specifier})); 431} 432} 433for ( from, target, specifier) in input. import_candidates { 434let from =canonical_uri ( & from) ?; 435let target =canonical_uri ( & target) ?; 436let from = files 437. get ( & from) 438. ok_or ( "Unknown import candidate referrer" ) ?; 439let target = files 440. get ( & target) 441. ok_or ( "Unknown import candidate target" ) ?; 442 module_specifiers. push ( json! ({ "from" : from, "to" : target, "specifier" : specifier})); 443} 444let backend =Backend :: start ( & binary, Arc :: new ( host)). await ?; 445let mut result =Self { 446 backend, 447 modules, 448 bridge, 449 module_specifiers, 450snapshot : 0 , 451projects : BTreeMap :: new (), 452 diagnostics, 453resolver : 0 , 454}; 455let init = result 456. initialize ( 457 input. compiler_options , 458 input. per_file_options , 459 globals, 460 resolutions, 461) 462. await ; 463if let Err ( error) = init{ 464let _ = result. close (). await ; 465return Err ( error); 466} 467Ok ( result) 468} 469async fn initialize ( 470& mut self , 471options : Value , 472per_file : BTreeMap < String , Value >, 473global_uris : BTreeSet < String >, 474resolutions : Vec < Value >, 475) ->Result <()> { 476self . backend . call ::< Value >( "initialize" , json! ({})). await ?; 477let mut groups: BTreeMap < String , ( Value , Vec < String >)> =BTreeMap :: new (); 478for modulein self . modules . values () { 479let mut settings = options. clone (); 480if let Some ( scoped) = per_file. get ( & module. source . uri ) { 481 settings 482. as_object_mut () 483. ok_or ( "Compiler options must be an object" ) ? 484. extend ( 485 scoped 486. as_object () 487. ok_or ( "Compiler options must be an object" ) ? 488. clone (), 489); 490} 491 groups 492. entry ( settings. to_string ()) 493. or_insert_with ( ||( settings, Vec :: new ())) 494. 1 495. push ( module. file . clone ()); 496} 497let builtins: Vec < _ > =self 498. modules 499. values () 500. filter ( |m| m. source . uri . starts_with ( "avril:" )) 501. map ( |m| m. file . clone ()) 502. collect (); 503let globals: Vec < _ > =self 504. modules 505. values () 506. filter ( |module| global_uris. contains ( & module. source . uri )) 507. map ( |module| module. file . clone ()) 508. collect (); 509let mut programs =Vec :: new (); 510let mut program_files =Vec :: new (); 511for ( settings, mut roots) in groups. into_values () { 512let parsed: Value =self 513. backend 514. call ( 515"parseJsonConfigFileContent" , 516json! ({ 517"configDirectory" : "/avril-lsp" , 518"json" : { "compilerOptions" : settings, "files" : roots} 519}), 520) 521. await ?; 522if parsed[ "errors" ]. as_array (). is_none_or ( |a| !a. is_empty ()) { 523return Err ( format! ( "Invalid compiler options: {}" , parsed[ "errors" ]). into ()); 524} 525// Custom resolution is authoritative, independently of compiler options. 526if self . resolver ==0 { 527self . resolver =self 528. backend 529. call ( 530"createModuleResolver" , 531json! ({ 532"compilerOptions" : parsed[ "options" ], 533"moduleResolutions" : { "fallback" : "unresolved" , "entries" : resolutions} 534}), 535) 536. await ?; 537} 538 program_files. push ( roots. clone ()); 539 roots. extend ( 540 builtins 541. iter () 542. chain ( & globals) 543. filter ( |file| !roots. contains ( file)) 544. cloned () 545. collect ::< Vec < _ >>(), 546); 547 programs. push ( json! ({ "rootFiles" : roots, "compilerOptions" : parsed[ "options" ], "options" : { "moduleResolver" : self . resolver }})); 548} 549let contents: BTreeMap < _ , _ > =self 550. modules 551. values () 552. map ( |m|( & m. file , & m. source . text )) 553. collect (); 554let snapshot: Snapshot =self 555. backend 556. call ( 557"createSnapshot" , 558json! ({ 559"fileSystem" : { "kind" : "layer" , "files" : contents}, 560"createPrograms" : programs, 561"userPreferences" : { 562"includeCompletionsForModuleExports" : true , 563"includeCompletionsForImportStatements" : true , 564"includeAutomaticOptionalChainCompletions" : true , 565"includeCompletionsWithInsertText" : true , 566"includeCompletionsWithSnippetText" : true , 567"includeCompletionsWithClassMemberSnippets" : true , 568"includeCompletionsWithObjectLiteralMethodSnippets" : true , 569"jsxAttributeCompletionStyle" : "auto" 570} 571}), 572) 573. await ?; 574self . snapshot = snapshot. snapshot ; 575if snapshot. operation . created_programs . len () != program_files. len () { 576return Err ( "Backend did not create all projects" . into ()); 577} 578for ( files, project) in program_files 579. into_iter () 580. zip ( snapshot. operation . created_programs ) 581{ 582for filein files{ 583self . projects . insert ( file, project. clone ()); 584} 585} 586Ok (()) 587} 588fn params ( & self , uri : & str , mut fields : Value ) ->Value { 589 fields[ "snapshot" ] =json! ( self . snapshot ); 590 fields[ "project" ] =json! ( self . projects [ & self . modules [ uri]. file ]); 591 fields 592} 593pub fn contains ( & self , uri : & str ) ->bool { 594self . bridge . contains ( uri) 595} 596pub fn is_read_only ( & self , uri : & str ) ->bool { 597self . modules . iter (). any ( |( editor, module) |{ 598( editor == uri || module. source . uri == uri) && module. source . read_only 599}) || uri. starts_with ( "avril-builtin:/typescript/" ) 600} 601pub fn read_document ( & self , uri : & str ) ->Result < & str > { 602let m =self 603. modules 604. get ( uri) 605. filter ( |m| m. source . read_only ) 606. ok_or ( "Unknown read-only document" ) ?; 607Ok ( & m. source . text ) 608} 609pub fn error_uris ( & self ) ->impl Iterator < Item =& String > { 610self . diagnostics . keys () 611} 612async fn diagnostic_list ( & self , uri : & str ) ->Result < Vec < Diagnostic >> { 613let original =self 614. modules 615. get ( uri) 616. map_or ( uri, |module| module. source . uri . as_str ()); 617let mut diagnostics: Vec < _ > =self 618. diagnostics 619. get ( uri) 620. into_iter () 621. flatten () 622. map ( |message|Diagnostic { 623uri : original. to_owned (), 624line : 0 , 625character : 0 , 626end_line : 0 , 627end_character : 0 , 628severity : 1 , 629code : None , 630source : "avril-resolver" . into (), 631message : message. clone (), 632}) 633. collect (); 634let Some ( module) =self . modules . get ( uri) else { 635return Ok ( diagnostics); 636}; 637for methodin [ "getSyntacticDiagnostics" , "getSemanticDiagnostics" ] { 638let values: Vec < Value > =self 639. backend 640. call ( method, self . params ( uri, json! ({ "files" :[ module. file ]}))) 641. await ?; 642for mut din values{ 643self . bridge . display ( & mut d); 644if !d[ "startPosition" ]. is_null () && !d[ "endPosition" ]. is_null () { 645 diagnostics. push ( Diagnostic { 646uri : original. to_owned (), 647line : d[ "startPosition" ][ "line" ]. as_u64 (). unwrap_or_default () as usize , 648character : d[ "startPosition" ][ "character" ]. as_u64 (). unwrap_or_default () 649as usize , 650end_line : d[ "endPosition" ][ "line" ]. as_u64 (). unwrap_or_default () as usize , 651end_character : d[ "endPosition" ][ "character" ]. as_u64 (). unwrap_or_default () 652as usize , 653severity : if d[ "category" ] ==1 { 1 } else { 2 }, 654code : d[ "code" ]. as_u64 (), 655source : "avril" . into (), 656message : { 657let mut message = d[ "text" ]. as_str (). unwrap_or_default (). to_owned (); 658let mut pending =vec! [( & d, 0 )]; 659while let Some (( diagnostic, depth)) = pending. pop () { 660if depth >0 { 661 message. push_str ( & format! ( 662"\n{}{}" , 663" " . repeat ( depth), 664 diagnostic[ "text" ]. as_str (). unwrap_or_default () 665)); 666} 667if let Some ( chain) = diagnostic[ "messageChain" ]. as_array () { 668 pending 669. extend ( chain. iter (). rev (). map ( |item|( item, depth +1 ))); 670} 671} 672 message 673}, 674}); 675} 676} 677} 678Ok ( diagnostics) 679} 680pub async fn diagnostics ( 681& mut self , 682uri : & str , 683client : & Value , 684preferences : & Value , 685) ->Result < Value > { 686let mut values =if self . modules . contains_key ( uri) { 687let mut report =self 688. language_service ( 689"textDocument/diagnostic" , 690json! ({ "textDocument" :{ "uri" : uri}}), 691 client, 692 preferences, 693) 694. await ?; 695 report[ "items" ] 696. take () 697. as_array () 698. cloned () 699. ok_or ( "Invalid native diagnostics" ) ? 700} else { 701Vec :: new () 702}; 703for messagein self . diagnostics . get ( uri). into_iter (). flatten () { 704 values. push ( json! ({ 705"range" :{ "start" :{ "line" : 0 , "character" : 0 }, "end" :{ "line" : 0 , "character" : 0 }}, 706"severity" : 1 , "source" : "avril-resolver" , "message" : message, 707})); 708} 709Ok ( json! ( values)) 710} 711pub async fn all_diagnostics ( & self ) ->Result < Vec < Diagnostic >> { 712// Resolver diagnostics can belong to modules whose sources were 713// unavailable, so they have no entry in the compiler program. 714let uris: BTreeSet < _ > =self . modules . keys (). chain ( self . diagnostics . keys ()). collect (); 715let mut diagnostics =Vec :: new (); 716for uriin uris{ 717 diagnostics. extend ( self . diagnostic_list ( uri). await ?); 718} 719Ok ( diagnostics) 720} 721pub async fn capabilities ( & self , client : & Value ) ->Result < Capabilities > { 722let result: Capabilities =self . backend . call ( "avril/lspCapabilities" , json! ({ "capabilities" : client})). await 723. map_err ( |error|format! ( "The language server requires Avril's patched TypeScript backend; see lib/lsp/typescript/README.md: {error}" )) ?; 724if result. version !=1 { 725return Err ( format! ( 726"Unsupported Avril TypeScript bridge version: {}" , 727 result. version 728) 729. into ()); 730} 731Ok ( result) 732} 733 734pub async fn language_service ( 735& mut self , 736method : & str , 737mut params : Value , 738client : & Value , 739preferences : & Value , 740) ->Result < Value > { 741// VS Code sends null for absent signature-context fields; the pinned 742// native decoder requires optional fields to be omitted instead. 743if method =="textDocument/signatureHelp" 744 &&let Some ( context) = params. get_mut ( "context" ). and_then ( Value :: as_object_mut) 745{ 746 context. retain ( |key, value|{ 747 !matches! ( key. as_str (), "triggerCharacter" |"activeSignatureHelp" ) 748 || !value. is_null () 749}); 750} 751let context = params 752. pointer ( "/textDocument/uri" ) 753. and_then ( Value :: as_str) 754. map ( canonical_uri) 755. transpose () ?; 756if matches! ( 757 method, 758"textDocument/completion" |"textDocument/codeAction" 759) && context. as_ref (). is_some_and ( |uri|self . is_read_only ( uri)) 760{ 761return Ok ( if method =="textDocument/codeAction" { 762json! ([]) 763} else { 764Value :: Null 765}); 766} 767if matches! ( 768 method, 769"textDocument/formatting" 770 |"textDocument/rangeFormatting" 771 |"textDocument/onTypeFormatting" 772 |"textDocument/rename" 773 |"textDocument/prepareRename" 774) { 775self . bridge 776. writable ( context. as_deref (). ok_or ( "Missing document URI" ) ?) ?; 777} 778let project =if let Some ( project) =self . bridge . restore ( method, & mut params) ?{ 779 project 780} else { 781self . bridge . input ( & mut params) ?; 782 context 783. as_ref () 784. and_then ( |uri|self . modules . get ( uri)) 785. and_then ( |module|self . projects . get ( & module. file )) 786. or_else ( ||self . projects . values (). next ()) 787. cloned () 788. unwrap_or_default () 789}; 790let projects: BTreeSet < _ > =self . projects . values (). collect (); 791if method =="avril/readDocument" { 792return self 793. backend 794. call ( 795 method, 796json! ({ 797"snapshot" : self . snapshot , "project" : project, "projects" : projects, 798"textDocument" : params[ "textDocument" ], 799}), 800) 801. await ; 802} 803let mut result =self 804. backend 805. call ( 806"avril/lsp" , 807json! ({ 808"snapshot" : self . snapshot , "project" : project, "projects" : projects, 809"method" : method, "params" : params, "capabilities" : client, 810"moduleSpecifiers" : self . module_specifiers , "preferences" : preferences, 811}), 812) 813. await ?; 814self . bridge . output ( method, & project, & mut result) ?; 815Ok ( result) 816} 817 818pub fn failed ( & self ) ->bool { 819self . backend . failed () 820} 821 822pub async fn terminate ( & mut self ) ->Result <()> { 823self . backend . close (). await 824} 825pub async fn close ( & mut self ) ->Result <()> { 826let release = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 1 ), async { 827if self . snapshot !=0 { 828self . backend 829. call ::< Value >( "release" , json! ({ "snapshot" : self . snapshot })) 830. await ?; 831self . snapshot =0 ; 832} 833if self . resolver !=0 { 834self . backend 835. call ::< Value >( "releaseModuleResolver" , json! ({ "resolver" : self . resolver })) 836. await ?; 837self . resolver =0 ; 838} 839Ok ::< _ , crate :: Error >(()) 840}) 841. await 842. unwrap_or_else ( |_|Err ( "Timed out releasing TypeScript handles" . into ())); 843let close =self . backend . close (). await ; 844 release. and ( close) 845} 846} 847 848# [ cfg ( test )] 849mod tests{ 850use super :: * ; 851fn new_host () ->Host { 852Host { 853contents : BTreeMap :: new (), 854aliases : BTreeMap :: new (), 855} 856} 857 858# [ test ] 859fn virtual_paths_reject_alias_and_directory_collisions () { 860let mut host =new_host (); 861insert_path_alias ( 862& mut host, 863"/virtual/shared.d.ts" . into (), 864"/target/a.d.ts" . into (), 865) 866. unwrap (); 867assert! ( 868insert_path_alias ( 869& mut host, 870"/virtual/shared.d.ts" . into (), 871"/target/b.d.ts" . into () 872) 873. is_err () 874); 875 876let mut host =new_host (); 877 host. contents 878. insert ( "/virtual/source.d.ts" . into (), "" . into ()); 879insert_path_alias ( 880& mut host, 881"/virtual/source.d.ts" . into (), 882"/target.d.ts" . into (), 883) 884. unwrap (); 885assert! ( validate_virtual_paths ( & mut host). is_err ()); 886 887let mut host =new_host (); 888insert_path_alias ( 889& mut host, 890"/virtual/node_modules/@types/x/index.d.ts/child.d.ts" . into (), 891"/target.d.ts" . into (), 892) 893. unwrap (); 894insert_path_alias ( 895& mut host, 896"/virtual/node_modules/@types/x/index.d.ts" . into (), 897"/other.d.ts" . into (), 898) 899. unwrap (); 900assert! ( validate_virtual_paths ( & mut host). is_err ()); 901 902let mut host =new_host (); 903 host. contents 904. insert ( "/virtual/source.d.ts" . into (), "" . into ()); 905insert_path_alias ( 906& mut host, 907"/virtual/source.d.ts" . into (), 908"/virtual/source.d.ts" . into (), 909) 910. unwrap (); 911validate_virtual_paths ( & mut host). unwrap (); 912assert! ( host. aliases . is_empty ()); 913} 914 915# [ test ] 916fn type_reference_names_are_safe_single_package_names () { 917assert_eq! ( type_package_name ( "node" ). unwrap (), "node" ); 918assert_eq! ( type_package_name ( "@scope/pkg" ). unwrap (), "scope__pkg" ); 919assert_eq! ( type_package_name ( "@types/foo" ). unwrap (), "types__foo" ); 920for invalidin [ 921"" , 922"/absolute" , 923"../escape" , 924"foo/bar" , 925"@scope/../x" , 926"@scope/a/b" , 927"bad\\name" , 928"bad\nname" , 929] { 930assert! ( type_package_name ( invalid). is_err (), "{invalid:?}" ); 931} 932} 933 934# [ test ] 935fn virtual_reference_filesystem_does_not_read_host_paths () { 936let host =new_host (); 937let exists = host. call ( "fileExists" , json! ( "/etc/passwd" )). unwrap (); 938assert_eq! ( exists, json! ({ "kind" : "value" , "value" : false })); 939let contents = host. call ( "readFile" , json! ( "/etc/passwd" )). unwrap (); 940assert_eq! ( contents, json! ({ "kind" : "missing" })); 941} 942 943# [ test ] 944fn editor_uris_preserve_transport_and_builtin_identity () { 945assert_eq! ( 946editor_uri ( "https://example.test/mod.ts" , ".ts" ). unwrap (), 947"avril-https://example.test/mod.ts" 948); 949assert_eq! ( 950editor_uri ( "http://example.test/mod.ts" , ".ts" ). unwrap (), 951"avril-http://example.test/mod.ts" 952); 953assert_eq! ( 954editor_uri ( "avril:/runtime/stdlib/std/sync.d.ts" , ".d.ts" ). unwrap (), 955"avril-builtin:/runtime/stdlib/std/sync.d.ts" 956); 957assert_eq! ( 958editor_uri ( "file:///app/main.ts" , ".ts" ). unwrap (), 959"file:///app/main.ts" 960); 961assert! ( editor_uri ( "npm:unsupported" , ".ts" ). is_err ()); 962} 963# [ tokio :: test ] 964async fn references_and_globals_resolve_through_canonical_virtual_files () { 965let Ok ( binary) = std:: env:: var ( "AVRIL_TEST_TYPESCRIPT" ) else { 966return ; 967}; 968let uri = |path : & str |format! ( "file:///project/{path}" ); 969let shared =uri ( "types/shared.d.cts" ); 970let shared_esm =uri ( "types/shared.d.mts" ); 971let path_types =uri ( "types/path.d.ts" ); 972let first =uri ( "src/first.ts" ); 973let second =uri ( "src/second.ts" ); 974let unbound =uri ( "src/unbound.ts" ); 975let global =uri ( "types/global.d.ts" ); 976let input =ProjectInput { 977script_kinds : BTreeMap :: new (), 978sources : vec! [ 979Source { 980uri : shared. clone (), 981text : "import type {Mode} from 'mode'; declare global { const sharedOnce: unique symbol; const requireMode: Mode; }" . into (), 982read_only : true , 983}, 984Source { 985uri : shared_esm. clone (), 986text : "import type {Mode} from 'mode'; declare global { const importMode: Mode; }" . into (), 987read_only : true , 988}, 989Source { 990uri : uri ( "types/import.d.ts" ), 991text : "export type Mode = 'import';" . into (), 992read_only : true , 993}, 994Source { 995uri : uri ( "types/require.d.ts" ), 996text : "export type Mode = 'require';" . into (), 997read_only : true , 998}, 999Source { 1000uri : path_types. clone (), 1001text : "interface FromPath { value: string }" . into (), 1002read_only : true , 1003}, 1004Source { 1005uri : first. clone (), 1006text : "/// <reference types=\"shared\" />\n/// <reference types=\"shared-esm\" />\n/// <reference path=\"../types/path.d.ts\" />\nconst a: FromPath = { value: \"ok\" }; const s: typeof sharedOnce = sharedOnce; const c: 'require' = requireMode; const e: 'import' = importMode;" . into (), 1007read_only : false , 1008}, 1009Source { 1010uri : second. clone (), 1011text : "const b: typeof sharedOnce = sharedOnce; const g: string = globalValue;" . into (), 1012read_only : false , 1013}, 1014Source { 1015uri : unbound. clone (), 1016text : "/// <reference types=\"shared\" />\nimport type {Mode} from 'shared';" . into (), 1017read_only : false , 1018}, 1019Source { 1020uri : global. clone (), 1021text : "declare const globalValue: string;" . into (), 1022read_only : true , 1023}, 1024], 1025compiler_options : json! ({ "strict" : true , "noEmit" : true , "module" : "preserve" , "moduleResolution" : "bundler" }), 1026imports : [ shared. clone (), shared_esm. clone ()]. into_iter (). map ( |from|{ 1027(( from, "mode" . into ()), ResolvedImport { 1028import : Some ( uri ( "types/import.d.ts" )), 1029require : Some ( uri ( "types/require.d.ts" )), 1030}) 1031}). collect (), 1032import_candidates : Vec :: new (), 1033per_file_options : BTreeMap :: from ([( second. clone (), json! ({ "strictNullChecks" : false }))]), 1034diagnostics : BTreeMap :: from ([( second. clone (), vec! [ "Unresolved explicit reference" . into ()])]), 1035type_references : BTreeMap :: from ([ 1036(( first. clone (), "shared" . into ()), shared. clone ()), 1037(( first. clone (), "shared-esm" . into ()), shared_esm), 1038(( unbound. clone (), "shared" . into ()), shared), 1039]), 1040path_references : BTreeMap :: from ([(( first. clone (), "../types/path.d.ts" . into ()), path_types)]), 1041globals : BTreeSet :: from ([ global]), 1042}; 1043let diagnostics =crate :: check ( Path :: new ( & binary), input). await . unwrap (); 1044assert! ( 1045 diagnostics 1046. iter () 1047. any ( |diagnostic| diagnostic. source =="avril-resolver" ) 1048); 1049assert! ( 1050 diagnostics 1051. iter () 1052. any ( |diagnostic|{ diagnostic. uri == second && diagnostic. code ==Some ( 2304 ) }), 1053"reference-scoped declaration leaked: {diagnostics:#?}" 1054); 1055assert! ( 1056 diagnostics 1057. iter () 1058. any ( |diagnostic| diagnostic. uri == unbound && diagnostic. code ==Some ( 2307 )), 1059"a type-reference binding must not authorize a module import: {diagnostics:#?}" 1060); 1061assert! ( 1062 diagnostics 1063. iter () 1064. filter ( |diagnostic| diagnostic. source =="avril" 1065 && diagnostic. uri != second 1066 && !( diagnostic. uri == unbound && diagnostic. code ==Some ( 2307 ))) 1067. all ( |diagnostic| diagnostic. severity !=1 ), 1068"{diagnostics:#?}" 1069); 1070} 1071 1072# [ test ] 1073fn remote_queries_survive_editor_uri_encoding_without_collapsing_delimiters () { 1074let uri =editor_uri ( "https://example.test/mod.ts?target=deno&dev" , ".ts" ). unwrap (); 1075assert_eq! ( uri, "avril-https://example.test/mod.ts?target%3Ddeno%26dev" ); 1076assert_eq! ( canonical_uri ( & uri). unwrap (), uri); 1077assert_eq! ( 1078canonical_uri ( "avril-https://example.test/mod.ts?target=deno&dev" ). unwrap (), 1079 uri 1080); 1081assert_ne! ( 1082editor_uri ( "https://example.test/mod.ts?target%3Ddeno%26dev" , ".ts" ). unwrap (), 1083 uri 1084); 1085} 1086 1087# [ test ] 1088fn uri_normalization_preserves_segments () { 1089assert_eq! ( 1090canonical_uri ( "avril-builtin:///runtime/stdlib/std/sync.d.ts" ). unwrap (), 1091canonical_uri ( "avril-builtin:/runtime/stdlib/std/sync.d.ts" ). unwrap () 1092); 1093assert_eq! ( 1094canonical_uri ( "avril-https://jsr.io/@example/mod.d.ts" ). unwrap (), 1095canonical_uri ( "avril-https://jsr.io/%40example/mod.d.ts" ). unwrap () 1096); 1097assert_ne! ( 1098canonical_uri ( "file:///a%2fb.ts" ). unwrap (), 1099canonical_uri ( "file:///a/b.ts" ). unwrap () 1100); 1101} 1102}