2023-12-25 22:44:16 +00:00
|
|
|
import fs from 'fs';
|
|
|
|
import * as process from 'process';
|
2023-12-31 15:33:43 +00:00
|
|
|
import { getDiagnostics, type GetDiagnosticsReturn } from './parser';
|
|
|
|
import { add_personal } from './languagetool';
|
2023-12-25 22:44:16 +00:00
|
|
|
|
|
|
|
export function log(str: string) {
|
2023-12-31 15:33:43 +00:00
|
|
|
fs.appendFileSync('/tmp/latex-lsp-test', str + "\n");
|
2023-12-25 22:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2023-12-31 15:33:43 +00:00
|
|
|
params: any,
|
2023-12-25 22:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ASSUMES that is has the right format
|
|
|
|
function sendRPC(obj: any) {
|
|
|
|
const msg = JSON.stringify(obj);
|
|
|
|
const buff = Buffer.from(msg, 'utf-8');
|
2023-12-31 15:33:43 +00:00
|
|
|
|
2023-12-25 22:44:16 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-26 20:01:14 +00:00
|
|
|
type TextDocumentCodeAction = RPCRequest & {
|
|
|
|
method: 'textDocument/codeAction',
|
|
|
|
params: {
|
|
|
|
range: Range,
|
|
|
|
textDocument: {
|
|
|
|
uri: string,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-12-31 15:33:43 +00:00
|
|
|
type WorkspaceExecuteCommand = RPCRequest & {
|
|
|
|
method: 'workspace/executeCommand',
|
|
|
|
params: {
|
|
|
|
command: string,
|
|
|
|
arguments: any[]
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-12-25 22:44:16 +00:00
|
|
|
type Position = {
|
|
|
|
line: number,
|
|
|
|
character: number
|
|
|
|
}
|
|
|
|
|
|
|
|
type Range = {
|
|
|
|
start: Position,
|
|
|
|
end: Position,
|
|
|
|
}
|
|
|
|
|
|
|
|
export type Dialog = {
|
|
|
|
range: Range,
|
|
|
|
message: string,
|
|
|
|
severity: Severity,
|
2023-12-31 15:33:43 +00:00
|
|
|
data?: any,
|
2023-12-25 22:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum Severity {
|
|
|
|
Error = 1,
|
|
|
|
Warning = 2,
|
|
|
|
Information = 3,
|
|
|
|
Hint = 4,
|
|
|
|
}
|
|
|
|
|
2023-12-31 15:33:43 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-12-26 20:01:14 +00:00
|
|
|
|
2023-12-25 22:44:16 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-12-31 15:33:43 +00:00
|
|
|
request(reqO.params.textDocument.text, reqO.params.textDocument.uri);
|
2023-12-25 22:44:16 +00:00
|
|
|
|
|
|
|
//error(`TODO ${req.method}`);
|
2023-12-26 20:01:14 +00:00
|
|
|
} else if (req.method === 'textDocument/codeAction') {
|
|
|
|
const reqO: TextDocumentCodeAction = req as any;
|
|
|
|
|
|
|
|
const diagRecord = saveRes[reqO.params.textDocument.uri];
|
|
|
|
|
|
|
|
if (!diagRecord) {
|
2023-12-31 15:33:43 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
let item: GetDiagnosticsReturn | undefined = undefined;
|
|
|
|
|
|
|
|
console.log(JSON.stringify(req.params));
|
|
|
|
|
|
|
|
if (!req.params.context?.diagnostics || !req.params.context?.diagnostics[0] || 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) {
|
2023-12-26 20:01:14 +00:00
|
|
|
sendRPCMessage(req, [{
|
|
|
|
title: "No diagnostics found in this file"
|
|
|
|
}]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-12-31 15:33:43 +00:00
|
|
|
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: item!.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;
|
|
|
|
|
2023-12-26 20:01:14 +00:00
|
|
|
log(JSON.stringify(req.params));
|
2023-12-31 15:33:43 +00:00
|
|
|
|
|
|
|
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)}`);
|
2023-12-25 22:44:16 +00:00
|
|
|
} 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,
|
2023-12-26 20:01:14 +00:00
|
|
|
},
|
|
|
|
codeActionProvider: {
|
2023-12-31 15:33:43 +00:00
|
|
|
codeActionKinds: ["quickfix"],
|
|
|
|
|
|
|
|
resolveSupport: {
|
|
|
|
properties: ["edit"],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
executeCommandProvider: {
|
|
|
|
commands: [
|
|
|
|
"add_to_dic"
|
|
|
|
]
|
2023-12-25 22:44:16 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|