diff --git a/index.ts b/index.ts index 1b5060b..6d93201 100644 --- a/index.ts +++ b/index.ts @@ -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']); diff --git a/languagetool.ts b/languagetool.ts new file mode 100644 index 0000000..0542ad5 --- /dev/null +++ b/languagetool.ts @@ -0,0 +1,22 @@ +export async function add_personal(words: string[]): Promise { + 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; + } +} diff --git a/lsp.ts b/lsp.ts index 4dcd39d..4827694 100644 --- a/lsp.ts +++ b/lsp.ts @@ -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 = { }; +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}`); @@ -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 + }, + 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: { diff --git a/parser.ts b/parser.ts index 46d0ad7..21c9589 100644 --- a/parser.ts +++ b/parser.ts @@ -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 { 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 { 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, }) }