char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1c8ffed
main
1const assert = require ( "node:assert/strict" ); 2const vscode = require ( "vscode" ); 3 4async function waitFor ( check ) { 5for ( let i = 0 ; i < 100 ; i ++ ) { 6if ( await check ()) return ; 7await new Promise (( resolve ) => setTimeout ( resolve , 100 )); 8} 9throw new Error ( "Editor condition timed out" ); 10} 11 12async function run () { 13const config = vscode . workspace . getConfiguration ( "avril.lsp" ); 14await config . update ( "preferences" , { referencesCodeLensEnabled :true , referencesCodeLensShowOnAllFunctions :true }, vscode . ConfigurationTarget . Global ); 15await config . update ( "executable" , process . env . AVRIL_LSP_AVRIL , vscode . ConfigurationTarget . Global ); 16await config . update ( "backend" , process . env . AVRIL_LSP_BACKEND , vscode . ConfigurationTarget . Global ); 17const folder = vscode . workspace . workspaceFolders [ 0 ]. uri ; 18const main = await vscode . workspace . openTextDocument ( vscode . Uri . joinPath ( folder , "main.tsx" )); 19const at = ( document , word , last = true ) => { 20const text = document . getText (); 21return document . positionAt (( last ?text . lastIndexOf ( word ) :text . indexOf ( word )) + 1 ); 22}; 23const hover = () => vscode . commands . executeCommand ( "vscode.executeHoverProvider" , main . uri , at ( main , "localValue" )); 24const definitions = ( word ) => vscode . commands . executeCommand ( "vscode.executeDefinitionProvider" , main . uri , at ( main , word , false )); 25const hasType = async ( type ) => ( await hover ()). some (( h ) => h . contents . some (( c ) => c . value . includes ( type ))); 26try { 27assert . ok ( vscode . extensions . getExtension ( "vscode.typescript-language-features" )); 28await vscode . extensions . getExtension ( "avril.avril" ). activate (); 29let editor = await vscode . window . showTextDocument ( main ); 30await waitFor (() => hasType ( "localValue: 42" )); 31await waitFor ( async () => ( await hover ()). length === 1 ); 32const [ local ] = await definitions ( "localValue" ); 33assert . equal ( local . targetUri . toString (), vscode . Uri . joinPath ( folder , "dep file.ts" ). toString ()); 34const dep = await vscode . workspace . openTextDocument ( local . targetUri ); 35assert . equal ( dep . getText ( local . targetSelectionRange ), "value" ); 36 37editor . selection = new vscode . Selection ( at ( main , "localValue" , false ), at ( main , "localValue" , false )); 38await vscode . commands . executeCommand ( "editor.action.revealDefinition" ); 39await waitFor (() => vscode . window . activeTextEditor ?. document . uri . toString () === dep . uri . toString ()); 40assert . equal ( vscode . window . activeTextEditor . selection . active . character , local . targetSelectionRange . start . character ); 41console . log ( "PASS actual editor F12 navigation: original local file and exact selection" ); 42 43const [ alias ] = await definitions ( "answer" ); 44assert . equal ( alias . targetUri . toString (), vscode . Uri . joinPath ( folder , "data.d.ts" ). toString ()); 45const declaration = await vscode . workspace . openTextDocument ( alias . targetUri ); 46assert . equal ( declaration . getText ( alias . targetSelectionRange ), "answer" ); 47await vscode . window . showTextDocument ( declaration ); 48console . log ( "PASS live avril.conf alias resolves to the original declaration" ); 49 50editor = await vscode . window . showTextDocument ( dep ); 51const edited = '\r\n// 😀 unsaved\r\nexport const value = "edited" as const;\r\n' ; 52await editor . edit (( builder ) => builder . replace ( new vscode . Range ( dep . positionAt ( 0 ), dep . positionAt ( dep . getText (). length )), edited )); 53await waitFor (() => hasType ( 'localValue: "edited"' )); 54const [ changed ] = await definitions ( "localValue" ); 55assert . equal ( changed . targetSelectionRange . start . line , 2 ); 56assert . equal ( dep . getText ( changed . targetSelectionRange ), "value" ); 57await waitFor (() => vscode . languages . getDiagnostics ( main . uri ). some (( d ) => d . code === 2322 )); 58console . log ( "PASS unsaved dependency edits update editor hover, definition ranges and diagnostics" ); 59await vscode . commands . executeCommand ( "workbench.action.revertAndCloseActiveEditor" ); 60await waitFor (() => hasType ( "localValue: 42" )); 61 62editor = await vscode . window . showTextDocument ( main ); 63const full = main . getText (); 64const completions = ( position ) => vscode . commands . executeCommand ( "vscode.executeCompletionItemProvider" , main . uri , position ); 65const label = ( item ) => item . label ?. label ?? item . label ; 66await editor . edit (( builder ) => builder . replace ( 67new vscode . Range ( main . positionAt ( 0 ), main . positionAt ( full . length )), 68full . replace ( "process.args;" , "process.ar;" ), 69)); 70const partial = main . positionAt ( main . getText (). indexOf ( "process.ar;" ) + "process.ar" . length ); 71await waitFor ( async () => ( await completions ( partial ))?. items . some (( item ) => label ( item ) === "args" )); 72const suggestion = ( await completions ( partial )). items . find (( item ) => label ( item ) === "args" ); 73const edit = suggestion . textEdit ; 74const candidate = suggestion . range ?? edit ?. range ?? edit ?. replace ; 75const replacementRange = candidate ?. replace ?? candidate ?. replacing ?? candidate ; 76assert . ok ( replacementRange , "Completion has no applicable range" ); 77const range = replacementRange instanceof vscode . Range 78 ?replacementRange 79 :Array . isArray ( replacementRange ) 80 ?new vscode . Range ( 81new vscode . Position ( ...replacementRange [ 0 ]), 82new vscode . Position ( ...replacementRange [ 1 ]), 83) 84 :new vscode . Range ( 85new vscode . Position ( replacementRange . start . line , replacementRange . start . character ), 86new vscode . Position ( replacementRange . end . line , replacementRange . end . character ), 87); 88const replacement = suggestion . insertText ?. value ?? suggestion . insertText ?? edit ?. newText ?? "args" ; 89await editor . edit (( builder ) => builder . replace ( range , replacement )); 90assert . equal ( main . getText (), full ); 91await vscode . commands . executeCommand ( "undo" ); 92await vscode . commands . executeCommand ( "undo" ); 93assert . equal ( main . getText (), full ); 94assert . equal ( main . isDirty , false ); 95console . log ( "PASS applying an avril completion replaces the partial word without saving" ); 96 97const referencesSource = "function localReferences(value: number): number { return value; }\nlocalReferences(1);" ; 98await editor . edit (( builder ) => builder . replace ( 99new vscode . Range ( main . positionAt ( 0 ), main . positionAt ( full . length )), 100referencesSource , 101)); 102await vscode . commands . executeCommand ( "vscode.executeCodeLensProvider" , main . uri ); 103const codeLenses = await vscode . commands . executeCommand ( "vscode.executeCodeLensProvider" , main . uri , 1 ); 104const codeLens = codeLenses . find (( lens ) => 105lens . command ?. command === "avril.showReferences" && main . getText ( lens . range ). includes ( "localReferences" )); 106assert . ok ( codeLens , `Expected a resolved references code lens: ${ JSON . stringify ( codeLenses )} ` ); 107const [ lensUri , lensPosition , locations ] = codeLens . command . arguments ; 108assert . equal ( lensUri , main . uri . toString ()); 109assert . ok ( Number . isInteger ( lensPosition . line ) && Number . isInteger ( lensPosition . character )); 110assert . ok ( Array . isArray ( locations ) && locations . length === 1 , JSON . stringify ( locations )); 111await vscode . commands . executeCommand ( codeLens . command . command , ...codeLens . command . arguments ); 112console . log ( "PASS resolved references code lens invokes editor.action.showReferences with editor-native arguments" ); 113await vscode . commands . executeCommand ( "closeReferenceSearch" ); 114editor = await vscode . window . showTextDocument ( main ); 115await vscode . commands . executeCommand ( "undo" ); 116assert . equal ( main . getText (), full ); 117assert . equal ( main . isDirty , false ); 118 119await editor . edit (( builder ) => builder . replace ( 120new vscode . Range ( main . positionAt ( 0 ), main . positionAt ( full . length )), 121full + "\nfunction takes(value: number): number { return value; }\ntakes(1);" , 122)); 123const signaturePosition = main . positionAt ( main . getText (). lastIndexOf ( "takes(1)" ) + "takes(1" . length ); 124let lastSignatureHelp ; 125try { 126await waitFor ( async () => { 127lastSignatureHelp = await vscode . commands . executeCommand ( "vscode.executeSignatureHelpProvider" , main . uri , signaturePosition ); 128return lastSignatureHelp ?. signatures . some (( signature ) => signature . label . includes ( "takes(value: number): number" )); 129}); 130} catch ( error ) { 131throw new Error ( ` ${ error . message } ; last signature help: ${ JSON . stringify ( lastSignatureHelp )} ` ); 132} 133const renamed = await vscode . commands . executeCommand ( 134"vscode.executeDocumentRenameProvider" , main . uri , at ( main , "localValue" , false ), "renamedValue" , 135); 136const renameEntries = renamed ?. entries ?.() || []; 137const renameChanges = renamed ?. changes ?Object . values ( renamed . changes ) :[]; 138assert . ok ( 139[ ...renameEntries . map (([, edits ]) => edits ), ...renameChanges ]. some (( edits ) => edits . length > 0 ), 140"Native rename returned no text edits" , 141); 142await vscode . commands . executeCommand ( "undo" ); 143assert . equal ( main . getText (), full ); 144assert . equal ( main . isDirty , false ); 145console . log ( "PASS native signature help and rename providers operate on unsaved documents" ); 146 147const [ builtin ] = await definitions ( "args" ); 148const builtinDocument = await vscode . workspace . openTextDocument ( builtin . targetUri ); 149assert . equal ( builtin . targetUri . scheme , "avril-builtin" ); 150assert . equal ( vscode . workspace . fs . isWritableFileSystem ( "avril-builtin" ), undefined ); 151assert . equal ( builtinDocument . getText ( builtin . targetSelectionRange ), "args" ); 152console . log ( "PASS builtin navigation selects the real avril declaration" ); 153 154editor = await vscode . window . showTextDocument ( main ); 155const inlineSource = "export const value: number = 42; // ?#% 😀" ; 156const inlineUrl = `data:text/typescript, ${ encodeURIComponent ( inlineSource )} ` ; 157await editor . edit (( builder ) => builder . replace ( 158new vscode . Range ( main . positionAt ( 0 ), main . positionAt ( full . length )), 159full + `\nimport {value as inlineValue} from ${ JSON . stringify ( inlineUrl )} ;\ninlineValue;` , 160)); 161let inlineHover ; 162try { 163await waitFor ( async () => { 164inlineHover = await vscode . commands . executeCommand ( "vscode.executeHoverProvider" , main . uri , at ( main , "inlineValue" )); 165return inlineHover . some (( h ) => h . contents . some (( c ) => c . value . includes ( "(alias) const value: number" ))); 166}); 167} catch ( error ) { 168throw new Error ( ` ${ error . message } ; last inline hover: ${ JSON . stringify ( inlineHover ?. flatMap (( h ) => h . contents . map (( c ) => c . value )))} ` ); 169} 170const [ inline ] = await definitions ( "inlineValue" ); 171assert . ok ( inline . targetUri . toString (). startsWith ( "avril-builtin:/data/" )); 172const inlineDocument = await vscode . workspace . openTextDocument ( inline . targetUri ); 173assert . equal ( inlineDocument . getText (), inlineSource ); 174assert . equal ( inlineDocument . getText ( inline . targetSelectionRange ), "value" ); 175assert . equal ( inlineDocument . languageId , "typescript" ); 176await vscode . window . showTextDocument ( inlineDocument ); 177editor = await vscode . window . showTextDocument ( main ); 178await vscode . commands . executeCommand ( "undo" ); 179assert . equal ( main . getText (), full ); 180assert . equal ( main . isDirty , false ); 181console . log ( "PASS data URL definition opens the decoded, read-only TypeScript source" ); 182 183await vscode . commands . executeCommand ( "avril.stopLanguageServer" ); 184await vscode . window . showTextDocument ( main ); 185const hasBuiltinError = () => vscode . languages . getDiagnostics ( main . uri ). some (( d ) => 186d . source === "ts" && ( d . code === 2307 || d . code ?. value === 2307 )); 187await waitFor ( hasBuiltinError ); 188await vscode . commands . executeCommand ( "avril.startLanguageServer" ); 189await waitFor ( async () => ( await hover ()). length === 1 && await hasType ( "localValue: 42" ) && ! hasBuiltinError ()); 190console . log ( "PASS automatic startup and reversible built-in TypeScript suppression" ); 191console . log ( "AVRIL_EDITOR_TESTS_PASSED" ); 192} finally { 193await vscode . commands . executeCommand ( "avril.stopLanguageServer" ); 194} 195} 196module . exports = { run};