latex-lsp/lsp.ts

385 lines
8.8 KiB
TypeScript

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<string, GetDiagnosticsReturn[]> = {};
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<string, { range: Range, newText: string }[]>
},
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);
}
}