char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c7a9569
main
1const vscode = require ( "vscode" ); 2const path = require ( "node:path" ); 3const { LanguageClient, State} = require ( "vscode-languageclient/node" ); 4 5let session ; 6let tsApi ; 7let paused = false ; 8let pending = Promise . resolve (); 9 10function enqueue ( action ) { 11const result = pending . then ( action ); 12pending = result . catch (() => {}); 13return result ; 14} 15 16function ownsDocument ( uri , folder ) { 17return uri . scheme !== "file" || vscode . workspace . getWorkspaceFolder ( uri )?. uri . toString () === folder . uri . toString (); 18} 19 20function configureTypeScript () { 21tsApi ?. configurePlugin ( "avril-typescript" , { 22workspaceRoots :( vscode . workspace . workspaceFolders || []). filter (( folder ) => folder . uri . scheme === "file" ). map (( folder ) => folder . uri . fsPath ), 23liveRoots :session ?. client . state === State . Running ?[ session . folder . uri . fsPath ] :[], 24}); 25} 26 27async function stop () { 28const previous = session ; 29session = undefined ; 30try { if ( previous ?. client . needsStop ()) await previous . client . stop (); } 31finally { 32previous ?. state . dispose (); 33previous ?. files . dispose (); 34configureTypeScript (); 35} 36} 37 38async function start ( automatic = false ) { 39if ( ! vscode . workspace . isTrusted ) throw new Error ( "The language server requires a trusted workspace." ); 40if ( session ?. client . state === State . Stopped ) await stop (); 41if ( session ) return ; 42const folder = vscode . workspace . workspaceFolders ?.[ 0 ]; 43if ( ! folder || folder . uri . scheme !== "file" ) throw new Error ( "Open a local workspace." ); 44const config = vscode . workspace . getConfiguration ( "avril.lsp" , folder . uri ); 45const backend = config . get ( "backend" ); 46if ( ! backend || ! path . isAbsolute ( backend )) throw new Error ( "Set avril.lsp.backend to the absolute native tsc path." ); 47const extension = vscode . extensions . getExtension ( "vscode.typescript-language-features" ); 48if ( extension && ! tsApi ) { 49tsApi = ( await extension . activate ()). getAPI ( 0 ); 50if ( ! tsApi ) throw new Error ( "Cannot configure VS Code's TypeScript plugin API." ); 51} 52const owns = ( uri ) => ownsDocument ( uri , folder ); 53const args = [ "--project=" + folder . uri . fsPath , ...config . get ( "options" , [])]; 54const files = vscode . workspace . createFileSystemWatcher ( new vscode . RelativePattern ( folder , "**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs,json,conf,lock}" )); 55const client = new LanguageClient ( "avril" , "avril" , { 56command :config . get ( "executable" ), 57args :[ "lsp" , "--typescript=" + backend , ...args ], 58options :{ cwd :folder . uri . fsPath }, 59}, { 60workspaceFolder :folder , 61initializationOptions :{ preferences :config . get ( "preferences" , {}) }, 62synchronize :{ fileEvents :files }, 63documentSelector :[ 64{ scheme :"file" , pattern :new vscode . RelativePattern ( folder , "**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}" ) }, 65{ scheme :"avril-https" }, 66{ scheme :"avril-builtin" }, 67{ scheme :"avril-http" }, 68], 69// A nested workspace folder can belong to a different language service. 70middleware :{ 71didOpen :( document , next ) => owns ( document . uri ) ?next ( document ) :undefined , 72didChange :( event , next ) => owns ( event . document . uri ) ?next ( event ) :undefined , 73didClose :( document , next ) => owns ( document . uri ) ?next ( document ) :undefined , 74didSave :( document , next ) => owns ( document . uri ) ?next ( document ) :undefined , 75provideHover :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 76provideDefinition :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 77provideCompletionItem :( document , position , context , token , next ) => owns ( document . uri ) ?next ( document , position , context , token ) :undefined , 78resolveCompletionItem :( item , token , next ) => owns ( folder . uri ) ?next ( item , token ) :undefined , 79provideSignatureHelp :( document , position , context , token , next ) => owns ( document . uri ) ?next ( document , position , context , token ) :undefined , 80provideReferences :( document , position , options , token , next ) => owns ( document . uri ) ?next ( document , position , options , token ) :undefined , 81provideDocumentHighlights :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 82provideRenameEdits :( document , position , newName , token , next ) => owns ( document . uri ) ?next ( document , position , newName , token ) :undefined , 83prepareRename :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 84provideDocumentSymbols :( document , token , next ) => owns ( document . uri ) ?next ( document , token ) :undefined , 85provideWorkspaceSymbols :( query , token , next ) => owns ( folder . uri ) ?next ( query , token ) :undefined , 86provideFoldingRanges :( document , context , token , next ) => owns ( document . uri ) ?next ( document , context , token ) :undefined , 87provideSelectionRanges :( document , positions , token , next ) => owns ( document . uri ) ?next ( document , positions , token ) :undefined , 88provideDocumentFormattingEdits :( document , options , token , next ) => owns ( document . uri ) ?next ( document , options , token ) :undefined , 89provideDocumentRangeFormattingEdits :( document , range , options , token , next ) => owns ( document . uri ) ?next ( document , range , options , token ) :undefined , 90provideDocumentSemanticTokens :( document , token , next ) => owns ( document . uri ) ?next ( document , token ) :undefined , 91provideDocumentSemanticTokensEdits :( document , previousResultId , token , next ) => owns ( document . uri ) ?next ( document , previousResultId , token ) :undefined , 92provideDocumentRangeSemanticTokens :( document , range , token , next ) => owns ( document . uri ) ?next ( document , range , token ) :undefined , 93provideInlayHints :( document , range , token , next ) => owns ( document . uri ) ?next ( document , range , token ) :undefined , 94prepareTypeHierarchy :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 95prepareCallHierarchy :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 96provideCallHierarchyIncomingCalls :( item , token , next ) => owns ( item . uri ) ?next ( item , token ) :undefined , 97provideCallHierarchyOutgoingCalls :( item , token , next ) => owns ( item . uri ) ?next ( item , token ) :undefined , 98provideTypeHierarchySupertypes :( item , token , next ) => owns ( item . uri ) ?next ( item , token ) :undefined , 99provideTypeHierarchySubtypes :( item , token , next ) => owns ( item . uri ) ?next ( item , token ) :undefined , 100provideLinkedEditingRange :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 101provideImplementation :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 102provideTypeDefinition :( document , position , token , next ) => owns ( document . uri ) ?next ( document , position , token ) :undefined , 103provideCodeActions :( document , range , context , token , next ) => owns ( document . uri ) ?next ( document , range , context , token ) :undefined , 104resolveCodeAction :( item , token , next ) => owns ( folder . uri ) ?next ( item , token ) :undefined , 105provideCodeLenses :( document , token , next ) => { 106if ( ! owns ( document . uri )) return undefined ; 107return Promise . resolve ( next ( document , token )). then (( lenses ) => { 108// The language client's converter recognizes its CodeLens subclass. 109for ( const lens of lenses || []) lens . data = { ...lens . data , avrilDocumentUri :document . uri . toString () }; 110return lenses ; 111}); 112}, 113resolveCodeLens :( codeLens , token , next ) => { 114const uri = codeLens . data ?. avrilDocumentUri ; 115if ( typeof uri !== "string" || ! owns ( vscode . Uri . parse ( uri ))) return undefined ; 116return Promise . resolve ( next ( codeLens , token )). then (( resolved ) => { 117if ( ! resolved ?. command ) return resolved ; 118if ( resolved . command . command !== "editor.action.showReferences" ) return undefined ; 119resolved . command . command = "avril.showReferences" ; 120resolved . data = { ...resolved . data , avrilDocumentUri :uri }; 121return resolved ; 122}); 123}, 124handleDiagnostics :( uri , diagnostics , next ) => next ( uri , owns ( uri ) ?diagnostics :[]), 125}, 126}); 127session = { client, files, folder, automatic, state :client . onDidChangeState ( configureTypeScript ) }; 128try { await client . start (); } 129catch ( error ) { await stop (); throw error ; } 130} 131 132async function reconcile () { 133const folder = vscode . workspace . workspaceFolders ?.[ 0 ]; 134if ( session && session . folder . uri . toString () !== folder ?. uri . toString ()) await stop (); 135configureTypeScript (); 136if ( ! vscode . workspace . isTrusted || ! folder || folder . uri . scheme !== "file" ) return ; 137const config = vscode . workspace . getConfiguration ( "avril.lsp" , folder . uri ); 138let hasConfig = false ; 139try { 140const stat = await vscode . workspace . fs . stat ( vscode . Uri . joinPath ( folder . uri , "avril.conf" )); 141hasConfig = Boolean ( stat . type & vscode . FileType . File ); 142} catch ( error ) { 143if ( error . code !== "FileNotFound" ) throw error ; 144} 145const enabled = config . get ( "autoStart" , true ) && hasConfig ; 146if ( session ?. automatic && ! enabled ) await stop (); 147if ( ! session && enabled && ! paused && config . get ( "backend" )) await start ( true ); 148} 149 150function activate ( context ) { 151const automatic = () => enqueue ( reconcile ). catch (( error ) => vscode . window . showErrorMessage ( `avril: ${ error . message } ` )); 152const roots = vscode . workspace . createFileSystemWatcher ( "**/avril.conf" ); 153const rootChanged = ( uri ) => { 154const folder = vscode . workspace . workspaceFolders ?.[ 0 ]; 155if ( folder && uri . toString () === vscode . Uri . joinPath ( folder . uri , "avril.conf" ). toString ()) return automatic (); 156}; 157context . subscriptions . push ( 158roots , 159roots . onDidCreate ( rootChanged ), 160roots . onDidDelete ( rootChanged ), 161vscode . workspace . onDidGrantWorkspaceTrust ( automatic ), 162vscode . workspace . onDidChangeWorkspaceFolders ( automatic ), 163vscode . workspace . onDidChangeConfiguration (( event ) => { 164if ( event . affectsConfiguration ( "avril.lsp.preferences" ) && session ?. client . state === State . Running ) { 165const preferences = vscode . workspace . getConfiguration ( "avril.lsp" , session . folder . uri ). get ( "preferences" , {}); 166session . client . sendNotification ( "workspace/didChangeConfiguration" , { settings :{ preferences} }) 167. catch (( error ) => vscode . window . showErrorMessage ( `avril: ${ error . message } ` )); 168} 169if ( event . affectsConfiguration ( "avril.lsp" )) return automatic (); 170}), 171 ...[ "avril-https" , "avril-http" , "avril-builtin" ]. map (( scheme ) => vscode . workspace . registerTextDocumentContentProvider ( scheme , { 172provideTextDocumentContent ( uri , token ) { 173if ( ! session ) throw new Error ( "Start the avril language server first." ); 174return session . client . sendRequest ( "avril/readDocument" , { uri :uri . toString () }, token ); 175}, 176})), 177vscode . commands . registerCommand ( "avril.showReferences" , ( uri , position , locations ) => { 178const client = session ?. client ; 179if ( ! client ) throw new Error ( "The avril language server is not running." ); 180if ( typeof uri !== "string" || ! position || ! Array . isArray ( locations )) { 181throw new TypeError ( "Invalid avril references command arguments." ); 182} 183const converter = client . protocol2CodeConverter ; 184const targetUri = converter . asUri ( uri ); 185if ( ! ownsDocument ( targetUri , session . folder )) { 186throw new Error ( "The avril references command contains a document outside its workspace." ); 187} 188return vscode . commands . executeCommand ( 189"editor.action.showReferences" , 190targetUri , 191converter . asPosition ( position ), 192locations . map (( location ) => converter . asLocation ( location )), 193); 194}), 195vscode . commands . registerCommand ( "avril.stopLanguageServer" , () => enqueue ( async () => { 196paused = true ; 197await stop (); 198})), 199vscode . commands . registerCommand ( "avril.startLanguageServer" , () => enqueue ( async () => { 200paused = false ; 201await start (); 202})), 203); 204return automatic (); 205} 206 207module . exports = { activate, deactivate :() => enqueue ( stop ) };