char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1c8ffed
main
1# [ cfg ( test )] 2mod tests; 3 4use avril_core:: ModuleLoader ; 5use avril_lsp::{ Documents , ProjectInput , ProjectService , Refresh , ScriptKind , Source }; 6use avril_runtime::{ caps:: Capabilities , registry:: Options }; 7use notify:: Watcher ; 8use serde_json:: json; 9use std::{ 10 collections::{ BTreeMap , BTreeSet }, 11 path::{ Path , PathBuf }, 12 sync::{ 13Arc , Mutex , 14 atomic::{ AtomicBool , Ordering }, 15}, 16}; 17 18use crate :: cli:: ParseError ; 19use crate :: fragments::{ Arg , reject}; 20 21struct LiveProject { 22root : PathBuf , 23caps : Arc < Capabilities >, 24options : Options , 25explicit_lock : bool , 26automatic_types : bool , 27dirty : Arc < AtomicBool >, 28rewatch : Arc < AtomicBool >, 29watcher : Option < notify:: RecommendedWatcher >, 30watched : BTreeMap < PathBuf , bool >, 31tracked_config : Arc < Mutex < BTreeSet < PathBuf >>>, 32} 33impl LiveProject { 34fn watch ( & mut self , path : & Path , recursive : bool ) { 35let Some ( watcher) =& mut self . watcher else { 36return ; 37}; 38if self 39. watched 40. get ( path) 41. is_some_and ( |current|* current || !recursive) 42 ||self 43. watched 44. iter () 45. any ( |( root, recursive) |* recursive && path. starts_with ( root)) 46{ 47return ; 48} 49if watcher 50. watch ( 51 path, 52if recursive{ 53 notify:: RecursiveMode :: Recursive 54} else { 55 notify:: RecursiveMode :: NonRecursive 56}, 57) 58. is_ok () 59{ 60self . watched . insert ( path. to_owned (), recursive); 61} 62} 63} 64impl ProjectService for LiveProject { 65fn changed ( & mut self ) ->bool { 66let dirty =self . dirty . swap ( false , Ordering :: AcqRel ); 67if self . rewatch . swap ( false , Ordering :: AcqRel ) { 68// A watch follows an inode, not a directory recreated at the same path. 69for ( path, recursive) in std:: mem:: take ( & mut self . watched ) { 70if let Some ( watcher) =& mut self . watcher { 71let _ = watcher. unwatch ( & path); 72} 73self . watch ( & path, recursive); 74} 75} 76 dirty 77} 78fn refresh ( & mut self , documents : Documents ) ->Refresh < ' _ > { 79Box :: pin ( async move { self . input ( documents, & []). await }) 80} 81} 82 83impl LiveProject { 84fn input ( & mut self , documents : Documents , extra_roots : & [ String ]) ->Refresh < ' _ > { 85let extra_roots = extra_roots. to_vec (); 86Box :: pin ( async move { 87if self . caps . unrestricted_js_exec () { 88self . watch ( & self . root . clone (), true ); 89} 90let authority = avril_runtime:: authority:: SourceAuthority :: execute ( self . caps . clone ()); 91let anchor =self . root . join ( ".avril-lsp.js" ); 92// Broken configs are reported on their modules by the type collector; 93// they must not prevent unrelated open documents from being checked. 94let root = avril_runtime:: modules:: ModuleGraph :: project_root_for_repl ( 95& authority, 96 anchor. to_str (). ok_or ( "Project path must be UTF-8" ) ?, 97) 98. unwrap_or_else ( |_|self . root . clone ()); 99let mut options =self . options . clone (); 100if !self . explicit_lock { 101 options. lock = root. join ( "avril.lock" ); 102} 103let lock_path = options. lock . clone (); 104 options. project_root = root. clone (); 105if self . caps . unrestricted_js_exec () { 106if let Some ( parent) = options. lock . ancestors (). skip ( 1 ). find ( |p| p. is_dir ()) { 107self . watch ( parent, false ); 108} 109if let Some ( vendor) =& options. vendor { 110self . watch ( vendor, true ); 111} 112} 113let discovered =if extra_roots. is_empty () &&self . caps . unrestricted_js_exec () { 114let workspace =self . root . clone (); 115let cache = options. cache . clone (); 116let vendor = options. vendor . clone (); 117 tokio:: task:: spawn_blocking ( move || -> avril_lsp:: Result < Vec < String >> { 118let mut sources =Vec :: new (); 119for entryin ignore:: WalkBuilder :: new ( workspace) 120. require_git ( false ) 121. filter_entry ( move |entry|{ 122 entry. path () != cache 123 && vendor. as_ref (). is_none_or ( |path| entry. path () != path) 124 && entry. file_name () !="node_modules" 125}) 126. build () 127{ 128let entry = entry?; 129if entry. file_type (). is_some_and ( |kind| kind. is_file ()) 130 &&matches! ( 131 entry. path (). extension (). and_then ( |s| s. to_str ()), 132Some ( "ts" |"tsx" |"js" |"jsx" |"mts" |"cts" |"mjs" |"cjs" ) 133) 134{ 135 sources. push ( 136 entry 137. path () 138. to_str () 139. ok_or ( "Module path must be UTF-8" ) ? 140. to_owned (), 141); 142} 143} 144 sources. sort (); 145Ok ( sources) 146}) 147. await ?? 148} else { 149Vec :: new () 150}; 151let graph = avril_runtime:: modules:: ModuleGraph :: open ( 152 tokio:: runtime:: Handle :: current (), 153 authority, 154 options, 155) ?; 156 graph. set_automatic_types ( self . automatic_types ); 157 graph. set_typescript_version ( avril_lsp:: BACKEND_VERSION ) ?; 158let mut overlays =BTreeMap :: new (); 159for ( uri, text) in documents{ 160let path = url:: Url :: parse ( & uri) ? 161. to_file_path () 162. map_err ( |_|"Only local source documents can be edited" ) ?; 163 overlays. insert ( 164 path. to_str (). ok_or ( "Module path must be UTF-8" ) ?. to_owned (), 165 text, 166); 167} 168let mut roots: Vec < _ > = overlays. keys (). cloned (). collect (); 169 roots. extend ( extra_roots. iter (). cloned ()); 170 roots. extend ( discovered); 171let types = graph. type_graph ( & roots, & overlays). await ?; 172let mut tracked = types. config_inputs . clone (); 173 tracked. insert ( lock_path); 174 tracked. extend ( 175 types 176. sources 177. keys () 178. filter ( |name| graph. is_workspace_source ( name)) 179. map ( PathBuf :: from), 180); 181* self . tracked_config . lock (). unwrap () = tracked; 182let uri = |name : & str | -> avril_lsp:: Result < String > { 183 avril_lsp:: canonical_uri ( & graph. module_url ( name). ok_or ( "Invalid module identity" ) ?) 184}; 185let mut input =ProjectInput { 186script_kinds : BTreeMap :: new (), 187sources : crate :: stdlib:: declarations () 188. map ( |( name, text) |Source { 189uri : format! ( "avril:/runtime/stdlib/{name}" ), 190text : text. into (), 191read_only : true , 192}) 193. collect (), 194// Dynamic import must remain import-mode even in CommonJS files. 195compiler_options : json! ({ "strict" : true , "noEmit" : true , "skipLibCheck" : true , "target" : "esnext" , "module" : "preserve" , "moduleResolution" : "bundler" , "moduleDetection" : "force" , "allowImportingTsExtensions" : true , "allowJs" : true , "types" :[], "jsx" : "react-jsx" }), 196imports : BTreeMap :: new (), 197import_candidates : Vec :: new (), 198per_file_options : BTreeMap :: new (), 199diagnostics : BTreeMap :: new (), 200type_references : BTreeMap :: new (), 201path_references : BTreeMap :: new (), 202globals : BTreeSet :: new (), 203}; 204if self . caps . unrestricted_js_exec () { 205for pathin & types. config_inputs { 206if let Some ( parent) = path. ancestors (). skip ( 1 ). find ( |p| p. is_dir ()) { 207self . watch ( parent, false ); 208} 209} 210} 211for ( name, text) in & types. sources { 212let identity =uri ( name) ?; 213let local = graph. is_workspace_source ( name); 214if avril_runtime:: modules:: data_url ( name). is_some () { 215let kind =if types. source_types [ name]. is_typescript () { 216ScriptKind :: TypeScript 217} else { 218ScriptKind :: JavaScript 219}; 220 input. script_kinds . insert ( identity. clone (), kind); 221} 222 input. sources . push ( Source { 223uri : identity. clone (), 224text : text. clone (), 225read_only : !local, 226}); 227if let Some ( options) = types. compiler_options . get ( name) { 228 input. per_file_options . insert ( identity, json! ( options)); 229} 230if local &&self . caps . unrestricted_js_exec () { 231// Parent watches detect new/moved configs as well as source changes. 232for parentin Path :: new ( name). ancestors (). skip ( 1 ) { 233self . watch ( parent, false ); 234} 235} 236} 237for ( name, imports) in & types. edges { 238for (( mode, specifier), target) in imports{ 239if target. starts_with ( "avril:" ) { 240continue ; 241} // Ambient builtin declarations. 242let mut target = target; 243let mut seen =BTreeSet :: new (); 244while let Some ( next) = types. redirects . get ( target) { 245if !seen. insert ( target) || !types. sources . contains_key ( next) { 246break ; 247} 248 target = next; 249} 250if types. sources . contains_key ( target) { 251let targets = input 252. imports 253. entry (( uri ( name) ?, specifier. clone ())) 254. or_default (); 255if mode. is_require () { 256 targets. require =Some ( uri ( target) ?); 257} else { 258 targets. import =Some ( uri ( target) ?); 259} 260} 261} 262} 263for ( name, errors) in & types. unresolved { 264for ( specifier, error) in errors{ 265 input 266. diagnostics 267. entry ( uri ( name) ?) 268. or_default () 269. push ( format! ( "{specifier}: {error}" )); 270} 271} 272for errorsin [ 273& types. unavailable , 274& types. package_errors , 275& types. compiler_errors , 276] { 277for ( name, error) in errors{ 278 input 279. diagnostics 280. entry ( uri ( name) ?) 281. or_default () 282. push ( error. clone ()); 283} 284} 285 input. globals = types 286. globals 287. iter () 288. filter ( |name| types. sources . contains_key ( * name)) 289. map ( |name|uri ( name)) 290. collect ::< avril_lsp:: Result < _ >>() ?; 291for ( references, targets) in [ 292( & types. type_references , & mut input. type_references ), 293( & types. path_references , & mut input. path_references ), 294] { 295for (( name, reference), target) in references{ 296if types. sources . contains_key ( target) { 297 targets. insert (( uri ( name) ?, reference. clone ()), uri ( target) ?); 298} 299} 300} 301for (( from, specifier), target) in 302 graph. type_import_candidates ( & types, overlays. keys ()). await 303{ 304 input 305. import_candidates 306. push (( uri ( & from) ?, uri ( & target) ?, specifier)); 307} 308// Acquisition may populate the cache, but editor refreshes never write locks, 309// snapshots, tsconfig files, or application sources. 310Ok ( input) 311}) 312} 313} 314 315/// Builds the type-checking generation for one on-disk entry module, without 316/// editor overlays or filesystem watches. 317pub ( crate ) async fn check_input ( 318root : PathBuf , 319caps : Arc < Capabilities >, 320options : Options , 321explicit_lock : bool , 322automatic_types : bool , 323entry : & str , 324) -> avril_lsp:: Result < ProjectInput > { 325let mut project =LiveProject { 326 root, 327 caps, 328 options, 329 explicit_lock, 330 automatic_types, 331dirty : Arc :: new ( AtomicBool :: new ( false )), 332rewatch : Arc :: new ( AtomicBool :: new ( false )), 333watcher : None , 334watched : BTreeMap :: new (), 335tracked_config : Arc :: new ( Mutex :: new ( BTreeSet :: new ())), 336}; 337 project. input ( BTreeMap :: new (), & [ entry. to_owned ()]). await 338} 339 340pub struct Args { 341pub typescript : PathBuf , 342pub project : Option < String >, 343pub entry : crate :: fragments:: Entry , 344} 345 346/// `avril lsp` argument parsing failed. 347# [ derive ( Debug , thiserror :: Error )] 348pub enum LspParseError { 349# [ error ( transparent )] 350Generic ( # [ from ] ParseError ), 351# [ error ( "lsp requires --typescript=PATH to the native backend" )] 352MissingBackend , 353} 354 355pub fn parse ( args : & [ String ]) -> std:: result:: Result < Args , LspParseError > { 356let cwd =crate :: fragments:: working_dir () ?; 357let mut parser =crate :: fragments:: Parser :: new ( "lsp" , args. to_vec (), & cwd) ?; 358let ( mut typescript, mut project) =( None , None ); 359while let Some ( arg) = parser. next () ?{ 360match arg{ 361Arg :: Positional ( arg) => parser. command . check ( & arg). map_err ( ParseError :: Check ) ?, 362Arg :: Flag ( arg) if parser. path_once ( & arg, "--typescript" , & mut typescript) ? =>{} 363Arg :: Flag ( arg) if parser. path_once ( & arg, "--project" , & mut project) ? =>{} 364Arg :: Flag ( arg) =>return Err ( reject ( & arg, & []). into ()), 365} 366} 367let typescript = typescript. ok_or ( LspParseError :: MissingBackend ) ?; 368// The language server takes no entry module: open documents are the roots. 369let entry = parser. synthetic ( ".avril-lsp.js" ) ?; 370Ok ( Args { 371typescript : PathBuf :: from ( typescript), 372 project, 373 entry, 374}) 375} 376 377pub fn run ( args : Args ) ->Result <(), Box < dyn std:: error:: Error >> { 378let Args { 379typescript : binary, 380 project, 381 entry, 382} = args; 383let root = std:: fs:: canonicalize ( project. as_deref (). unwrap_or ( "." )) ?; 384if !root. is_dir () { 385return Err ( "--project must name a workspace directory" . into ()); 386} 387let dirty =Arc :: new ( AtomicBool :: new ( false )); 388let rewatch =Arc :: new ( AtomicBool :: new ( false )); 389let tracked_config =Arc :: new ( Mutex :: new ( BTreeSet ::< PathBuf >:: new ())); 390let mut ignore_builder = ignore:: gitignore:: GitignoreBuilder :: new ( & root); 391let _ = ignore_builder. add ( root. join ( ".gitignore" )); 392let _ = ignore_builder. add ( root. join ( ".ignore" )); 393let ignored = ignore_builder. build () ?; 394let watcher = notify:: recommended_watcher ({ 395let dirty = dirty. clone (); 396let rewatch = rewatch. clone (); 397let cache = 398 avril_runtime:: caps:: PathTarget :: for_creation ( & entry. packages . cache ) ?. into_path (); 399let tracked_config = tracked_config. clone (); 400let root = root. clone (); 401let mut ignored = ignored. clone (); 402move |event : notify:: Result < notify:: Event > |{ 403// Cache acquisition is an output of refreshing, not an input change. 404// Watch directories coarsely so arbitrary extends/lock filenames and 405// missing-file creation or directory renames cannot leave stale graphs. 406if event. map_or ( true , |e|{ 407// Reading ignore rules generates access events of its own. 408if matches! ( e. kind , notify:: EventKind :: Access ( _)) && !e. need_rescan () { 409return false ; 410} 411let configs = tracked_config. lock (). unwrap (); 412if e. paths 413. iter () 414. any ( |path| path ==& root. join ( ".gitignore" ) || path ==& root. join ( ".ignore" )) 415{ 416let mut builder = ignore:: gitignore:: GitignoreBuilder :: new ( & root); 417let _ = builder. add ( root. join ( ".gitignore" )); 418let _ = builder. add ( root. join ( ".ignore" )); 419if let Ok ( updated) = builder. build () { 420 ignored = updated; 421} 422} 423let relevant = e. need_rescan () 424 || e. paths . iter (). any ( |path|{ 425if configs. contains ( path) { 426return true ; 427} 428if path. starts_with ( & cache) 429 ||( path. starts_with ( & root) 430 && ignored 431. matched_path_or_any_parents ( path, path. is_dir ()) 432. is_ignore ()) 433 || path. strip_prefix ( & root). unwrap_or ( path). components (). any ( 434 |component|{ 435matches! ( 436 component. as_os_str (). to_str (), 437Some ( ".git" |".jj" |".hg" |".svn" |"node_modules" ) 438) 439}, 440) 441{ 442return false ; 443} 444if path. starts_with ( root. join ( ".avril" ). join ( "npm" )) { 445return false ; 446} 447let directory_event =matches! ( 448 e. kind , 449 notify:: EventKind :: Create ( notify:: event:: CreateKind :: Folder ) 450 | notify:: EventKind :: Remove ( notify:: event:: RemoveKind :: Folder ) 451 | notify:: EventKind :: Modify ( notify:: event:: ModifyKind :: Name ( _)) 452); 453if directory_event 454 &&( path. starts_with ( & root) 455 || configs. iter (). any ( |config| config. starts_with ( path))) 456{ 457return true ; 458} 459let name = path 460. file_name () 461. and_then ( |name| name. to_str ()) 462. unwrap_or_default (); 463if !path. starts_with ( & root) { 464return false ; 465} 466if matches! ( 467 name, 468"package.json" |"avril.conf" |"avril.lock" |".gitignore" |".ignore" 469) || name. starts_with ( "tsconfig" ) 470{ 471return true ; 472} 473matches! ( 474 path. extension (). and_then ( |extension| extension. to_str ()), 475Some ( "ts" |"tsx" |"js" |"jsx" |"mts" |"cts" |"mjs" |"cjs" ) 476) 477}); 478if relevant 479 &&( e. need_rescan () 480 ||matches! ( 481 e. kind , 482 notify:: EventKind :: Remove ( notify:: event:: RemoveKind :: Folder ) 483 | notify:: EventKind :: Modify ( notify:: event:: ModifyKind :: Name ( _)) 484)) 485{ 486 rewatch. store ( true , Ordering :: Release ); 487} 488 relevant 489}) { 490 dirty. store ( true , Ordering :: Release ); 491} 492} 493}) ?; 494let mut service =LiveProject { 495root : root. clone (), 496caps : entry. caps , 497options : entry. packages , 498explicit_lock : entry. explicit_lock , 499automatic_types : entry. automatic_types , 500 dirty, 501 rewatch, 502watcher : Some ( watcher), 503watched : BTreeMap :: new (), 504 tracked_config, 505}; 506if service. caps . unrestricted_js_exec () { 507 service. watch ( & root, true ); 508} 509let runtime = tokio:: runtime:: Builder :: new_multi_thread () 510. worker_threads ( 2 ) 511. enable_all () 512. build () ?; 513let result = runtime. block_on ( avril_lsp:: serve ( 514 tokio:: io:: stdin (), 515 tokio:: io:: stdout (), 516 binary, 517Box :: new ( service), 518)); 519// An editor can send exit without closing stdin; its blocking read cannot 520// be cancelled. Backend teardown is independent of that read. 521 runtime. shutdown_background (); 522 result. map_err ( |e| ->Box < dyn std:: error:: Error > { e}) 523}