import fs from 'fs'; import * as process from 'process'; import { getDiagnostics, type GetDiagnosticsReturn } from './parser'; import { add_personal } from './languagetool'; export function log(str: string) { fs.appendFileSync('/tmp/latex-lsp-test', str + "\n"); } export function error(str: string) { log("ERROR:" + str); process.exit(1); } let init = false; type RPCResult = { jsonrpc: string, id: string, } & ({ result: any, } | { error: any, }) type RPCRequest = { id: string; jsonrpc: string, method: string, params: any, } // ASSUMES that is has the right format function sendRPC(obj: any) { const msg = JSON.stringify(obj); const buff = Buffer.from(msg, 'utf-8'); const fullmessage = `Content-Length: ${buff.byteLength}\r\n\r\n${msg}`; log("Sending message: " + fullmessage.replace(/\n/g, '\\n').replace(/\r/g, '\\r')) process.stdout.cork() process.stdout.write(fullmessage); process.stdout.uncork(); } function sendRPCMessage(req: RPCRequest, result: any = undefined, error: any = undefined) { if (!result && !error) { error("Invalid rpc message to send") return; } sendRPC({ id: req.id, jsonrpc: req.jsonrpc, result, error } as RPCResult) } function notifyRPC(method: string, params: any) { sendRPC({ jsonrpc: '2.0', method, params, }) } type TextDocumentDidOpen = RPCRequest & { method: 'textDocument/didOpen', params: { textDocument: { uri: string; languageId: string; version: number; text: string; } } } type TextDocumentCodeAction = RPCRequest & { method: 'textDocument/codeAction', params: { range: Range, textDocument: { uri: string, } }, }; type WorkspaceExecuteCommand = RPCRequest & { method: 'workspace/executeCommand', params: { command: string, arguments: any[] } } type Position = { line: number, character: number } type Range = { start: Position, end: Position, } export type Dialog = { range: Range, message: string, severity: Severity, data?: any, } export enum Severity { Error = 1, Warning = 2, Information = 3, Hint = 4, } let lastRequest: null | Date = null; const saveRes: Record = {}; async function request(file: string, uri: string) { if (lastRequest) { if (((new Date()).getTime() - lastRequest.getTime()) <= 60 * 1000) { return; } } lastRequest = new Date(); let diags = await getDiagnostics(file); saveRes[uri] = diags; const params = { uri: uri, diagnostics: diags.map((a, i) => ({ message: a.message, range: a.range, severity: a.severity, data: i, }) as Dialog), } notifyRPC('textDocument/publishDiagnostics', params); return diags; } async function handleJSON(req: RPCRequest) { log(`New Message entered: method: ${req.method}`); if (init) { if (req.method === 'initialized') { // On init confirm do nothing return; } else if (req.method === 'textDocument/didOpen') { const reqO: TextDocumentDidOpen = req as any; if (reqO.params.textDocument.languageId !== 'latex') { error(`This server only supports latex! Got: ${reqO.params.textDocument.languageId}`) return; } request(reqO.params.textDocument.text, reqO.params.textDocument.uri); //error(`TODO ${req.method}`); } else if (req.method === 'textDocument/codeAction') { const reqO: TextDocumentCodeAction = req as any; const diagRecord = saveRes[reqO.params.textDocument.uri]; if (!diagRecord) { sendRPCMessage(req, [{ title: "File does not seam to be loaded" }]); return; } const saved = saveRes[req.params.textDocument.uri]; const character = req.params.range.start.character; const line = req.params.range.start.line; if (!req.params.context?.diagnostics[0]) { sendRPCMessage(req, [{ title: "No diagnostics found in this file" }]); return; } let item: GetDiagnosticsReturn | undefined = undefined; console.log(JSON.stringify(req.params) + "\n\n\n"); console.log(JSON.stringify(req.params.context.diagnostics[0]) + "\n\n\n"); if (typeof req.params.context?.diagnostics[0].data != 'number') { for (const _item of saved) { if (_item.range.end.line >= line && _item.range.start.line <= line) { if (_item.range.end.character >= character && _item.range.start.character <= line) { item = _item as GetDiagnosticsReturn; break; } } } } else { console.log("Using " + req.params.context.diagnostics[0].data) item = saved[req.params.context.diagnostics[0].data]; } if (item === undefined) { sendRPCMessage(req, [{ title: "No diagnostics found in this file" }]); return; } if (item.replacements.length === 0) { sendRPCMessage(req, [{ title: item.message, }]); return; } const items: { title: string, edit?: { changes: Record }, command?: { title: string, command: string, arguments?: any[] }, }[] = item.replacements.map((value) => ( { title: value, edit: { changes: { [reqO.params.textDocument.uri]: [ { range: reqO.params.context.diagnostics[0].range, newText: value } ] } } } )); if (item.rule_id.startsWith("MORFOLOGIK_RULE")) { items.push({ title: "Add to personal dictinoary", command: { title: "Add to personal dictionary", command: "add_to_dic", arguments: [ reqO.params.textDocument.uri, item.word, ] } }); } sendRPCMessage(req, items); return; } else if (req.method === 'workspace/executeCommand') { let reqO: WorkspaceExecuteCommand = req as any; log(JSON.stringify(req.params)); if (reqO.params.command === 'add_to_dic') { if (reqO.params.arguments[0] && reqO.params.arguments[1]) { await add_personal([reqO.params.arguments[1]]); } return; } error(`TODO Handle: ${req.method} ${JSON.stringify(req.params)}`); } else { error(`Handle: ${req.method} after init`); } } else { if (req.method === 'initialize') { log("Recived init"); init = true; log(JSON.stringify(req)); sendRPCMessage(req, { capabilities: { diagnosticProvider: { // TODO change in the future when the server also checks the ib file as well interFileDependencies: false, workspaceDiagnostics: false, }, codeActionProvider: { codeActionKinds: ["quickfix"], resolveSupport: { properties: ["edit"], }, }, executeCommandProvider: { commands: [ "add_to_dic" ] } }, serverInfo: { name: "Super Cool latex server", version: "1.0.0", } }); return; } else { error(`Expected init method found '${req.method}'`) } } } export function handleData(data: Buffer) { let strData = data.toString(); let jsonData = strData.split('\n'); let req: RPCRequest; if (jsonData.length === 3) { try { req = JSON.parse(jsonData[2]); } catch (e) { log("Failed to parse json 1/2! Msg:\n"); log(strData.replace(/\n/g, '\\n').replace(/\r/g, '\\r')); log("\n\nFailed to parse json 2/2! JSON Line:\n"); log(jsonData[2]); return; } handleJSON(req); return; } for (let i = 2; i < jsonData.length; i += 2) { let jsonLine = jsonData[i]; if (i != jsonData.length - 1) { let index = jsonLine.indexOf('Content-Length'); if (index == -1) { error("Handling multiline expected 'Content-Length'"); return; } try { req = JSON.parse(jsonLine.substring(0, index)); } catch (e) { log("Failed to parse json 1/2! Msg:\n"); log(strData.replace(/\n/g, '\\n').replace(/\r/g, '\\r')); log("\n\nFailed to parse json 2/2! JSON Line:\n"); log(jsonLine.substring(0, index)); return; } } else { try { req = JSON.parse(jsonLine); } catch (e) { log("Failed to parse json 1/2! Msg:\n"); log(strData.replace(/\n/g, '\\n').replace(/\r/g, '\\r')); log("\n\nFailed to parse json 2/2! JSON Line:\n"); log(jsonLine); return; } } handleJSON(req); } }