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 lsp_support; 4# [ path = "common/registry.rs" ] 5mod registry_support; 6 7use lsp_support::{ Server , backend, files, position_at}; 8use registry_support:: Registry ; 9use serde_json::{ Value , json}; 10use std:: path::{ Path , PathBuf }; 11 12fn files_below ( root : & Path ) ->Vec < PathBuf > { 13let mut found =Vec :: new (); 14let mut pending =vec! [ root. to_owned ()]; 15while let Some ( directory) = pending. pop () { 16let Ok ( entries) = std:: fs:: read_dir ( directory) else { 17continue ; 18}; 19for entryin entries. flatten () { 20let path = entry. path (); 21if path. is_dir () { 22 pending. push ( path); 23} else { 24 found. push ( path); 25} 26} 27} 28 found 29} 30 31# [ test ] 32fn malformed_continuations_and_empty_completions_leave_the_server_usable () { 33let Some ( backend) =backend () else { 34return ; 35}; 36let dir = tempfile:: tempdir (). unwrap (); 37let source ="// comment\nconst value = 42;\nvalue;" ; 38files ( dir. path (), & [( "main.ts" , source)]); 39let mut server =Server :: start ( dir. path (), & backend, & []); 40 server. initialize (); 41 server. open ( "main.ts" , source); 42for methodin [ "callHierarchy/incomingCalls" , "callHierarchy/outgoingCalls" ] { 43for paramsin [ json! ([]), json! ( 42 ), Value :: Null ] { 44let error = server. call ( method, params). unwrap_err (); 45assert_eq! ( error. code , -32602 , "{error:?}" ); 46} 47} 48let uri = server. uri ( "main.ts" ); 49assert_eq! ( 50 server 51. call ( 52"textDocument/completion" , 53json! ({ 54"textDocument" :{ "uri" : uri}, "position" :{ "line" : 0 , "character" : 3 } 55}) 56) 57. unwrap (), 58Value :: Null 59); 60let error = server 61. call ( 62"textDocument/hover" , 63json! ({ 64"textDocument" :{ "uri" : uri}, "position" :{ "line" : -1 , "character" : 0 } 65}), 66) 67. unwrap_err (); 68assert_eq! ( error. code , -32602 , "{error:?}" ); 69assert! ( server. hover ( "main.ts" , source, "value" ). contains ( "42" )); 70 server. close (); 71} 72 73# [ test ] 74fn immutable_npm_sources_and_workspace_completions_respect_boundaries () { 75let Some ( backend) =backend () else { 76eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 77return ; 78}; 79let registry =Registry :: new (); 80 registry. publish_npm ( 81"typed" , 82"1.0.0" , 83& [ 84( 85"package.json" , 86r#"{"name":"typed","version":"1.0.0","types":"index.d.ts"}"# , 87), 88( "index.d.ts" , "export declare const answer: number;\n" ), 89], 90); 91let dir = tempfile:: tempdir (). unwrap (); 92let root = dir. path (); 93let main ="import { answer } from \"typed\";\nconst local = answer;\nimport \"./\";\n" ; 94files ( 95 root, 96& [ 97( "main.ts" , main), 98( "sibling.ts" , "export const sibling = true;\n" ), 99( "my file.ts" , "export const spacedExport = true;\n" ), 100( "café.ts" , "export const unicodeExport = true;\n" ), 101( 102"isolated/tsconfig.json" , 103r#"{"compilerOptions":{"strictNullChecks":false}}"# , 104), 105( "isolated/export.ts" , "export const scopedExport = true;\n" ), 106( 107"avril.conf" , 108& json! ({ "imports" :{ "typed" : "npm : typed@ 1 . 0 . 0 " }}). to_string (), 109), 110], 111); 112let mut server =Server :: start ( 113 root, 114& backend, 115& [ & format! ( "--npm-registry={}" , registry. url )], 116); 117 server. initialize (); 118 server. open ( "main.ts" , main); 119 120let reference = main. find ( "answer;\nimport" ). unwrap (); 121let definition = server 122. call ( 123"textDocument/definition" , 124json! ({ 125"textDocument" :{ "uri" : server. uri ( "main.ts" )}, 126"position" : position_at ( main, reference +1 ) 127}), 128) 129. unwrap (); 130let declaration = definition 131. as_array () 132. and_then ( |items| items. first ()) 133. expect ( "type definition should resolve to the package declaration" ); 134let declaration_uri = declaration 135. get ( "targetUri" ) 136. or_else ( || declaration. get ( "uri" )) 137. and_then ( Value :: as_str) 138. unwrap_or_else ( ||panic! ( "Missing definition URI: {definition}" )); 139assert! ( 140 declaration_uri. starts_with ( "avril-builtin:/dependencies" ), 141"{declaration}" 142); 143let bytes = server 144. call ( "avril/readDocument" , json! ({ "uri" : declaration_uri})) 145. unwrap (); 146assert_eq! ( bytes, json! ( "export declare const answer: number;\n" )); 147 148let rename = server. request ( 149"textDocument/rename" , 150json! ({ 151"textDocument" :{ "uri" : declaration_uri}, 152"position" : position_at ( "export declare const answer: number;\n" , "export declare const " . len () +2 ), 153"newName" : "renamedAnswer" 154}), 155); 156assert_eq! ( rename[ "error" ][ "code" ], json! ( -32803 ), "{rename}" ); 157assert! ( 158 rename. get ( "result" ). is_none (), 159"read-only rename must not partially edit workspace references: {rename}" 160); 161 162assert_eq! ( 163 server 164. call ( 165"textDocument/completion" , 166json! ({ 167"textDocument" :{ "uri" : declaration_uri}, "position" :{ "line" : 0 , "character" : 0 } 168}) 169) 170. unwrap (), 171Value :: Null 172); 173assert_eq! ( server. call ( "textDocument/codeAction" , json! ({ 174"textDocument" :{ "uri" : declaration_uri}, "range" :{ "start" :{ "line" : 0 , "character" : 0 }, "end" :{ "line" : 0 , "character" : 0 }}, 175"context" :{ "diagnostics" :[], "only" :[ "quickfix" ]} 176})). unwrap (), json! ([])); 177 178let overlay ="export declare const poisoned: string;\n" ; 179for uriin [ 180 declaration_uri. to_owned (), 181 url:: Url :: from_file_path ( 182files_below ( & root. join ( ".avril" )) 183. into_iter () 184. find ( |path| path. file_name (). is_some_and ( |name| name =="index.d.ts" )) 185. expect ( "materialized npm declaration" ), 186) 187. unwrap () 188. to_string (), 189] { 190 server. notify ( 191"textDocument/didOpen" , 192json! ({ "textDocument" :{ 193"uri" : uri, "languageId" : "typescript" , "version" : 1 , "text" : overlay 194}}), 195); 196} 197let hover = server. call ( 198"textDocument/hover" , 199json! ({ "textDocument" :{ "uri" : server. uri ( "main.ts" )}, "position" : position_at ( main, reference +1 )}), 200). unwrap (); 201assert! ( 202 hover. to_string (). contains ( "number" ), 203"trusted npm declaration should remain in effect: {hover}" 204); 205assert! ( 206 !hover. to_string (). contains ( "poisoned" ), 207"immutable overlays must be ignored: {hover}" 208); 209assert_eq! ( 210 server 211. call ( "avril/readDocument" , json! ({ "uri" : declaration_uri})) 212. unwrap (), 213 bytes, 214"opening either immutable URI must not replace trusted source bytes" , 215); 216 217let start = main. find ( "\"./\"" ). unwrap () +3 ; 218let completion = server. call ( 219"textDocument/completion" , 220json! ({ "textDocument" :{ "uri" : server. uri ( "main.ts" )}, "position" : position_at ( main, start)}), 221). unwrap (); 222let labels: Vec < _ > = completion[ "items" ] 223. as_array () 224. into_iter () 225. flatten () 226. filter_map ( |item : & Value | item[ "label" ]. as_str ()) 227. collect (); 228assert! ( 229 labels. contains ( & "sibling.ts" ), 230"unopened workspace sibling should be suggested: {completion}" 231); 232assert! ( 233 labels. contains ( & "my file.ts" ) && labels. contains ( & "café.ts" ), 234"{completion}" 235); 236assert! ( 237 !labels. iter (). any ( |label| label. parse ::< u64 >(). is_ok ()), 238"compiler-internal numeric directories leaked: {completion}" 239); 240for ( index, ( symbol, specifier)) in [ 241( "spacedExport" , "./my file.ts" ), 242( "unicodeExport" , "./café.ts" ), 243( "scopedExport" , "./isolated/export.ts" ), 244] 245. into_iter () 246. enumerate () 247{ 248let text =& symbol[ ..3 ]; 249 server. change ( "main.ts" , text, indexas i64 +2 ); 250let completion = server. call ( "textDocument/completion" , json! ({ 251"textDocument" :{ "uri" : server. uri ( "main.ts" )}, "position" :{ "line" : 0 , "character" : 3 }, 252})). unwrap (); 253let item = completion[ "items" ] 254. as_array () 255. unwrap () 256. iter () 257. find ( |item| item[ "label" ] == symbol) 258. unwrap_or_else ( ||panic! ( "Missing export {symbol}: {completion}" )) 259. clone (); 260let resolved = server. call ( "completionItem/resolve" , item). unwrap (); 261assert! ( 262 resolved[ "additionalTextEdits" ] 263. as_array () 264. unwrap () 265. iter () 266. any ( |edit| edit[ "newText" ]. as_str (). unwrap (). contains ( specifier)), 267"{resolved}" 268); 269assert! ( !resolved. to_string (). contains ( "/avril-lsp/" ), "{resolved}" ); 270} 271 server. close (); 272}