Inital work on the lsp
This commit is contained in:
commit
57682ed72b
178
.gitignore
vendored
Normal file
178
.gitignore
vendored
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
res
|
||||||
|
test
|
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# latex-lsp
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.0.20. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
16
index.ts
Normal file
16
index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import process from 'process';
|
||||||
|
import { getDiagnostics, parseLsp } from './parser';
|
||||||
|
|
||||||
|
if (process.argv.length !== 3) {
|
||||||
|
console.log("Please provide only one pass");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.readFileSync(process.argv[2]).toString();
|
||||||
|
|
||||||
|
const res = parseLsp(file);
|
||||||
|
|
||||||
|
fs.writeFileSync('./res', res.text);
|
||||||
|
|
||||||
|
console.log(await getDiagnostics(file));
|
218
lsp.ts
Normal file
218
lsp.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import * as process from 'process';
|
||||||
|
import { getDiagnostics } from './parser';
|
||||||
|
|
||||||
|
export function log(str: string) {
|
||||||
|
fs.appendFileSync('/home/sylv/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: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitRequest = RPCRequest & {
|
||||||
|
method: 'initialize',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 Position = {
|
||||||
|
line: number,
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Range = {
|
||||||
|
start: Position,
|
||||||
|
end: Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dialog = {
|
||||||
|
range: Range,
|
||||||
|
message: string,
|
||||||
|
severity: Severity,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Severity {
|
||||||
|
Error = 1,
|
||||||
|
Warning = 2,
|
||||||
|
Information = 3,
|
||||||
|
Hint = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
uri: reqO.params.textDocument.uri,
|
||||||
|
diagnostics: (await getDiagnostics(reqO.params.textDocument.text)),
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyRPC('textDocument/publishDiagnostics', params);
|
||||||
|
|
||||||
|
|
||||||
|
//error(`TODO ${req.method}`);
|
||||||
|
} 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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
13
lsp_run.ts
Normal file
13
lsp_run.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { error, handleData, log } from "./lsp";
|
||||||
|
|
||||||
|
console.log = log;
|
||||||
|
|
||||||
|
process.stdin.resume();
|
||||||
|
|
||||||
|
process.stdin.on('data', function(data) {
|
||||||
|
try {
|
||||||
|
handleData(data);
|
||||||
|
} catch (e) {
|
||||||
|
error("TRY GOT ERROR:" + e)
|
||||||
|
}
|
||||||
|
})
|
11
package.json
Normal file
11
package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "latex-lsp",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
474
parser.ts
Normal file
474
parser.ts
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import process from 'process';
|
||||||
|
import { Severity, type Dialog } from './lsp';
|
||||||
|
|
||||||
|
|
||||||
|
type ParseResultConversion = {
|
||||||
|
length: number,
|
||||||
|
original_length?: number,
|
||||||
|
original_position: number,
|
||||||
|
position: number,
|
||||||
|
type: 'text' | 'h1' | 'h2'
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParseResult = {
|
||||||
|
text: string,
|
||||||
|
originalString: string,
|
||||||
|
conversions: ParseResultConversion[],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the number of character skiped
|
||||||
|
// This puts the i at the \n so it's skiped
|
||||||
|
function parseComment(text: string, curPos: number): number {
|
||||||
|
for (let i = curPos + 1; i < text.length; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
if (char === '\n') {
|
||||||
|
return i - curPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.length - curPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPartition(text: string, startPos: number, curPos: number, result: string): [ParseResultConversion[], string] | null {
|
||||||
|
if (startPos >= curPos || text.substring(startPos, curPos).match(/^\s*$/)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var t = text.substring(startPos, curPos);
|
||||||
|
|
||||||
|
if (t.indexOf('\n') == -1) {
|
||||||
|
return [[{
|
||||||
|
length: curPos - startPos,
|
||||||
|
position: result.length,
|
||||||
|
original_position: startPos,
|
||||||
|
type: 'text',
|
||||||
|
}], t];
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = t.split('\n');
|
||||||
|
|
||||||
|
let nt = "";
|
||||||
|
const convs: ParseResultConversion[] = [];
|
||||||
|
|
||||||
|
let n = startPos;
|
||||||
|
|
||||||
|
let pos = result.length;
|
||||||
|
|
||||||
|
for (const line of split) {
|
||||||
|
let nLine = line.replace(/^\s*/, '');
|
||||||
|
n += line.length - nLine.length;
|
||||||
|
nt += nLine + '\n';
|
||||||
|
convs.push({
|
||||||
|
length: nLine.length,
|
||||||
|
original_position: n,
|
||||||
|
position: pos,
|
||||||
|
type: 'text'
|
||||||
|
});
|
||||||
|
pos += nLine.length + 1;
|
||||||
|
n += nLine.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [convs, nt];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChar(charCode: number): boolean {
|
||||||
|
return (charCode >= 92 && charCode <= 122) || (charCode >= 65 && charCode <= 90);
|
||||||
|
}
|
||||||
|
function readBalanced(endChar: string, startChar: string, text: string, curPos: number): number {
|
||||||
|
let bal = 1;
|
||||||
|
for (let i = curPos; i < text.length; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
if (char == endChar) {
|
||||||
|
if (bal == 1) {
|
||||||
|
return i - curPos + 1;
|
||||||
|
} else {
|
||||||
|
bal -= 1;
|
||||||
|
}
|
||||||
|
} else if (char == startChar) {
|
||||||
|
bal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Can not find end of balance read")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWhiteSpace(char: string): boolean {
|
||||||
|
return [' ', '\t', '\n'].includes(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCommand(text: string, curPos: number, result: string): [number, ParseResultConversion, string] | [number] {
|
||||||
|
|
||||||
|
if (text.length - 1 == curPos) {
|
||||||
|
throw new Error("The latex file has the wrong format the file can not end with a empty command");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[curPos + 1] === '\\') {
|
||||||
|
return [1, {
|
||||||
|
length: 1,
|
||||||
|
position: result.length,
|
||||||
|
original_position: curPos + 1,
|
||||||
|
type: 'text'
|
||||||
|
}, '\\'];
|
||||||
|
} else if (text[curPos + 1] === '%') {
|
||||||
|
return [1, {
|
||||||
|
length: 1,
|
||||||
|
position: result.length,
|
||||||
|
original_position: curPos + 1,
|
||||||
|
type: 'text'
|
||||||
|
}, '%'];
|
||||||
|
}
|
||||||
|
|
||||||
|
let commandName = "";
|
||||||
|
let commandNameFinished = false;
|
||||||
|
|
||||||
|
// TODO store the location of the opts and args
|
||||||
|
let options = [];
|
||||||
|
let args = [];
|
||||||
|
|
||||||
|
let findEnd = false;
|
||||||
|
|
||||||
|
|
||||||
|
let len = 0;
|
||||||
|
|
||||||
|
for (let i = curPos + 1; i < text.length; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
if (isChar(char.charCodeAt(0))) {
|
||||||
|
if (!commandNameFinished) {
|
||||||
|
commandName += char;
|
||||||
|
} else {
|
||||||
|
len = i;
|
||||||
|
findEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (char === '[') {
|
||||||
|
commandNameFinished = true;
|
||||||
|
const len = readBalanced(']', '[', text, i + 1);
|
||||||
|
options.push(text.substring(i + 1, i + len))
|
||||||
|
i += len;
|
||||||
|
} else if (char === '{') {
|
||||||
|
commandNameFinished = true;
|
||||||
|
const len = readBalanced('}', '{', text, i + 1);
|
||||||
|
args.push(text.substring(i + 1, i + len))
|
||||||
|
i += len;
|
||||||
|
} else if (isWhiteSpace(char)) {
|
||||||
|
len = i;
|
||||||
|
findEnd = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (char == '.' || char == ',') {
|
||||||
|
if (commandNameFinished) {
|
||||||
|
len = i;
|
||||||
|
findEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(text.substring(i - 20, i + 20));
|
||||||
|
console.log('Char:', char.charCodeAt(0));
|
||||||
|
throw new Error("TODO handle not char chars in the parse command function");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!findEnd) {
|
||||||
|
throw new Error("Could not end of the command");
|
||||||
|
}
|
||||||
|
|
||||||
|
len = len - curPos;
|
||||||
|
|
||||||
|
switch (commandName) {
|
||||||
|
case 'documentclass':
|
||||||
|
case 'usepackage':
|
||||||
|
case 'graphicspath':
|
||||||
|
case 'hypersetup':
|
||||||
|
case 'pagestyle':
|
||||||
|
case 'fancyhead':
|
||||||
|
case 'fancyfoot':
|
||||||
|
case 'renewcommand':
|
||||||
|
case 'setlength':
|
||||||
|
case 'addbibresource':
|
||||||
|
case 'date':
|
||||||
|
case 'maketitle':
|
||||||
|
case 'newpage':
|
||||||
|
case 'tableofcontents':
|
||||||
|
case 'includegraphics':
|
||||||
|
case 'appendix':
|
||||||
|
case 'printbibliography':
|
||||||
|
return [len];
|
||||||
|
case 'title':
|
||||||
|
case 'author':
|
||||||
|
case 'end':
|
||||||
|
console.log("TODO: add way to check the", commandName)
|
||||||
|
return [len];
|
||||||
|
|
||||||
|
case 'cite':
|
||||||
|
console.log("TODO check if it exists on the bibliography");
|
||||||
|
return [len];
|
||||||
|
|
||||||
|
case 'begin':
|
||||||
|
case 'item':
|
||||||
|
console.log("TODO handle", commandName, ":", args)
|
||||||
|
return [len];
|
||||||
|
|
||||||
|
case 'section':
|
||||||
|
return [len, {
|
||||||
|
length: args[0].length + 1,
|
||||||
|
original_length: len,
|
||||||
|
original_position: curPos - commandName.length,
|
||||||
|
position: result.length,
|
||||||
|
type: 'h1',
|
||||||
|
}, args[0] + '\n']
|
||||||
|
|
||||||
|
case 'subsection':
|
||||||
|
return [len, {
|
||||||
|
length: args[0].length + 1,
|
||||||
|
original_length: len,
|
||||||
|
original_position: curPos - commandName.length,
|
||||||
|
position: result.length,
|
||||||
|
type: 'h2',
|
||||||
|
}, args[0] + '\n']
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log("Command name:", commandName, "options:", options, "args:", args);
|
||||||
|
throw new Error("TODO handle this case");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function readUntil(text: string, char: string, curPos: number): number {
|
||||||
|
for (let i = curPos; i < text.length; i++) {
|
||||||
|
if (text[i] === char) {
|
||||||
|
return i - curPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Could not find matching pair");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLsp(text: string): ParseResult {
|
||||||
|
const result: ParseResult = {
|
||||||
|
text: '',
|
||||||
|
originalString: text,
|
||||||
|
conversions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let conversionStartPosition = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
let char = text[i];
|
||||||
|
|
||||||
|
if (char === '%') {
|
||||||
|
//console.log("Found comment");
|
||||||
|
|
||||||
|
const possiblePartition = createPartition(text, conversionStartPosition, i - 1, result.text);
|
||||||
|
|
||||||
|
if (possiblePartition) {
|
||||||
|
const [conv, toAdd] = possiblePartition;
|
||||||
|
result.conversions = result.conversions.concat(conv)
|
||||||
|
result.text += toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const len = parseComment(text, i);
|
||||||
|
|
||||||
|
i += len;
|
||||||
|
|
||||||
|
// Skip the begining \n
|
||||||
|
conversionStartPosition = i + 1;
|
||||||
|
|
||||||
|
} else if (char === '\\') {
|
||||||
|
//console.log("Found command")
|
||||||
|
|
||||||
|
const possiblePartition = createPartition(text, conversionStartPosition, i - 1, result.text);
|
||||||
|
|
||||||
|
if (possiblePartition) {
|
||||||
|
const [conv, toAdd] = possiblePartition;
|
||||||
|
result.conversions = result.conversions.concat(conv);
|
||||||
|
result.text += toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = parseCommand(text, i, result.text);
|
||||||
|
|
||||||
|
if (res.length === 1) {
|
||||||
|
const [len] = res;
|
||||||
|
i += len;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const [len, conv, toAdd] = res;
|
||||||
|
|
||||||
|
result.conversions.push(conv);
|
||||||
|
result.text += toAdd;
|
||||||
|
i += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversionStartPosition = i + 1;
|
||||||
|
} else if (char === '$') {
|
||||||
|
console.log('Found math expr')
|
||||||
|
if (text[i + 1] === '$') {
|
||||||
|
throw new Error("Handle double math expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
const possiblePartition = createPartition(text, conversionStartPosition, i - 1, result.text);
|
||||||
|
|
||||||
|
if (possiblePartition) {
|
||||||
|
const [conv, toAdd] = possiblePartition;
|
||||||
|
result.conversions = result.conversions.concat(conv);
|
||||||
|
result.text += toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const len = readUntil(text, '$', i + 1);
|
||||||
|
|
||||||
|
i += len + 1;
|
||||||
|
|
||||||
|
conversionStartPosition = i + 1;
|
||||||
|
} else {
|
||||||
|
//console.log(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineAndChar(lineIndex: number[], offset: number): [number, number] {
|
||||||
|
let l = 0;
|
||||||
|
let r = lineIndex.length;
|
||||||
|
|
||||||
|
while (r >= l) {
|
||||||
|
const i = Math.floor((r + l) / 2);
|
||||||
|
//console.log(i, l ,r, offset, lineIndex[i]);
|
||||||
|
|
||||||
|
if (lineIndex[i + 1] < offset) {
|
||||||
|
l = i + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lineIndex[i] > offset) {
|
||||||
|
r = i - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return [i, offset - lineIndex[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [-1, -1];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getOriginalPostion(res: ParseResult, offset: number): number {
|
||||||
|
let l = 0;
|
||||||
|
let r = res.conversions.length;
|
||||||
|
|
||||||
|
while (r >= l) {
|
||||||
|
const i = Math.floor((r + l) / 2);
|
||||||
|
const conv = res.conversions[i];
|
||||||
|
|
||||||
|
if (conv.position > offset) {
|
||||||
|
r = i - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conv.position + conv.length < offset) {
|
||||||
|
l = i + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conv.original_position + (offset - conv.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLineIndex(file: string) {
|
||||||
|
const lines = file.split('\n');
|
||||||
|
let i = 0;
|
||||||
|
const lineIndex = [0];
|
||||||
|
for (const line of lines) {
|
||||||
|
i += line.length + 1;
|
||||||
|
lineIndex.push(i);
|
||||||
|
}
|
||||||
|
return lineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDiagnostics(file: string): Promise<Dialog[]> {
|
||||||
|
|
||||||
|
const res = parseLsp(file);
|
||||||
|
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
|
||||||
|
formData.set('text', res.text);
|
||||||
|
formData.set('language', 'en-GB');
|
||||||
|
formData.set('username', process.env.USERNAME ?? '');
|
||||||
|
formData.set('apiKey', process.env.APIKEY ?? '');
|
||||||
|
formData.set('level', 'picky');
|
||||||
|
|
||||||
|
const rawRes = await fetch('https://api.languagetoolplus.com/v2/check', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rawRes.status !== 200) {
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
const body = await rawRes.json();
|
||||||
|
|
||||||
|
type Match = {
|
||||||
|
message: string,
|
||||||
|
shortMessage: string,
|
||||||
|
offset: number,
|
||||||
|
length: number,
|
||||||
|
replacements: {
|
||||||
|
value: string
|
||||||
|
}[],
|
||||||
|
context: {
|
||||||
|
text: string,
|
||||||
|
offset: number,
|
||||||
|
length: number
|
||||||
|
},
|
||||||
|
sentence: string,
|
||||||
|
rule: {
|
||||||
|
id: string,
|
||||||
|
subId: string,
|
||||||
|
description: string,
|
||||||
|
urls: {
|
||||||
|
value: string
|
||||||
|
}[],
|
||||||
|
issueType: string,
|
||||||
|
category: {
|
||||||
|
id: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineIndex = buildLineIndex(file);
|
||||||
|
|
||||||
|
const diags = [];
|
||||||
|
|
||||||
|
for (const i of body.matches) {
|
||||||
|
const match: Match = i;
|
||||||
|
const original_position = getOriginalPostion(res, match.offset);
|
||||||
|
if (original_position == -1) {
|
||||||
|
console.log("Could not find the original position")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [startLine, startChar] = getLineAndChar(lineIndex, original_position);
|
||||||
|
const [endLine, endChar] = getLineAndChar(lineIndex, original_position + match.length);
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
start: { line: startLine, character: startChar },
|
||||||
|
end: { line: endLine, character: endChar },
|
||||||
|
}
|
||||||
|
|
||||||
|
diags.push({
|
||||||
|
range,
|
||||||
|
severity: Severity.Error,
|
||||||
|
message: match.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags;
|
||||||
|
}
|
||||||
|
|
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user