Update lsp

This commit is contained in:
Andre Henriques 2023-12-31 15:33:43 +00:00
parent 11e7c303e3
commit 7b1e9e063e
4 changed files with 265 additions and 43 deletions

View File

@ -1,6 +1,7 @@
import fs from 'fs';
import process from 'process';
import { diagnosticsRequests, getDiagnostics, parseLsp } from './parser';
import { add_personal } from './languagetool';
if (process.argv.length !== 3) {
console.log("Please provide only one pass");
@ -13,4 +14,7 @@ const res = parseLsp(file);
fs.writeFileSync('./res', res.text);
console.log((await diagnosticsRequests(res)).map(a => a.replacements));
//const diag = await diagnosticsRequests(res);
const diag = await getDiagnostics(file);
//add_personal(['AutoML']);

22
languagetool.ts Normal file
View File

@ -0,0 +1,22 @@
export async function add_personal(words: string[]): Promise<void> {
console.log("adding " + JSON.stringify({ words }));
try {
const res = await fetch('https://api.languagetoolplus.com/enterprise/v1/dictionary/words?type=personal', {
method: 'POST',
headers: {
'Accept': 'application/json',
'authorization': process.env.AUTHORIZATION ?? '',
'content-type': 'application/json',
},
body: JSON.stringify({
words
}),
});
console.log(await res.text());
} catch (e) {
console.log("got error:" + e);
throw e;
}
}

173
lsp.ts
View File

@ -1,9 +1,10 @@
import fs from 'fs';
import * as process from 'process';
import { getDiagnostics } from './parser';
import { getDiagnostics, type GetDiagnosticsReturn } from './parser';
import { add_personal } from './languagetool';
export function log(str: string) {
fs.appendFileSync('/home/sylv/latex-lsp/test', str + "\n");
fs.appendFileSync('/tmp/latex-lsp-test', str + "\n");
}
export function error(str: string) {
@ -26,18 +27,14 @@ type RPCRequest = {
id: string;
jsonrpc: string,
method: string,
params: string,
}
type InitRequest = RPCRequest & {
method: 'initialize',
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'))
@ -91,6 +88,15 @@ type TextDocumentCodeAction = RPCRequest & {
},
};
type WorkspaceExecuteCommand = RPCRequest & {
method: 'workspace/executeCommand',
params: {
command: string,
arguments: any[]
}
}
type Position = {
line: number,
character: number
@ -105,6 +111,7 @@ export type Dialog = {
range: Range,
message: string,
severity: Severity,
data?: any,
}
export enum Severity {
@ -114,7 +121,36 @@ export enum Severity {
Hint = 4,
}
const saveRes: Record<string, (Dialog & {replacements: string[]})[]> = { };
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}`);
@ -131,20 +167,7 @@ async function handleJSON(req: RPCRequest) {
return;
}
const diags = await getDiagnostics(reqO.params.textDocument.text);
saveRes[reqO.params.textDocument.uri] = diags;
const params = {
uri: reqO.params.textDocument.uri,
diagnostics: diags.map(a => ({
message: a.message,
range: a.range,
severity: a.severity,
}) as Dialog),
}
notifyRPC('textDocument/publishDiagnostics', params);
request(reqO.params.textDocument.text, reqO.params.textDocument.uri);
//error(`TODO ${req.method}`);
} else if (req.method === 'textDocument/codeAction') {
@ -153,15 +176,104 @@ async function handleJSON(req: RPCRequest) {
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;
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) {
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: 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;
log(JSON.stringify(req.params));
error("TODO handle " + req.method);
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`);
}
@ -181,7 +293,16 @@ async function handleJSON(req: RPCRequest) {
workspaceDiagnostics: false,
},
codeActionProvider: {
codeActionKinds: [ "quickfix" ]
codeActionKinds: ["quickfix"],
resolveSupport: {
properties: ["edit"],
},
},
executeCommandProvider: {
commands: [
"add_to_dic"
]
}
},
serverInfo: {

107
parser.ts
View File

@ -1,7 +1,8 @@
import fs from 'fs';
import * as fs from 'fs';
import process from 'process';
import { Severity, type Dialog } from './lsp';
import { createSourceFile } from 'typescript';
type ParseResultConversion = {
@ -30,7 +31,8 @@ function parseComment(text: string, curPos: number): number {
return text.length - curPos;
}
function createPartition(text: string, startPos: number, curPos: number, result: string): [ParseResultConversion[], string] | null {
function createPartition(text: string, startPos: number, curPos: number, result: string, ignoreLast: number = 0): [ParseResultConversion[], string] | null {
curPos = curPos - ignoreLast;
if (startPos >= curPos || text.substring(startPos, curPos).match(/^\s*$/)) {
return null;
}
@ -74,7 +76,7 @@ function createPartition(text: string, startPos: number, curPos: number, result:
}
function isChar(charCode: number): boolean {
return (charCode >= 92 && charCode <= 122) || (charCode >= 65 && charCode <= 90);
return (charCode >= 92 && charCode <= 122) || (charCode >= 65 && charCode <= 90) || charCode == 42;
}
function readBalanced(endChar: string, startChar: string, text: string, curPos: number): number {
let bal = 1;
@ -104,19 +106,26 @@ function parseCommand(text: string, curPos: number, result: string): [number, Pa
}
if (text[curPos + 1] === '\\') {
return [1, {
return [2, {
length: 1,
position: result.length,
original_position: curPos + 1,
type: 'text'
}, '\\'];
} else if (text[curPos + 1] === '%') {
return [1, {
return [2, {
length: 1,
position: result.length,
original_position: curPos + 1,
type: 'text'
}, '%'];
} else if (text[curPos + 1] === '_') {
return [2, {
length: 1,
position: result.length,
original_position: curPos + 1,
type: 'text'
}, '_'];
}
let commandName = "";
@ -166,7 +175,7 @@ function parseCommand(text: string, curPos: number, result: string): [number, Pa
}
console.log(text.substring(i - 20, i + 20));
console.log('Char:', char.charCodeAt(0));
console.log('Char:' + char.charCodeAt(0));
throw new Error("TODO handle not char chars in the parse command function");
}
}
@ -175,6 +184,9 @@ function parseCommand(text: string, curPos: number, result: string): [number, Pa
throw new Error("Could not end of the command");
}
//console.log("Parsed '" + text.substring(curPos, len) + "'")
//console.log("Ranged '" + text.substring(curPos - 5 , len + 5) + "'")
len = len - curPos;
switch (commandName) {
@ -195,11 +207,13 @@ function parseCommand(text: string, curPos: number, result: string): [number, Pa
case 'includegraphics':
case 'appendix':
case 'printbibliography':
case 'subsection*':
case 'section*':
return [len];
case 'title':
case 'author':
case 'end':
console.log("TODO: add way to check the", commandName)
console.log("TODO: add way to check the " + commandName)
return [len];
case 'cite':
@ -207,15 +221,49 @@ function parseCommand(text: string, curPos: number, result: string): [number, Pa
return [len];
case 'begin':
case 'item':
console.log("TODO handle", commandName, ":", args)
switch (args[0]) {
case 'verbatim':
const find = '\end{verbatim}';
let endPos = text.indexOf(find, curPos) + find.length;
len = endPos - curPos;
break
default:
console.log("Do not know how to handle " + args[0])
}
return [len];
case 'item':
if (args[0]) {
return [len, {
length: 2 + args[0].length + 1,
original_length: len,
original_position: curPos + commandName.length,
position: result.length,
type: 'text'
}, "— " + args[0] + '\n'];
}
return [len, {
length: 2,
original_length: len,
original_position: curPos,
position: result.length,
type: 'text'
}, "— "];
case 'section':
return [len, {
length: args[0].length + 1,
original_length: len,
original_position: curPos - commandName.length,
original_position: curPos + 2 + commandName.length,
position: result.length,
type: 'h1',
}, args[0] + '\n']
@ -224,13 +272,13 @@ function parseCommand(text: string, curPos: number, result: string): [number, Pa
return [len, {
length: args[0].length + 1,
original_length: len,
original_position: curPos - commandName.length,
original_position: curPos + 2 + commandName.length,
position: result.length,
type: 'h2',
}, args[0] + '\n']
default:
console.log("Command name:", commandName, "options:", options, "args:", args);
console.log("Command name: " + commandName + " options: " + options + " args: " + args);
throw new Error("TODO handle this case");
}
@ -294,7 +342,6 @@ export function parseLsp(text: string): ParseResult {
const [len] = res;
i += len;
} else {
const [len, conv, toAdd] = res;
result.conversions.push(conv);
@ -309,7 +356,7 @@ export function parseLsp(text: string): ParseResult {
throw new Error("Handle double math expression");
}
const possiblePartition = createPartition(text, conversionStartPosition, i - 1, result.text);
const possiblePartition = createPartition(text, conversionStartPosition, i, result.text);
if (possiblePartition) {
const [conv, toAdd] = possiblePartition;
@ -319,6 +366,18 @@ export function parseLsp(text: string): ParseResult {
const len = readUntil(text, '$', i + 1);
let to_add = 'mathexpr' + (text[i + len + 1 + 1] === ' ' ? ' ' : '');
result.conversions = result.conversions.concat(
[{
length: to_add.length,
position: result.text.length,
original_position: i,
type: 'text',
}]
);
result.text += to_add;
i += len + 1;
conversionStartPosition = i + 1;
@ -328,6 +387,8 @@ export function parseLsp(text: string): ParseResult {
}
result.text = result.text.replace(/``/g, "''");
return result;
}
@ -418,7 +479,7 @@ type Match = {
}
export async function diagnosticsRequests(res: ParseResult): Promise < Match[] > {
export async function diagnosticsRequests(res: ParseResult): Promise<Match[]> {
const formData = new URLSearchParams();
formData.set('text', res.text);
@ -436,6 +497,7 @@ export async function diagnosticsRequests(res: ParseResult): Promise < Match[] >
});
if (rawRes.status !== 200) {
console.log("Error:" + (await (await rawRes.blob()).text()))
process.exit(2);
}
const body = await rawRes.json();
@ -443,10 +505,14 @@ export async function diagnosticsRequests(res: ParseResult): Promise < Match[] >
return body.matches;
}
export async function getDiagnostics(file: string): Promise<(Dialog & {replacements: string[]})[]> {
export type GetDiagnosticsReturn = (Dialog & { replacements: string[], rule_id: string, word?: string });
export async function getDiagnostics(file: string): Promise<GetDiagnosticsReturn[]> {
const res = parseLsp(file);
fs.writeFileSync('/tmp/latex-lsp-res', res.text)
const matches = await diagnosticsRequests(res);
const lineIndex = buildLineIndex(file);
@ -460,6 +526,13 @@ export async function getDiagnostics(file: string): Promise<(Dialog & {replaceme
console.log("Could not find the original position")
continue;
}
let word: string | undefined = undefined;
if (match.rule.id.startsWith("MORFOLOGIK_RULE")) {
word = file.substring(original_position, original_position + match.length);
}
const [startLine, startChar] = getLineAndChar(lineIndex, original_position);
const [endLine, endChar] = getLineAndChar(lineIndex, original_position + match.length);
@ -473,6 +546,8 @@ export async function getDiagnostics(file: string): Promise<(Dialog & {replaceme
severity: Severity.Error,
message: match.message,
replacements: match.replacements.map(a => a.value),
rule_id: match.rule.id,
word,
})
}