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