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, position_at}; 7 8fn request ( uri : & str , text : & str , offset : usize ) ->Value { 9json! ({ 10"textDocument" : { "uri" : uri}, 11"position" : position_at ( text, offset) 12}) 13} 14 15# [ test ] 16fn native_language_service_capabilities_and_requests () { 17let Some ( backend) =backend () else { 18eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 19return ; 20}; 21let dir = tempfile:: tempdir (). unwrap (); 22let root = dir. path (); 23let source =[ 24"import { process } from \"avril:std\";" , 25"export interface Shape { area(): number }" , 26"export class Square implements Shape {" , 27" constructor(public size: number) {}" , 28" area() { return this.size ** 2; }" , 29"}" , 30"export class LargeSquare extends Square {}" , 31"const emoji = \"😀\";" , 32"const square:Shape=new Square(2);" , 33"function inferred() { return square.area(); }" , 34"square.area();" , 35"const markup = <div>{square.area()}</div>;" , 36"process.args;" , 37] 38. join ( "\r\n" ); 39files ( 40 root, 41& [ 42( "main.tsx" , & source), 43( 44"avril.conf" , 45& json! ({ 46"scopes" : { 47"./a.ts" : { "scoped" : "./one.ts" }, 48"./b.ts" : { "scoped" : "./two.ts" } 49} 50}) 51. to_string (), 52), 53( "a.ts" , "import {value} from \"scoped\"; value.aaa;" ), 54( "b.ts" , "import {value} from \"scoped\"; value.bbb;" ), 55( "one.ts" , "export const value = { aaa: 1 } as const;" ), 56( "two.ts" , "export const value = { bbb: 2 } as const;" ), 57], 58); 59let mut server =Server :: start ( root, & backend, & [ "--offline" ]); 60let initialized = server. initialize_params ( json! ({ 61"initializationOptions" : { 62"preferences" : { 63"includeInlayParameterNameHints" : "all" , 64"includeInlayFunctionLikeReturnTypeHints" : true , 65"includeInlayVariableTypeHints" : true 66} 67}, 68"capabilities" : { 69"textDocument" : { 70"definition" : { "linkSupport" : true }, 71"completion" : { 72"completionItem" : { 73"snippetSupport" : true , 74"insertReplaceSupport" : true , 75"resolveSupport" : { "properties" : [ "documentation" , "detail" ]} 76} 77}, 78"semanticTokens" : { 79"requests" : { "full" : true , "range" : true }, 80"tokenTypes" : [ "namespace" , "type" , "class" , "enum" , "interface" , "struct" , "typeParameter" , "parameter" , "variable" , "property" , "enumMember" , "event" , "function" , "method" , "macro" , "keyword" , "modifier" , "comment" , "string" , "number" , "regexp" , "operator" , "decorator" ], 81"tokenModifiers" : [ "declaration" , "definition" , "readonly" , "static" , "deprecated" , "abstract" , "async" , "modification" , "documentation" , "defaultLibrary" ], 82"formats" : [ "relative" ] 83}, 84"inlayHint" : { "dynamicRegistration" : true } 85}, 86"workspace" : { "workspaceFolders" : true } 87} 88})); 89let caps =& initialized[ "capabilities" ]; 90for namein [ 91"hoverProvider" , 92"completionProvider" , 93"signatureHelpProvider" , 94"definitionProvider" , 95"typeDefinitionProvider" , 96"implementationProvider" , 97"callHierarchyProvider" , 98"referencesProvider" , 99"documentHighlightProvider" , 100"renameProvider" , 101"documentSymbolProvider" , 102"workspaceSymbolProvider" , 103"foldingRangeProvider" , 104"selectionRangeProvider" , 105"documentFormattingProvider" , 106"documentRangeFormattingProvider" , 107"semanticTokensProvider" , 108"inlayHintProvider" , 109"codeActionProvider" , 110"linkedEditingRangeProvider" , 111] { 112assert! ( 113 caps. get ( name). is_some_and ( |value| !value. is_null ()), 114"Missing native capability {name}: {caps}" 115); 116} 117assert_eq! ( caps[ "completionProvider" ][ "resolveProvider" ], json! ( true )); 118assert! ( caps. get ( "typeHierarchyProvider" ). is_none ()); 119 server. open_as ( "main.tsx" , & source, 1 , "typescriptreact" ); 120 server. notify ( 121"workspace/didChangeConfiguration" , 122json! ({ "settings" :{ "preferences" :{ 123"includeInlayParameterNameHints" : "all" , 124"includeInlayFunctionLikeReturnTypeHints" : true , 125"includeInlayVariableTypeHints" : true 126}}}), 127); 128 129let uri = server. uri ( "main.tsx" ); 130let completion_offset = source. find ( "square.area" ). unwrap () +"square." . len (); 131let completions = server. complete ( "main.tsx" , & source, completion_offset, Some ( "." )); 132let area = completions[ "items" ] 133. as_array () 134. and_then ( |items| items. iter (). find ( |item| item[ "label" ] =="area" )) 135. expect ( "Native member completion should include area" ); 136assert! ( 137 area. get ( "data" ). is_some (), 138"Resolve data must be opaque but present: {area}" 139); 140let resolved = server. call ( "completionItem/resolve" , area. clone ()). unwrap (); 141assert_eq! ( resolved[ "label" ], "area" ); 142 server. change ( "main.tsx" , & format! ( "{source}\n" ), 2 ); 143let stale = server. request ( "completionItem/resolve" , area. clone ()); 144assert_eq! ( stale[ "error" ][ "code" ], json! ( -32801 ), "{stale}" ); 145let square = source. rfind ( "square.area" ). unwrap (); 146let params =request ( & uri, & source, square +2 ); 147let hover = server. call ( "textDocument/hover" , params. clone ()). unwrap (); 148assert! ( 149 !hover. is_null (), 150"Native hover should describe the selected member" 151); 152let type_definition = server 153. call ( "textDocument/typeDefinition" , params. clone ()) 154. unwrap (); 155assert! ( 156 type_definition 157. as_array () 158. is_some_and ( |locations| !locations. is_empty ()), 159"{type_definition}" 160); 161let implementations = server 162. call ( 163"textDocument/implementation" , 164json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( & source, source. find ( "area(): number" ). unwrap () +1 )}), 165) 166. unwrap (); 167assert! ( 168 implementations 169. as_array () 170. is_some_and ( |locations| !locations. is_empty ()), 171"{implementations}" 172); 173let call_hierarchy = server 174. call ( 175"textDocument/prepareCallHierarchy" , 176json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( & source, square +"square." . len () +1 )}), 177) 178. unwrap (); 179let call_item = call_hierarchy 180. as_array () 181. and_then ( |items| items. first ()) 182. expect ( "Call hierarchy should prepare area" ); 183let incoming = server 184. call ( "callHierarchy/incomingCalls" , json! ({ "item" : call_item})) 185. unwrap (); 186assert! ( 187 incoming. as_array (). is_some_and ( |calls| !calls. is_empty ()), 188"{incoming}" 189); 190let references = server 191. call ( 192"textDocument/references" , 193json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( & source, square +2 ), "context" :{ "includeDeclaration" : true }}), 194) 195. unwrap (); 196assert! ( 197 references 198. as_array () 199. is_some_and ( |locations| locations. len () >=2 ), 200"{references}" 201); 202 203let a_text = std:: fs:: read_to_string ( root. join ( "a.ts" )). unwrap (); 204let b_text = std:: fs:: read_to_string ( root. join ( "b.ts" )). unwrap (); 205let one_text = std:: fs:: read_to_string ( root. join ( "one.ts" )). unwrap (); 206let two_text = std:: fs:: read_to_string ( root. join ( "two.ts" )). unwrap (); 207for ( path, text) in [ 208( "a.ts" , a_text. as_str ()), 209( "b.ts" , b_text. as_str ()), 210( "one.ts" , one_text. as_str ()), 211( "two.ts" , two_text. as_str ()), 212] { 213 server. open ( path, text); 214} 215let refs = server 216. call ( 217"textDocument/references" , 218json! ({ "textDocument" :{ "uri" : server. uri ( "one.ts" )}, "position" : position_at ( & one_text, one_text. find ( "value" ). unwrap () +1 ), "context" :{ "includeDeclaration" : true }}), 219) 220. unwrap (); 221let ref_uris: Vec < _ > = refs 222. as_array () 223. unwrap () 224. iter () 225. map ( |location| location[ "uri" ]. as_str (). unwrap ()) 226. collect (); 227assert! ( ref_uris. contains ( & server. uri ( "one.ts" ). as_str ()), "{refs}" ); 228assert! ( ref_uris. contains ( & server. uri ( "a.ts" ). as_str ()), "{refs}" ); 229assert! ( !ref_uris. contains ( & server. uri ( "two.ts" ). as_str ()), "{refs}" ); 230assert! ( !ref_uris. contains ( & server. uri ( "b.ts" ). as_str ()), "{refs}" ); 231let cross_project_rename = server 232. call ( 233"textDocument/rename" , 234json! ({ "textDocument" :{ "uri" : server. uri ( "one.ts" )}, "position" : position_at ( & one_text, one_text. find ( "value" ). unwrap () +1 ), "newName" : "renamedValue" }), 235) 236. unwrap (); 237let changed_uris: Vec < _ > = cross_project_rename[ "changes" ] 238. as_object () 239. expect ( "rename changes" ) 240. keys () 241. map ( String :: as_str) 242. collect (); 243assert! ( 244 changed_uris. contains ( & server. uri ( "one.ts" ). as_str ()), 245"{cross_project_rename}" 246); 247assert! ( 248 changed_uris. contains ( & server. uri ( "a.ts" ). as_str ()), 249"{cross_project_rename}" 250); 251assert! ( 252 !changed_uris. contains ( & server. uri ( "two.ts" ). as_str ()), 253"{cross_project_rename}" 254); 255assert! ( 256 !changed_uris. contains ( & server. uri ( "b.ts" ). as_str ()), 257"{cross_project_rename}" 258); 259let highlights = server 260. call ( "textDocument/documentHighlight" , params. clone ()) 261. unwrap (); 262assert! ( !highlights. as_array (). unwrap (). is_empty (), "{highlights}" ); 263 264let rename = server 265. call ( 266"textDocument/prepareRename" , 267json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( & source, square +2 )}), 268) 269. unwrap (); 270assert! ( 271 !rename. is_null (), 272"prepareRename should return the native rename range" 273); 274let edit = server 275. call ( 276"textDocument/rename" , 277json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( & source, square +2 ), "newName" : "measure" }), 278) 279. unwrap (); 280assert! ( 281 edit[ "changes" ] 282. as_object () 283. is_some_and ( |changes| !changes. is_empty ()) 284 || edit[ "documentChanges" ] 285. as_array () 286. is_some_and ( |changes| !changes. is_empty ()), 287"{edit}" 288); 289let readonly_rename = server. request ( 290"textDocument/rename" , 291json! ({ 292"textDocument" :{ "uri" : uri}, 293"position" : position_at ( & source, source. rfind ( "args" ). unwrap () +1 ), 294"newName" : "arguments" 295}), 296); 297assert! ( 298 readonly_rename. get ( "error" ). is_some (), 299"Read-only builtin rename must fail atomically: {readonly_rename}" 300); 301 302let symbols = server 303. call ( 304"textDocument/documentSymbol" , 305json! ({ "textDocument" :{ "uri" : uri}}), 306) 307. unwrap (); 308assert! ( 309 symbols 310. as_array () 311. is_some_and ( |items| items. iter (). any ( |item| item[ "name" ] =="Square" )), 312"{symbols}" 313); 314let workspace_symbols = server 315. call ( "workspace/symbol" , json! ({ "query" : " LargeSquare " })) 316. unwrap (); 317assert! ( 318 workspace_symbols 319. as_array () 320. is_some_and ( |items| items. iter (). any ( |item| item[ "name" ] =="LargeSquare" )), 321"{workspace_symbols}" 322); 323let folding = server 324. call ( 325"textDocument/foldingRange" , 326json! ({ "textDocument" :{ "uri" : uri}}), 327) 328. unwrap (); 329assert! ( !folding. as_array (). unwrap (). is_empty (), "{folding}" ); 330let selections = server 331. call ( 332"textDocument/selectionRange" , 333json! ({ "textDocument" :{ "uri" : uri}, "positions" :[ position_at ( & source, square +2 )]}), 334) 335. unwrap (); 336assert_eq! ( selections. as_array (). unwrap (). len (), 1 ); 337 338let full_range = 339json! ({ "start" : position_at ( & source, 0 ), "end" : position_at ( & source, source. len ())}); 340let formatting = server 341. call ( 342"textDocument/formatting" , 343json! ({ "textDocument" :{ "uri" : uri}, "options" :{ "tabSize" : 2 , "insertSpaces" : true }}), 344) 345. unwrap (); 346assert! ( 347 formatting. as_array (). is_some_and ( |edits| !edits. is_empty ()), 348"{formatting}" 349); 350let range_formatting = server 351. call ( 352"textDocument/rangeFormatting" , 353json! ({ 354"textDocument" :{ "uri" : uri}, 355"range" : full_range, 356"options" :{ "tabSize" : 2 , "insertSpaces" : true } 357}), 358) 359. unwrap (); 360assert! ( range_formatting. as_array (). is_some (), "{range_formatting}" ); 361let semantic = server 362. call ( 363"textDocument/semanticTokens/full" , 364json! ({ "textDocument" :{ "uri" : uri}}), 365) 366. unwrap (); 367assert! ( 368 semantic[ "data" ] 369. as_array () 370. is_some_and ( |data| !data. is_empty ()), 371"{semantic}" 372); 373let ranged = server 374. call ( 375"textDocument/semanticTokens/range" , 376json! ({ "textDocument" :{ "uri" : uri}, "range" : full_range}), 377) 378. unwrap (); 379assert! ( !ranged[ "data" ]. as_array (). unwrap (). is_empty (), "{ranged}" ); 380let hints = server 381. call ( 382"textDocument/inlayHint" , 383json! ({ "textDocument" :{ "uri" : uri}, "range" : full_range}), 384) 385. unwrap (); 386assert! ( 387 hints. as_array (). is_some_and ( |hints| !hints. is_empty ()), 388"Configured native inlay preferences should produce hints: {hints}" 389); 390let actions = server 391. call ( 392"textDocument/codeAction" , 393json! ({ "textDocument" :{ "uri" : uri}, "range" : full_range, "context" :{ "diagnostics" :[]}}), 394) 395. unwrap (); 396assert! ( actions. as_array (). is_some (), "{actions}" ); 397 398let unsupported_hierarchy = server. request ( 399"textDocument/prepareTypeHierarchy" , 400json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( & source, source. find ( "Square implements" ). unwrap () +2 )}), 401); 402assert_eq! ( 403 unsupported_hierarchy[ "error" ][ "code" ], 404json! ( -32601 ), 405"Pinned TypeScript has protocol types but no type-hierarchy implementation: {unsupported_hierarchy}" 406); 407 408let linked_source ="const same = <div></div>;" ; 409 server. change ( "main.tsx" , linked_source, 3 ); 410let linked = server 411. call ( 412"textDocument/linkedEditingRange" , 413json! ({ "textDocument" :{ "uri" : uri}, "position" : position_at ( linked_source, linked_source. find ( "div" ). unwrap () +1 )}), 414) 415. unwrap (); 416assert! ( 417 linked[ "ranges" ] 418. as_array () 419. is_some_and ( |ranges| ranges. len () >=2 ), 420"{linked}" 421); 422}