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 serde_json::{ Value , json}; 6use support::{ Server , backend, files}; 7 8fn items ( result : & Value ) ->& [ Value ] { 9assert! ( 10 !result. to_string (). contains ( "/avril-lsp/" ), 11"Completion leaked a compiler-internal filename: {result}" 12); 13 result[ "items" ]. as_array (). expect ( "CompletionList.items" ) 14} 15 16fn find < ' a >( items : &' a [ Value ], label : & str ) ->&' a Value { 17 items 18. iter () 19. find ( |item| item[ "label" ] == label) 20. unwrap_or_else ( ||panic! ( "Missing completion {label}: {items:?}" )) 21} 22 23fn labels ( items : & [ Value ]) ->Vec < & str > { 24 items 25. iter () 26. map ( |item| item[ "label" ]. as_str (). unwrap ()) 27. collect () 28} 29 30fn has_no_items ( result : & Value ) ->bool { 31 result. is_null () 32 || result. as_array (). is_some_and ( |items| items. is_empty ()) 33 || result[ "items" ] 34. as_array () 35. is_some_and ( |items| items. is_empty ()) 36} 37 38# [ test ] 39fn native_completions_resolve_and_follow_the_live_project_graph () { 40let Some ( backend) =backend () else { 41eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 42return ; 43}; 44let dir = tempfile:: tempdir (). unwrap (); 45let root = dir. path (); 46let main =[ 47"import { record } from \"alias\";" , 48"import { process } from \"avril:std\";" , 49"import \"./a.ts\";" , 50"import \"./b.ts\";" , 51"import \"./second.ts\";" , 52"import \"./auto.ts\";" , 53"import \"./named.ts\";" , 54"import \"./astral.ts\";" , 55"import \"./dangle.ts\";" , 56"import \"./fsimp.ts\";" , 57"import \"./element.ts\";" , 58"import \"./union.ts\";" , 59"import \"./tmpl.ts\";" , 60"const localName = 1;" , 61"record.hello;" , 62"process.args;" , 63"loc;" , 64] 65. join ( "\n" ); 66files ( 67 root, 68& [ 69( "main.ts" , & main), 70( 71"avril.conf" , 72& json! ({ 73"imports" : { "alias" : "./dep.ts" }, 74"scopes" : { 75"./a.ts" : { "scoped" : "./one.ts" }, 76"./b.ts" : { "scoped" : "./two.ts" } 77} 78}) 79. to_string (), 80), 81( 82"dep.ts" , 83"export const record: { hello: number; extra?: number; \"not-valid\": number } = { hello: 1, \"not-valid\": 2 };" , 84), 85( "one.ts" , "export const value = { aaa: 1 } as const;" ), 86( "two.ts" , "export const value = { bbb: 2 } as const;" ), 87( "a.ts" , "import {value} from \"scoped\"; value.aaa;" ), 88( "b.ts" , "import {value} from \"scoped\"; value.bbb;" ), 89( "second.ts" , "export const farAwayName = 1;" ), 90( "dangle.ts" , "import { record } from \"alias\";\nrecord." ), 91( "fsimp.ts" , "import { read } from \"avril:fs\";" ), 92( 93"element.ts" , 94"import { record } from \"alias\";\nrecord[\"\"]" , 95), 96( 97"union.ts" , 98"declare function take(flag: \"alpha\" | \"beta\"): void;\ntake(\"\");" , 99), 100( 101"tmpl.ts" , 102"import { record } from \"alias\";\nconst view = `${record.}`;" , 103), 104( "auto.ts" , "import { record } from \"alias\";\nfar;\n" ), 105( "named.ts" , "import { } from \"alias\";\n" ), 106( "astral.ts" , "const placeholder = 0;" ), 107], 108); 109let mut server =Server :: start ( root, & backend, & [ "--offline" ]); 110 server. initialize_with ( json! ({ 111"textDocument" : { 112"completion" : { 113"completionItem" : { 114"snippetSupport" : true , 115"insertReplaceSupport" : true 116} 117} 118} 119})); 120 server. open ( "main.ts" , & main); 121 122let offset = main. find ( "record.hello;" ). unwrap () +"record." . len (); 123let result = server. complete ( "main.ts" , & main, offset, None ); 124let members =items ( & result); 125let hello =find ( members, "hello" ); 126assert_eq! ( hello[ "kind" ], json! ( 5 )); 127let optional =find ( members, "extra?" ); 128assert_eq! ( optional[ "filterText" ], "extra" ); 129let triggered = server. complete ( "main.ts" , & main, offset, Some ( "." )); 130assert! ( labels ( items ( & triggered)). contains ( & "hello" )); 131 132let resolved = server 133. call ( "completionItem/resolve" , hello. clone ()) 134. unwrap (); 135assert_eq! ( resolved[ "label" ], "hello" ); 136assert! ( 137 resolved. get ( "detail" ). is_some (), 138"Native completion resolve should add detail: {resolved}" 139); 140let stale = hello. clone (); 141let edited ="export const record: { hello: number; extra?: number; added: boolean } = { hello: 1, added: true };" ; 142 server. open ( "dep.ts" , edited); 143let updated_result = server. complete ( "main.ts" , & main, offset, None ); 144let updated =items ( & updated_result); 145assert! ( labels ( updated). contains ( & "added" )); 146let stale = server. request ( "completionItem/resolve" , stale); 147assert_eq! ( stale[ "error" ][ "code" ], json! ( -32801 ), "{stale}" ); 148 server. notify ( 149"textDocument/didClose" , 150json! ({ "textDocument" : { "uri" : server. uri ( "dep.ts" )}}), 151); 152assert! ( !labels ( items ( & server. complete ( "main.ts" , & main, offset, None ))). contains ( & "added" )); 153 154let builtin_result = server. complete ( 155"main.ts" , 156& main, 157 main. find ( "process.args;" ). unwrap () +"process." . len (), 158None , 159); 160let builtin =items ( & builtin_result); 161assert! ( labels ( builtin). contains ( & "args" )); 162assert! ( labels ( builtin). contains ( & "cwd" )); 163 164for ( path, present, absent) in [( "a.ts" , "aaa" , "bbb" ), ( "b.ts" , "bbb" , "aaa" )] { 165let text = std:: fs:: read_to_string ( root. join ( path)). unwrap (); 166 server. open ( path, & text); 167let result = server. complete ( 168 path, 169& text, 170 text. find ( "value." ). unwrap () +"value." . len (), 171None , 172); 173assert! ( labels ( items ( & result)). contains ( & present)); 174assert! ( !labels ( items ( & result)). contains ( & absent)); 175} 176 177let astral ="const localName = 1;\r\nconst e = \"😀\"; loc;\r\n" ; 178 server. open ( "astral.ts" , astral); 179let result = server. complete ( 180"astral.ts" , 181 astral, 182 astral. find ( "loc;" ). unwrap () +"loc" . len (), 183None , 184); 185let local =find ( items ( & result), "localName" ); 186assert_eq! ( 187 local[ "textEdit" ][ "insert" ][ "start" ][ "line" ], 188json! ( 1 ), 189"{local}" 190); 191assert_eq! ( local[ "textEdit" ][ "insert" ][ "start" ][ "character" ], json! ( 16 )); 192assert_eq! ( local[ "textEdit" ][ "replace" ][ "end" ][ "character" ], json! ( 19 )); 193 194 server. open ( "second.ts" , "export const farAwayName = 1;" ); 195let auto ="import { record } from \"alias\";\nfar;\n" ; 196 server. open ( "auto.ts" , auto); 197let result = server. complete ( "auto.ts" , auto, auto. find ( "far;" ). unwrap () +3 , None ); 198let imported =find ( items ( & result), "farAwayName" ); 199let resolved_import = server 200. call ( "completionItem/resolve" , imported. clone ()) 201. unwrap (); 202assert! ( 203 !resolved_import. to_string (). contains ( "/avril-lsp/" ), 204"Resolved auto-import leaked a synthetic source: {resolved_import}" 205); 206let import_edits = resolved_import[ "additionalTextEdits" ] 207. as_array () 208. expect ( "Auto-import edits should be attached during completion resolve" ); 209assert! ( 210 import_edits. iter (). any ( |edit|{ 211 edit[ "newText" ] 212. as_str () 213. is_some_and ( |text| text. contains ( "farAwayName" ) && text. contains ( "./second.ts" )) 214}), 215"Resolved auto-import should use the graph's canonical specifier ./second.ts: {resolved_import}" 216); 217 218let named ="import { } from \"alias\";\n" ; 219 server. open ( "named.ts" , named); 220let import_result = server. complete ( "named.ts" , named, "import { " . len (), None ); 221let imports =items ( & import_result); 222assert! ( labels ( imports). contains ( & "record" )); 223let fs_import ="import { read } from \"avril:fs\";" ; 224 server. open ( "fsimp.ts" , fs_import); 225let result = server. complete ( 226"fsimp.ts" , 227 fs_import, 228 fs_import. find ( "read }" ). unwrap () +"read" . len (), 229None , 230); 231assert! ( labels ( items ( & result)). contains ( & "readTextFile" )); 232 233let element ="import { record } from \"alias\";\nrecord[\"\"];" ; 234 server. open ( "element.ts" , element); 235let result = server. complete ( 236"element.ts" , 237 element, 238 element. find ( "\"\"" ). unwrap () +1 , 239None , 240); 241assert! ( 242labels ( items ( & result)) 243. iter () 244. any ( |label| label. contains ( "not-valid" )), 245"String-literal property completion should preserve non-identifier keys" 246); 247 248let union ="declare function take(flag: \"alpha\" | \"beta\"): void;\ntake(\"\");" ; 249 server. open ( "union.ts" , union); 250let result = server. complete ( "union.ts" , union, union. find ( "\"\"" ). unwrap () +1 , None ); 251assert! ( 252labels ( items ( & result)) 253. iter () 254. any ( |label| label. contains ( "alpha" )), 255"Native literal-union completion should expose its allowed values" 256); 257 258let template ="import { record } from \"alias\";\nconst view = `${record.}`;" ; 259 server. open ( "tmpl.ts" , template); 260let result = server. complete ( 261"tmpl.ts" , 262 template, 263 template. find ( "record." ). unwrap () +"record." . len (), 264None , 265); 266assert! ( labels ( items ( & result)). contains ( & "hello" )); 267 268let quiet ="// record.hello;\nconst s = \"record.hello;\";\n" ; 269 server. change ( "main.ts" , quiet, 2 ); 270assert! ( has_no_items ( & server. complete ( 271"main.ts" , 272 quiet, 273"// record." . len (), 274None 275))); 276assert! ( has_no_items ( & server. complete ( 277"main.ts" , 278 quiet, 279 quiet. find ( "\"record." ). unwrap () +"\"record." . len (), 280None 281))); 282let missing = server. request ( 283"textDocument/completion" , 284json! ({ 285"textDocument" : { "uri" : server. uri ( "missing.ts" )}, 286"position" : { "line" : 0 , "character" : 1 } 287}), 288); 289assert_eq! ( missing[ "error" ][ "code" ], json! ( -32602 ), "{missing}" ); 290assert! ( 291 missing[ "error" ][ "message" ] 292. as_str () 293. unwrap () 294. contains ( "Document not in this project" ) 295); 296 297let dangling ="import { record } from \"alias\";\nrecord." ; 298 server. open ( "dangle.ts" , dangling); 299assert! ( 300labels ( items ( & server. complete ( 301"dangle.ts" , 302 dangling, 303 dangling. len (), 304None 305))) 306. contains ( & "hello" ) 307); 308 server. close (); 309}