Files
newt/newt-vscode-lsp/src/lsp.ts
2026-03-27 20:20:08 -07:00

163 lines
4.5 KiB
TypeScript

/**
* Wraps newt.js (compiled from src/LSP.newt with some tweaks to `export`) with the
* vscode LSP server module.
*/
import { LSP_checkFile, LSP_updateFile, LSP_hoverInfo, LSP_codeActionInfo, LSP_docSymbols } from './newt.js'
import {
createConnection,
TextDocuments,
ProposedFeatures,
Hover,
InitializeParams,
InitializeResult,
TextDocumentSyncKind,
Location,
TextDocumentIdentifier,
} from "vscode-languageserver/node";
import fs from 'node:fs';
import path from 'node:path';
import { TextDocument } from "vscode-languageserver-textdocument";
const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);
// the last is the most important to the user, but we run FIFO
// to ensure dependencies are seen in causal order
let changes: (TextDocument|TextDocumentIdentifier)[] = []
let running = false
let lastChange = 0
function addChange(doc: TextDocument | TextDocumentIdentifier) {
console.log('enqueue', doc.uri)
// drop stale pending changes
let before = changes.length
changes = changes.filter(ch => ch.uri != doc.uri)
console.log('DROPPED', before - changes.length);
changes.push(doc)
lastChange = +new Date()
if (!running) runChange()
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function runChange() {
try {
running = true;
while (changes.length) {
console.log('LOOP TOP')
// Wait until things stop changing
let prev = lastChange
while (1) {
await sleep(100)
if (prev == lastChange) break
prev = lastChange
console.log('DELAY')
}
let doc = changes.shift()
if (!doc) {
running = false
return
}
const uri = doc.uri
const start = +new Date()
const diagnostics = LSP_checkFile(doc.uri)
const end = +new Date()
console.log('CHECK', doc.uri, 'in', end - start);
await sleep(1);
if (!changes.find(ch => ch.uri === uri)) {
console.log('SEND', diagnostics.length, 'for', uri)
connection.sendDiagnostics({ uri, diagnostics })
} else {
console.log('STALE result not sent for', uri)
}
}
} catch (e) {
console.error(e);
} finally {
running = false;
}
}
documents.onDidChangeContent(async (change) => {
console.log('DIDCHANGE', change.document.uri)
const uri = change.document.uri;
const text = change.document.getText();
// update/invalidate happens now, check happens on quiesce.
writeCache(path.basename(uri), text);
LSP_updateFile(uri, text);
addChange(change.document);
});
connection.onHover((params): Hover | null => {
// wait until quiesced (REVIEW after query-based)
if (running) return null
const uri = params.textDocument.uri;
const pos = params.position;
console.log('HOVER', uri, pos)
let res = LSP_hoverInfo(uri, pos.line, pos.character)
if (!res) return null
if (res == true) {
addChange(params.textDocument)
return null
} else {
console.log('HOVER is ', res)
return { contents: { kind: "plaintext", value: res.info } };
}
});
connection.onDefinition((params): Location | null => {
const uri = params.textDocument.uri;
const pos = params.position;
let value = LSP_hoverInfo(uri, pos.line, pos.character)
if (!value || value == true) return null;
return value.location
})
connection.onCodeAction(({textDocument, range}) => {
let actions = LSP_codeActionInfo(textDocument.uri, range.start.line, range.start.character);
console.log('ACTIONS is ', JSON.stringify(actions,null,' '))
return actions
})
connection.onDocumentSymbol((params) => {
try {
const uri = params.textDocument.uri;
let symbols = LSP_docSymbols(uri);
console.log("docs got", symbols)
return symbols;
} catch (e) {
console.error('ERROR in onDocumentSymbol', e);
}
})
connection.onInitialize((_params: InitializeParams): InitializeResult => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
hoverProvider: true,
definitionProvider: true,
codeActionProvider: true,
documentSymbolProvider: true,
},
}));
function writeCache(fn: string, content: string) {
const home = process.env.HOME;
if (!home) return;
const dname = path.join(home, '.cache/newt-lsp');
const fname = path.join(dname, fn);
try {
fs.mkdirSync(dname, {recursive: true});
} catch (e) {
}
try {
fs.writeFileSync(fname, content, 'utf8');
} catch (e) {
console.error(e);
}
}
documents.listen(connection);
connection.listen();
console.log('STARTED')