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# [ path = "common/lsp.rs" ] 2# [ allow ( dead_code )] 3mod support; 4 5use std:: fs; 6use std:: io::{ BufReader , Cursor }; 7use std:: path:: PathBuf ; 8 9use serde_json::{ Value , json}; 10use support::{ Server , backend, hover_text, position_at, read_frame, selected, write_frame}; 11 12fn query ( uri : & str , source : & str , word : & str , last : bool ) ->Value { 13let offset =if last{ 14 source. rfind ( word). unwrap () 15} else { 16 source. find ( word). unwrap () 17} +1 ; 18json! ({ 19"textDocument" : { "uri" : uri}, 20"position" : position_at ( source, offset) 21}) 22} 23 24# [ test ] 25fn framing_survives_split_utf8_and_coalesced_messages () { 26let mut bytes =Vec :: new (); 27write_frame ( 28& mut bytes, 29& json! ({ "jsonrpc" : "2.0" , "id" : 1 , "result" : "π" }), 30) 31. unwrap (); 32write_frame ( 33& mut bytes, 34& json! ({ "jsonrpc" : "2.0" , "id" : 2 , "result" : "next" }), 35) 36. unwrap (); 37// A one-byte buffer forces every frame to arrive in pieces. 38let mut split =BufReader :: with_capacity ( 1 , Cursor :: new ( bytes. clone ())); 39let mut results =Vec :: new (); 40while let Some ( message) =read_frame ( & mut split). unwrap () { 41 results. push ( message[ "result" ]. clone ()); 42} 43assert_eq! ( results, [ json! ( "π" ), json! ( "next" )]); 44let mut coalesced =BufReader :: new ( Cursor :: new ( bytes)); 45let mut results =Vec :: new (); 46while let Some ( message) =read_frame ( & mut coalesced). unwrap () { 47 results. push ( message[ "result" ]. clone ()); 48} 49assert_eq! ( results, [ json! ( "π" ), json! ( "next" )]); 50} 51 52# [ test ] 53fn native_backend_through_real_lsp_messages () { 54let Some ( backend) =backend () else { 55eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 56return ; 57}; 58let dir = tempfile:: Builder :: new () 59. prefix ( "avril lsp # % ΓΌ-" ) 60. tempdir () 61. unwrap (); 62let root = dir. path (); 63let fixture =PathBuf :: from ( env! ( "CARGO_MANIFEST_DIR" )). join ( "../lib/lsp/tests/fixture" ); 64for namein [ 65"avril.conf" , 66"tsconfig.json" , 67"main.tsx" , 68"dep file.ts" , 69"a.ts" , 70"b.ts" , 71"scope-a.ts" , 72"scope-b.ts" , 73"jsx-runtime.d.ts" , 74"data.d.ts" , 75] { 76 fs:: write ( root. join ( name), fs:: read ( fixture. join ( name)). unwrap ()). unwrap (); 77} 78let mut server =Server :: start ( root, & backend, & [ "--offline" ]); 79let main = server. uri ( "main.tsx" ); 80let dep = server. uri ( "dep file.ts" ); 81let text = fs:: read_to_string ( root. join ( "main.tsx" )). unwrap (); 82let dep_text = fs:: read_to_string ( root. join ( "dep file.ts" )). unwrap (); 83 84let initialize = server 85. call ( 86"initialize" , 87json! ({ "capabilities" : { "textDocument" : { "definition" : { "linkSupport" : true }}}}), 88) 89. unwrap (); 90assert_eq! ( 91 initialize[ "capabilities" ][ "positionEncoding" ], 92json! ( "utf-16" ) 93); 94assert! ( initialize[ "capabilities" ]. get ( "renameProvider" ). is_some ()); 95assert! ( 96 initialize[ "capabilities" ] 97. pointer ( "/workspace/fileOperations" ) 98. is_none () 99); 100assert_eq! ( 101 initialize[ "capabilities" ][ "completionProvider" ][ "resolveProvider" ], 102json! ( true ) 103); 104assert! ( 105 initialize[ "capabilities" ] 106. get ( "signatureHelpProvider" ) 107. is_some () 108); 109 server. notify ( "initialized" , json! ({})); 110let unsupported = server 111. call ( "workspace/willRenameFiles" , json! ({ "files" : []})) 112. unwrap_err (); 113assert_eq! ( unsupported. code , -32601 ); 114 server. open_as ( "main.tsx" , & text, 1 , "typescriptreact" ); 115 116// Hover, aliases, shorthand, original URI and exact Unicode ranges. 117let hover = server 118. call ( 119"textDocument/hover" , 120query ( & main, & text, "localValue" , true ), 121) 122. unwrap (); 123assert! ( hover_text ( & hover). contains ( "localValue: 42" )); 124assert_eq! ( selected ( & text, & hover[ "range" ]), "localValue" ); 125let links = server 126. call ( 127"textDocument/definition" , 128query ( & main, & text, "localValue" , false ), 129) 130. unwrap (); 131assert_eq! ( links. as_array (). unwrap (). len (), 1 ); 132assert_eq! ( links[ 0 ][ "targetUri" ], json! ( dep)); 133assert_eq! ( 134selected ( & dep_text, & links[ 0 ][ "targetSelectionRange" ]), 135"value" 136); 137assert_eq! ( 138 links[ 0 ][ "targetSelectionRange" ][ "start" ], 139position_at ( & dep_text, dep_text. find ( "value" ). unwrap ()) 140); 141assert! ( !links. to_string (). contains ( "/avril-lsp/" )); 142 143// Original declaration aliases and read-only builtin declarations. 144let links = server 145. call ( 146"textDocument/definition" , 147query ( & main, & text, "answer" , false ), 148) 149. unwrap (); 150let declaration =& links[ 0 ]; 151assert_eq! ( declaration[ "targetUri" ], json! ( server. uri ( "data.d.ts" ))); 152let data_text = fs:: read_to_string ( root. join ( "data.d.ts" )). unwrap (); 153assert_eq! ( 154selected ( & data_text, & declaration[ "targetSelectionRange" ]), 155"answer" 156); 157let links = server 158. call ( "textDocument/definition" , query ( & main, & text, "args" , true )) 159. unwrap (); 160let builtin =& links[ 0 ]; 161let builtin_uri = builtin[ "targetUri" ]. as_str (). unwrap (); 162assert! ( 163 builtin_uri. ends_with ( "/runtime/stdlib/std.d.ts" ), 164"{builtin_uri}" 165); 166let builtin_text =if builtin_uri. starts_with ( "file:" ) { 167let path = url:: Url :: parse ( builtin_uri) 168. unwrap () 169. to_file_path () 170. unwrap (); 171 fs:: read_to_string ( path). unwrap () 172} else { 173 server 174. call ( "avril/readDocument" , json! ({ "uri" : builtin_uri})) 175. unwrap () 176. as_str () 177. unwrap () 178. to_owned () 179}; 180assert_eq! ( 181selected ( & builtin_text, & builtin[ "targetSelectionRange" ]), 182"args" 183); 184 185// Split builtin declarations retain cross-module types and globals. 186let source =[ 187"import { copy, type CopyOptions } from \"avril:fs/sync\";" , 188"import { normalize, type ParsedPath } from \"avril:path/win32\";" , 189"import { stringify } from \"avril:fjson\";" , 190"import { test, type TestContext } from \"avril:test\";" , 191"test(\"typed\", async (t) => { const passed: boolean = await t.step(\"step\", () => {}); });" , 192"const contextName = (context: TestContext): string => context.name;" , 193"const serialized: string = stringify({ answer: 42 });" , 194"const options: CopyOptions = { dereference: true };" , 195"const parsed: ParsedPath = { root: \"\", dir: \"\", base: \"\", name: \"\", ext: \"\" };" , 196"copy(\"a\", normalize(\"b\"), options);" , 197"const main: boolean = import.meta.main;" , 198"const invalid: CopyOptions = { dereference: \"wrong\" };" , 199"export { parsed, main, invalid };" , 200] 201. join ( "\n" ); 202 fs:: write ( root. join ( "builtins.ts" ), & source). unwrap (); 203 server. open ( "builtins.ts" , & source); 204for ( symbol, file) in [ 205( "copy" , "fs/sync.d.ts" ), 206( "normalize" , "path/win32.d.ts" ), 207( "CopyOptions" , "fs.d.ts" ), 208( "stringify" , "fjson.d.ts" ), 209( "test" , "test.d.ts" ), 210( "TestContext" , "test.d.ts" ), 211] { 212let links = server 213. call ( 214"textDocument/definition" , 215query ( & server. uri ( "builtins.ts" ), & source, symbol, true ), 216) 217. unwrap (); 218let link =& links[ 0 ]; 219let target = link[ "targetUri" ]. as_str (). unwrap (); 220assert! ( 221 target. ends_with ( & format! ( "/runtime/stdlib/{file}" )), 222"{target}" 223); 224let declaration = server 225. call ( "avril/readDocument" , json! ({ "uri" : target})) 226. unwrap () 227. as_str () 228. unwrap () 229. to_owned (); 230assert_eq! ( 231selected ( & declaration, & link[ "targetSelectionRange" ]), 232 symbol 233); 234} 235let codes: Vec < Value > = server 236. diagnostics ( "builtins.ts" ) 237. iter () 238. map ( |diagnostic| diagnostic[ "code" ]. clone ()) 239. collect (); 240assert_eq! ( codes. iter (). filter ( |code|** code ==json! ( 2322 )). count (), 1 ); 241 server. notify ( 242"textDocument/didClose" , 243json! ({ "textDocument" : { "uri" : server. uri ( "builtins.ts" )}}), 244); 245 246// File-specific scopes survive editor URI mapping. 247for ( name, target) in [( "a.ts" , "scope-a.ts" ), ( "b.ts" , "scope-b.ts" )] { 248let source = fs:: read_to_string ( root. join ( name)). unwrap (); 249 server. open ( name, & source); 250let links = server. definitions ( name, & source, "value" ); 251assert_eq! ( links[ 0 ][ "targetUri" ], json! ( server. uri ( target))); 252} 253 254// Unsaved dependency edits update hover, locations and dependent diagnostics. 255let changed ="\r\n// π unsaved\r\nexport const value = \"edited\" as const;\r\n" ; 256 server. open ( "dep file.ts" , & dep_text); 257 server. change ( "dep file.ts" , changed, 2 ); 258assert! ( 259 server 260. hover ( "main.tsx" , & text, "localValue" ) 261. contains ( "localValue: \"edited\"" ) 262); 263let links = server 264. call ( 265"textDocument/definition" , 266query ( & main, & text, "localValue" , false ), 267) 268. unwrap (); 269assert_eq! ( 270 links[ 0 ][ "targetSelectionRange" ][ "start" ], 271position_at ( changed, changed. find ( "value" ). unwrap ()) 272); 273assert! ( 274 server 275. diagnostics ( "main.tsx" ) 276. iter () 277. any ( |diagnostic| diagnostic[ "code" ] ==json! ( 2322 )) 278); 279// Stale versions are rejected; closing reverts to the on-disk source. 280 server. change ( "dep file.ts" , & dep_text, 1 ); 281assert! ( 282 server 283. hover ( "main.tsx" , & text, "localValue" ) 284. contains ( "localValue: \"edited\"" ) 285); 286 server. notify ( 287"textDocument/didClose" , 288json! ({ "textDocument" : { "uri" : dep}}), 289); 290assert! ( 291 server 292. hover ( "main.tsx" , & text, "localValue" ) 293. contains ( "localValue: 42" ) 294); 295 296// Escaped identifier ranges and re-export targets come from native ASTs. 297let escaped ="export const \\u0076alue = 99 as const;\n" ; 298 server. open_version ( "dep file.ts" , escaped, 1 ); 299let links = server 300. call ( 301"textDocument/definition" , 302query ( & main, & text, "localValue" , false ), 303) 304. unwrap (); 305assert_eq! ( 306selected ( escaped, & links[ 0 ][ "targetSelectionRange" ]), 307"\\u0076alue" 308); 309assert! ( 310 server 311. hover ( "main.tsx" , & text, "localValue" ) 312. contains ( "localValue: 99" ) 313); 314 server. change ( 315"dep file.ts" , 316"export { value } from \"./scope-a.ts\";\n" , 3172 , 318); 319let links = server 320. call ( 321"textDocument/definition" , 322query ( & main, & text, "localValue" , false ), 323) 324. unwrap (); 325assert_eq! ( links[ 0 ][ "targetUri" ], json! ( server. uri ( "scope-a.ts" ))); 326 server. notify ( 327"textDocument/didClose" , 328json! ({ "textDocument" : { "uri" : dep}}), 329); 330 331// Namespace hover does not expose compiler-only paths. 332let source =format! ( "{text}\nimport * as dep from \"./dep file.ts\"; dep;" ); 333 server. change ( "main.tsx" , & source, 2 ); 334let hover = server 335. call ( "textDocument/hover" , query ( & main, & source, "dep" , true )) 336. unwrap (); 337let value =hover_text ( & hover); 338assert! ( value. contains ( "./dep file.ts" ), "{value}" ); 339assert! ( !value. contains ( "/avril-lsp/" ), "{value}" ); 340 341// Comments return no navigation or suggestions; unknown files are rejected. 342let comments =format! ( "// localValue\n{text}" ); 343 server. change ( "main.tsx" , & comments, 3 ); 344assert_eq! ( 345 server 346. call ( 347"textDocument/hover" , 348query ( & main, & comments, "localValue" , false ) 349) 350. unwrap (), 351Value :: Null 352); 353let completions = server 354. call ( 355"textDocument/completion" , 356query ( & main, & comments, "localValue" , false ), 357) 358. unwrap (); 359assert! ( 360 completions. is_null () 361 || completions[ "items" ] 362. as_array () 363. is_some_and ( |items| items. is_empty ()), 364"{completions}" 365); 366let unknown = server. request ( 367"textDocument/hover" , 368query ( "file:///not-in-project.ts" , "value" , "value" , true ), 369); 370assert_eq! ( unknown[ "error" ][ "code" ], json! ( -32602 ), "{unknown}" ); 371assert! ( 372 unknown[ "error" ][ "message" ] 373. as_str () 374. unwrap () 375. contains ( "Document not in this project" ) 376); 377let unknown = server. request ( 378"textDocument/completion" , 379query ( "file:///not-in-project.ts" , "value" , "value" , true ), 380); 381assert_eq! ( unknown[ "error" ][ "code" ], json! ( -32602 ), "{unknown}" ); 382assert! ( 383 unknown[ "error" ][ "message" ] 384. as_str () 385. unwrap () 386. contains ( "Document not in this project" ) 387); 388assert_eq! ( 389 server 390. call ( 391"textDocument/definition" , 392query ( & main, & comments, "localValue" , false ) 393) 394. unwrap (), 395json! ([]) 396); 397 398 server. close (); 399}