├── .gitignore ├── .vscodeignore ├── LICENSE ├── README.md ├── assets ├── texpresso_logo.png └── texpresso_logo.svg ├── package-lock.json ├── package.json ├── src ├── extension.ts ├── rope.ts └── test │ └── extension.test.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | /src/rope 132 | /internal 133 | /examples 134 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | assets/texpresso_logo.svg 2 | internal 3 | src/rope -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dominik Peters 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TeXpresso VS Code Extension 2 | Visual Studio Code extension for interacting with [TeXpresso](https://github.com/let-def/texpresso/), which provides a "live rendering" experience when editing LaTeX documents. TeXpresso needs to be installed separately to use this extension, following its [install guide](https://github.com/let-def/texpresso/blob/main/INSTALL.md) which contains instructions for macOS, Fedora, Arch Linux, Debian, and Ubuntu. The extension can be used simultaneously with other LaTeX extensions such as [LaTeX Workshop](https://github.com/James-Yu/LaTeX-Workshop). 3 | 4 | ![ezgif-6-3b2ad402f4](https://github.com/DominikPeters/texpresso-vscode/assets/3543224/0ff5cf57-5a2e-48cd-9e5f-633a5ed44411) 5 | 6 | After installing the extension, you need to configure the path to the TeXpresso binary in the settings. 7 | 8 | To use this extension, open the `.tex` document you wish to edit, then open the command pallete (Ctrl + Shift + P), and select `TeXpresso: Start Document`. A separate window will open showing the compiled preview. The preview immediately updates when you edit the file in VS Code, and using SyncTeX the preview automatically jumps to the current code position (and vice versa for clicks in the preview window). Buttons at the top of the editor are provided to switch pages, and a compile log for seeing compilation errors can be found by using the Output panel. 9 | 10 | ## Features 11 | 12 | To change pages, use the buttons: 13 | ![recording10](https://github.com/DominikPeters/texpresso-vscode/assets/3543224/2dbfb081-409e-4f31-b3af-e64cea25414b) 14 | 15 | Use SyncTeX (forwards - editor to preview): 16 | ![recording11](https://github.com/DominikPeters/texpresso-vscode/assets/3543224/80824192-f9e9-4f71-9959-df5ed7d5d617) 17 | 18 | Use SyncTeX (backwards - preview to editor): 19 | ![recording12](https://github.com/DominikPeters/texpresso-vscode/assets/3543224/4a9c7709-275f-48d5-b6f9-dcaeede0c622) 20 | 21 | Use theme colors in preview (if the `useEditorTheme` setting is activated): 22 | 23 | 24 | ## Requirements 25 | 26 | TeXpresso must be installed, and must be callable at the path provided in the `texpresso.command` setting. 27 | 28 | ## Extension Settings 29 | 30 | This extension contributes the following settings: 31 | 32 | * `texpresso.command`: The path to the texpresso binary. 33 | * `texpresso.useWSL`: Controls whether to run TeXpresso within Windows Subsystem for Linux (WSL). 34 | * `texpresso.syncTeXForwardOnSelection`: Controls whether the preview should be updated when the selection in the editor changes. 35 | * `texpresso.useEditorTheme`: Controls whether the preview should use the same color theme as the editor. 36 | 37 | ## Architecture 38 | 39 | TeXpresso and the underlying LaTeX compilers are based on a UTF-8 byte representation, and [communication between the editor and TeXpresso](https://github.com/let-def/texpresso/blob/main/EDITOR-PROTOCOL.md) occurs in terms of byte offsets. However, VS Code only provides access to character positions and not their byte position. Thus, this extension keeps a copy of the current document in a *rope* data structure ([wikipedia](https://en.wikipedia.org/wiki/Rope_(data_structure))), enriched with byte offsets. This allows for efficient conversion between character and byte positions, and also allows for efficient edits to the underlying text string. The [code for the rope data structure](https://github.com/DominikPeters/texpresso-vscode/blob/master/src/rope.ts) builds on https://github.com/component/rope. 40 | 41 | ## Known Issues 42 | 43 | The extension does not yet react instantaenously to changes to files that are included using commands like `\input`. 44 | 45 | Loses connection if the filename of the main document is changed. 46 | 47 | ## Release Notes 48 | 49 | ### 1.4.0 50 | 51 | Add support for executing TeXpresso on WSL (Windows Subsystem for Linux). 52 | 53 | ### 1.3.0 54 | 55 | Add a button for toggling automatic SyncTeX forward when the selection changes. 56 | 57 | Turn off the extension when the TeXpresso window is closed. 58 | 59 | ### 1.2.0 60 | 61 | Added support for color themes. 62 | 63 | ### 1.1.0 64 | 65 | Changed settings identifiers. 66 | 67 | ### 1.0.0 68 | 69 | Initial release of the extension. 70 | -------------------------------------------------------------------------------- /assets/texpresso_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DominikPeters/texpresso-vscode/30dfb45280517f03231dacc93c45f2d7fd8421d8/assets/texpresso_logo.png -------------------------------------------------------------------------------- /assets/texpresso_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 37 | 39 | 41 | 45 | 50 | 51 | 53 | 58 | 63 | 64 | 74 | 76 | 80 | 85 | 86 | 93 | 97 | 102 | 103 | 111 | 119 | 123 | 128 | 129 | 137 | 141 | 146 | 147 | 155 | 159 | 164 | 168 | 169 | 178 | 187 | 196 | 205 | 215 | 224 | 228 | 233 | 234 | 242 | 246 | 251 | 252 | 261 | 265 | 269 | 270 | 279 | 283 | 287 | 291 | 296 | 297 | 306 | 316 | 324 | 328 | 329 | 337 | 341 | 342 | 343 | 347 | 349 | 350 | 351 | image/svg+xml 352 | 354 | 356 | 357 | 359 | Openclipart 360 | 361 | 362 | 363 | 365 | 367 | 369 | 371 | 372 | 373 | 374 | 377 | 380 | 387 | 393 | 397 | 401 | 404 | 409 | 410 | 411 | 417 | 423 | 429 | 435 | 442 | 448 | 454 | 462 | 468 | 474 | 478 | 482 | 486 | 487 | 495 | 501 | 507 | 514 | 520 | 528 | 535 | 541 | 547 | 552 | 553 | 558 | 562 | 566 | 567 | 570 | 573 | 576 | 579 | 580 | 581 | 582 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "texpresso-basic", 3 | "displayName": "TeXpresso", 4 | "description": "Basic extension for interacting with TeXpresso for live previewing LaTeX documents", 5 | "publisher": "DominikPeters", 6 | "version": "1.6.0", 7 | "icon": "./assets/texpresso_logo.png", 8 | "homepage": "https://github.com/DominikPeters/texpresso-vscode", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/DominikPeters/texpresso-vscode.git" 12 | }, 13 | "engines": { 14 | "vscode": "^1.88.0" 15 | }, 16 | "categories": [ 17 | "Education", 18 | "Other" 19 | ], 20 | "keywords": [ 21 | "texpresso", 22 | "latex", 23 | "tex", 24 | "preview", 25 | "syncTeX" 26 | ], 27 | "activationEvents": [], 28 | "main": "./dist/extension.js", 29 | "license": "MIT", 30 | "contributes": { 31 | "commands": [ 32 | { 33 | "command": "texpresso.startDocument", 34 | "title": "TeXpresso: Start Document" 35 | }, 36 | { 37 | "command": "texpresso.refresh", 38 | "title": "TeXpresso: Refresh", 39 | "enablement": "texpresso.running" 40 | }, 41 | { 42 | "command": "texpresso.externalCompileAndRefresh", 43 | "title": "TeXpresso: External Compile and Refresh", 44 | "enablement": "texpresso.running" 45 | }, 46 | { 47 | "command": "texpresso.previousPage", 48 | "title": "TeXpresso: Show Previous Page in Preview Window", 49 | "icon": "$(arrow-circle-left)", 50 | "enablement": "texpresso.inActiveEditor" 51 | }, 52 | { 53 | "command": "texpresso.nextPage", 54 | "title": "TeXpresso: Show Next Page in Preview Window", 55 | "icon": "$(arrow-circle-right)", 56 | "enablement": "texpresso.inActiveEditor" 57 | }, 58 | { 59 | "command": "texpresso.syncTeXForward", 60 | "title": "TeXpresso: Show Current Position in Preview Window (SyncTeX)", 61 | "shortTitle": "TeXpresso SyncTeX", 62 | "enablement": "texpresso.inActiveEditor" 63 | }, 64 | { 65 | "command": "texpresso.syncTeXForwardShortName", 66 | "title": "TeXpresso SyncTeX", 67 | "enablement": "texpresso.inActiveEditor" 68 | }, 69 | { 70 | "command": "texpresso.activateSyncTeXForward", 71 | "title": "TeXpresso: Activate SyncTeX Forward", 72 | "enablement": "texpresso.inActiveEditor && !config.texpresso.syncTeXForwardOnSelection", 73 | "icon": "$(sync-ignored)" 74 | }, 75 | { 76 | "command": "texpresso.deactivateSyncTeXForward", 77 | "title": "TeXpresso: Deactivate SyncTeX Forward", 78 | "enablement": "texpresso.inActiveEditor && config.texpresso.syncTeXForwardOnSelection", 79 | "icon": "$(sync)" 80 | }, 81 | { 82 | "command": "texpresso.adoptTheme", 83 | "title": "TeXpresso: Adopt Editor Color Theme", 84 | "enablement": "texpresso.running" 85 | }, 86 | { 87 | "command": "texpresso.defaultTheme", 88 | "title": "TeXpresso: Adopt Default Color Theme", 89 | "enablement": "texpresso.running" 90 | }, 91 | { 92 | "command": "texpresso.stop", 93 | "title": "TeXpresso: Stop", 94 | "enablement": "texpresso.running" 95 | } 96 | ], 97 | "configuration": { 98 | "title": "TeXpresso", 99 | "properties": { 100 | "texpresso.command": { 101 | "type": "string", 102 | "default": "texpresso", 103 | "description": "Path to the texpresso binary" 104 | }, 105 | "texpresso.externalCompileCommand": { 106 | "type": "string", 107 | "default": "latexmk", 108 | "description": "External command for multi-pass compilation of LaTeX documents" 109 | }, 110 | "texpresso.useWSL": { 111 | "type": "boolean", 112 | "default": false, 113 | "description": "Specifies whether texpresso should be run in WSL (Windows Subsystem for Linux)" 114 | }, 115 | "texpresso.syncTeXForwardOnSelection": { 116 | "type": "boolean", 117 | "default": true, 118 | "description": "Specifies whether the preview should be updated when the selection in the editor changes (SyncTeX forward)" 119 | }, 120 | "texpresso.useEditorTheme": { 121 | "type": "boolean", 122 | "default": false, 123 | "description": "Specifies whether the preview should use the same color theme as the editor (if activated), or the default theme (otherwise)" 124 | } 125 | } 126 | }, 127 | "menus": { 128 | "editor/title": [ 129 | { 130 | "when": "texpresso.inActiveEditor", 131 | "command": "texpresso.previousPage", 132 | "group": "navigation@1" 133 | }, 134 | { 135 | "when": "texpresso.inActiveEditor", 136 | "command": "texpresso.nextPage", 137 | "group": "navigation@2" 138 | }, 139 | { 140 | "when": "texpresso.inActiveEditor && !config.texpresso.syncTeXForwardOnSelection", 141 | "command": "texpresso.activateSyncTeXForward", 142 | "group": "navigation@1" 143 | }, 144 | { 145 | "when": "texpresso.inActiveEditor && config.texpresso.syncTeXForwardOnSelection", 146 | "command": "texpresso.deactivateSyncTeXForward", 147 | "group": "navigation@1" 148 | } 149 | ], 150 | "editor/context": [ 151 | { 152 | "when": "texpresso.inActiveEditor", 153 | "command": "texpresso.syncTeXForwardShortName", 154 | "group": "2_preview" 155 | } 156 | ], 157 | "commandPalette": [ 158 | { 159 | "command": "texpresso.syncTeXForwardShortName", 160 | "when": "false" 161 | } 162 | ] 163 | } 164 | }, 165 | "scripts": { 166 | "vscode:prepublish": "npm run package", 167 | "compile": "webpack", 168 | "watch": "webpack --watch", 169 | "package": "webpack --mode production --devtool hidden-source-map", 170 | "compile-tests": "tsc -p . --outDir out", 171 | "watch-tests": "tsc -p . -w --outDir out", 172 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 173 | "lint": "eslint src --ext ts", 174 | "test": "vscode-test" 175 | }, 176 | "devDependencies": { 177 | "@types/mocha": "^10.0.6", 178 | "@types/node": "18.x", 179 | "@types/tmp": "^0.2.6", 180 | "@types/vscode": "^1.88.0", 181 | "@typescript-eslint/eslint-plugin": "^7.7.1", 182 | "@typescript-eslint/parser": "^7.7.1", 183 | "@vscode/test-cli": "^0.0.8", 184 | "@vscode/test-electron": "^2.3.9", 185 | "eslint": "^8.57.0", 186 | "ts-loader": "^9.5.1", 187 | "typescript": "^5.4.5", 188 | "webpack": "^5.91.0", 189 | "webpack-cli": "^5.1.4" 190 | }, 191 | "dependencies": { 192 | "tmp": "^0.2.3" 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ChildProcess, spawn, execSync } from 'child_process'; 3 | import * as tmp from 'tmp'; 4 | import * as fs from 'fs'; 5 | 6 | import Rope from './rope'; 7 | 8 | tmp.setGracefulCleanup(); 9 | 10 | let texpresso: ChildProcess; 11 | let rope: Rope; 12 | let filePath: string; 13 | 14 | export function activate(context: vscode.ExtensionContext) { 15 | 16 | if (texpresso) { 17 | texpresso.kill(); 18 | } 19 | 20 | const outputChannel = vscode.window.createOutputChannel('TeXpresso', { log: true }); 21 | const debugChannel = vscode.window.createOutputChannel('TeXpresso Debug', { log: true }); 22 | let providedOutput = ""; 23 | let outputChanged = true; 24 | 25 | const useWSL = vscode.workspace.getConfiguration('texpresso').get('useWSL') as boolean; 26 | 27 | let activeEditor: vscode.TextEditor | undefined; 28 | 29 | function startDocumentFromEditor(editor: vscode.TextEditor | undefined) { 30 | if (texpresso) { 31 | texpresso.kill(); 32 | } 33 | 34 | if (editor) { 35 | activeEditor = editor; 36 | // vscode.window.showInformationMessage('Starting TeXpresso for this document'); 37 | filePath = activeEditor.document.fileName; 38 | const text = activeEditor.document.getText(); 39 | rope = new Rope(text); 40 | if (activeEditor.document.isUntitled) { 41 | const tmpDir = tmp.dirSync(); 42 | filePath = tmpDir.name + '/untitled.tex'; 43 | fs.writeFileSync(filePath, text); 44 | // on macOS, the temp file goes into a symlinked directory, so we need to resolve the path 45 | filePath = fs.realpathSync(filePath); 46 | } 47 | 48 | vscode.commands.executeCommand('setContext', 'texpresso.inActiveEditor', true); 49 | vscode.commands.executeCommand('setContext', 'texpresso.running', true); 50 | // Start texpresso 51 | const command = vscode.workspace.getConfiguration('texpresso').get('command') as string; 52 | // Check if command exists 53 | try { 54 | if (!useWSL) { 55 | fs.accessSync(command, fs.constants.X_OK); 56 | } else { 57 | execSync(`wsl -e test -x ${command}`); 58 | } 59 | } catch (error) { 60 | let message = `TeXpresso command '${command}' does not exist or is not executable. Please check the 'texpresso.command' setting.`; 61 | if (process.platform === 'win32') { 62 | message = `TeXpresso command '${command}' does not exist or is not executable. Please check the 'texpresso.command' and 'texpresso.wsl' settings.`; 63 | if (useWSL) { 64 | message = `TeXpresso command '${command}' is not executable, or the 'wsl' command is not available. Please check the 'texpresso.command' and 'texpresso.wsl' settings.`; 65 | } 66 | } 67 | vscode.window.showErrorMessage( 68 | message, 69 | 'Open Settings' 70 | ).then(value => { 71 | if (value === 'Open Settings') { 72 | vscode.commands.executeCommand('workbench.action.openSettings', 'texpresso.'); 73 | } 74 | }); 75 | return; 76 | } 77 | 78 | // react to messages from texpresso 79 | if (!useWSL) { 80 | texpresso = spawn(command, ['-json', filePath]); 81 | } else { 82 | filePath = execSync(`wsl wslpath "${filePath}"`).toString().trim(); 83 | texpresso = spawn('wsl', ['-e', command, '-json', filePath]); 84 | } 85 | if (texpresso && texpresso.stdout) { 86 | texpresso.stdout.on('data', data => { 87 | const message = JSON.parse(data.toString()); 88 | if (message[0] === 'synctex' && message[1] === activeEditor?.document.fileName) { 89 | const line = message[2]; 90 | activeEditor?.revealRange(new vscode.Range(line - 1, 0, line - 1, 0)); 91 | } 92 | else if (message[0] === 'append' && message[1] === 'log') { 93 | providedOutput += message[3]; 94 | outputChanged = true; 95 | } 96 | else if (message[0] === 'truncate' && message[1] === 'log') { 97 | const bytesToKeep = message[2]; 98 | providedOutput = providedOutput.slice(-bytesToKeep); 99 | outputChanged = true; 100 | } 101 | else if (message[0] === 'flush') { 102 | if (outputChanged) { 103 | outputChanged = false; 104 | outputChannel.replace(providedOutput); 105 | } 106 | } 107 | else { 108 | console.log("Received unhandled message", message); 109 | } 110 | }); 111 | } 112 | if (texpresso && texpresso.stderr) { 113 | texpresso.stderr.on('data', data => { 114 | debugChannel.append(data.toString()); 115 | }); 116 | } 117 | if (texpresso) { 118 | texpresso.on('close', code => { 119 | if (code !== 0) { 120 | vscode.window.showErrorMessage(`TeXpresso exited with code ${code}`); 121 | } 122 | activeEditor = undefined; 123 | vscode.commands.executeCommand('setContext', 'texpresso.inActiveEditor', false); 124 | vscode.commands.executeCommand('setContext', 'texpresso.running', false); 125 | outputChannel.clear(); 126 | debugChannel.clear(); 127 | }); 128 | } 129 | // Send file content to texpresso 130 | const message = ["open", filePath, activeEditor.document.getText()]; 131 | if (texpresso && texpresso.stdin) { 132 | texpresso.stdin.write(JSON.stringify(message) + '\n'); 133 | } 134 | // Send color theme to texpresso if setting is enabled 135 | if (vscode.workspace.getConfiguration('texpresso').get('useEditorTheme') as boolean) { 136 | adoptTheme(); 137 | } 138 | } 139 | } 140 | 141 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.startDocument', () => { 142 | startDocumentFromEditor(vscode.window.activeTextEditor); 143 | })); 144 | 145 | vscode.workspace.onDidChangeTextDocument(event => { 146 | if (activeEditor && event.document === activeEditor.document) { 147 | // send change message to texpresso via stdin 148 | texpresso?.stdin?.cork(); 149 | for (const change of event.contentChanges) { 150 | // get byte offsets of change 151 | const start = rope.byteOffset(change.rangeOffset); 152 | const end = rope.byteOffset(change.rangeOffset + change.rangeLength); 153 | // send change message 154 | const message = ["change", filePath, start, end - start, change.text]; 155 | texpresso?.stdin?.write(JSON.stringify(message) + '\n'); 156 | // implement change in rope 157 | rope.remove(change.rangeOffset, change.rangeOffset + change.rangeLength); 158 | rope.insert(change.rangeOffset, change.text); 159 | } 160 | texpresso?.stdin?.uncork(); 161 | } 162 | }, null, context.subscriptions); 163 | 164 | vscode.window.onDidChangeActiveTextEditor(editor => { 165 | if (activeEditor && editor && editor.document === activeEditor.document) { 166 | vscode.commands.executeCommand('setContext', 'texpresso.inActiveEditor', true); 167 | } else { 168 | vscode.commands.executeCommand('setContext', 'texpresso.inActiveEditor', false); 169 | } 170 | }); 171 | 172 | /*********************************** 173 | *********************************** 174 | **** SyncTeX and Page Choice **** 175 | *********************************** 176 | **********************************/ 177 | 178 | let previouslySentLineNumber: number | undefined; 179 | function doSyncTeXForward(editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor) { 180 | if (!editor || editor.document !== activeEditor?.document) { 181 | return; 182 | } 183 | const lineNumber = editor.selection.active.line; 184 | if (!previouslySentLineNumber || previouslySentLineNumber !== lineNumber) { 185 | previouslySentLineNumber = lineNumber; 186 | if (texpresso && texpresso.stdin) { 187 | const message = ["synctex-forward", filePath, lineNumber]; 188 | texpresso.stdin.write(JSON.stringify(message) + '\n'); 189 | } 190 | } 191 | } 192 | 193 | vscode.window.onDidChangeTextEditorSelection(event => { 194 | if (activeEditor 195 | && event.textEditor.document === activeEditor.document 196 | && vscode.workspace.getConfiguration('texpresso').get('syncTeXForwardOnSelection') as boolean) { 197 | doSyncTeXForward(event.textEditor); 198 | } 199 | }); 200 | 201 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.syncTeXForward', () => { 202 | doSyncTeXForward(); 203 | })); 204 | 205 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.syncTeXForwardShortName', (event) => { 206 | doSyncTeXForward(); 207 | })); 208 | 209 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.activateSyncTeXForward', () => { 210 | vscode.workspace.getConfiguration('texpresso').update('syncTeXForwardOnSelection', true, true); 211 | })); 212 | 213 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.deactivateSyncTeXForward', () => { 214 | vscode.workspace.getConfiguration('texpresso').update('syncTeXForwardOnSelection', false, true); 215 | })); 216 | 217 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.nextPage', () => { 218 | if (activeEditor) { 219 | const message = ["next-page"]; 220 | texpresso?.stdin?.write(JSON.stringify(message) + '\n'); 221 | } 222 | })); 223 | 224 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.previousPage', () => { 225 | if (activeEditor) { 226 | const message = ["previous-page"]; 227 | texpresso?.stdin?.write(JSON.stringify(message) + '\n'); 228 | } 229 | })); 230 | 231 | /*********************************** 232 | *********************************** 233 | **** Theme Color Adaption **** 234 | *********************************** 235 | **********************************/ 236 | 237 | function adoptTheme() { 238 | if (!texpresso || !activeEditor) { 239 | return; 240 | } 241 | // to get explicit colors of the theme (which are not available in the API), we need to open a webview 242 | // see https://github.com/microsoft/vscode/issues/32813#issuecomment-798680103 243 | const webviewPanel = vscode.window.createWebviewPanel( 244 | 'texpresso.colorTheme', 245 | 'Color Theme', 246 | { preserveFocus: true, viewColumn: vscode.ViewColumn.Beside }, 247 | { 248 | enableScripts: true, 249 | } 250 | ); 251 | const webview = webviewPanel.webview; 252 | webview.html = ``; 264 | webview.onDidReceiveMessage((cssVars) => { 265 | webviewPanel.dispose(); 266 | const colors = {} as { [key: string]: number[] }; 267 | for (const cssVar of cssVars) { 268 | const key = Object.keys(cssVar)[0]; 269 | const value = cssVar[key]; 270 | if (key === '--vscode-editor-background' || key === '--vscode-editor-foreground') { 271 | // value is for example "#cccccc" 272 | // convert it to rgb as three floats between 0 and 1 273 | const rgb = value.match(/#(..)(..)(..)/); 274 | if (rgb) { 275 | colors[key] = [ 276 | parseInt(rgb[1], 16) / 255, 277 | parseInt(rgb[2], 16) / 255, 278 | parseInt(rgb[3], 16) / 255, 279 | ]; 280 | } 281 | } 282 | } 283 | const message = ["theme", colors['--vscode-editor-background'], colors['--vscode-editor-foreground']]; 284 | texpresso?.stdin?.write(JSON.stringify(message) + '\n'); 285 | }); 286 | } 287 | 288 | vscode.window.onDidChangeActiveColorTheme(() => { 289 | if (vscode.workspace.getConfiguration('texpresso').get('useEditorTheme') as boolean) { 290 | adoptTheme(); 291 | } 292 | }); 293 | 294 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.adoptTheme', () => { 295 | adoptTheme(); 296 | })); 297 | 298 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.defaultTheme', () => { 299 | if (texpresso) { 300 | const message = ["theme", [1, 1, 1], [0, 0, 0]]; 301 | texpresso.stdin?.write(JSON.stringify(message) + '\n'); 302 | } 303 | })); 304 | 305 | /*********************************** 306 | *********************************** 307 | ***** Refresh and Stop ***** 308 | *********************************** 309 | **********************************/ 310 | 311 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.refresh', () => { 312 | if (activeEditor) { 313 | const text = activeEditor.document.getText(); 314 | rope = new Rope(text); 315 | // resend "open" command 316 | const message = ["open", filePath, text]; 317 | texpresso?.stdin?.write(JSON.stringify(message) + '\n'); 318 | texpresso?.stdin?.write(JSON.stringify(["rescan"]) + '\n'); 319 | } 320 | })); 321 | 322 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.externalCompileAndRefresh', async () => { 323 | if (!activeEditor) { 324 | vscode.window.showErrorMessage('TeXpresso: No active editor'); 325 | return; 326 | } 327 | 328 | // Save the document first if it has unsaved changes 329 | if (activeEditor.document.isDirty) { 330 | await activeEditor.document.save(); 331 | } 332 | 333 | // Get the configured compile command 334 | const externalCommand = vscode.workspace.getConfiguration('texpresso').get('externalCompileCommand') as string; 335 | 336 | // Show a status bar message 337 | const statusBar = vscode.window.setStatusBarMessage(`TeXpresso: Running ${externalCommand}...`); 338 | 339 | try { 340 | // Execute the command 341 | const { exec } = require('child_process'); 342 | const path = require('path'); 343 | const workDir = path.dirname(filePath); 344 | 345 | outputChannel.appendLine(`\n--- External Compilation ---`); 346 | outputChannel.appendLine(`Working directory: ${workDir}`); 347 | 348 | // Prepare the command 349 | let cmd: string; 350 | let opts: any = {}; 351 | 352 | if (!useWSL) { 353 | cmd = `${externalCommand} "${filePath}"`; 354 | opts.cwd = workDir; 355 | outputChannel.appendLine(`Command: ${cmd}`); 356 | } else { 357 | // When using WSL, we need to convert the Windows path to a WSL path 358 | const wslFilePath = execSync(`wsl wslpath "${filePath}"`).toString().trim(); 359 | const wslWorkDir = path.dirname(wslFilePath); 360 | cmd = `cd "${wslWorkDir}" && ${externalCommand} "${wslFilePath}"`; 361 | outputChannel.appendLine(`WSL Command: ${cmd}`); 362 | cmd = `wsl -e sh -c "${cmd.replace(/"/g, '\\"')}"`; 363 | } 364 | 365 | outputChannel.show(true); 366 | 367 | // Execute the command 368 | const childProcess = exec(cmd, opts); 369 | 370 | await new Promise((resolve, reject) => { 371 | childProcess.stdout.on('data', (data: Buffer) => { 372 | outputChannel.append(data.toString()); 373 | }); 374 | 375 | childProcess.stderr.on('data', (data: Buffer) => { 376 | outputChannel.append(data.toString()); 377 | }); 378 | 379 | childProcess.on('close', (code: number) => { 380 | if (code === 0) { 381 | resolve(); 382 | } else { 383 | const errorMsg = `Exit code ${code}`; 384 | outputChannel.appendLine(`\nExternal compilation failed: ${errorMsg}`); 385 | reject(new Error(errorMsg)); 386 | } 387 | }); 388 | 389 | childProcess.on('error', (error: Error) => { 390 | outputChannel.appendLine(`\nError: ${error.message}`); 391 | reject(error); 392 | }); 393 | }); 394 | 395 | // Refresh TeXpresso's internal state 396 | vscode.commands.executeCommand('texpresso.refresh'); 397 | } catch (error) { 398 | const errorMessage = error instanceof Error ? error.message : String(error); 399 | outputChannel.appendLine(`\nError: ${errorMessage}`); 400 | vscode.window.showErrorMessage(`TeXpresso: External compilation failed: ${errorMessage}`); 401 | } finally { 402 | statusBar.dispose(); 403 | } 404 | })); 405 | 406 | context.subscriptions.push(vscode.commands.registerCommand('texpresso.stop', () => { 407 | if (texpresso) { 408 | texpresso.kill(); 409 | } 410 | activeEditor = undefined; 411 | vscode.commands.executeCommand('setContext', 'texpresso.inActiveEditor', false); 412 | vscode.commands.executeCommand('setContext', 'texpresso.running', false); 413 | outputChannel.clear(); 414 | debugChannel.clear(); 415 | })); 416 | } 417 | 418 | // This method is called when your extension is deactivated 419 | export function deactivate() { 420 | if (texpresso) { 421 | texpresso.kill(); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/rope.ts: -------------------------------------------------------------------------------- 1 | // This code derived from https://github.com/component/rope 2 | 3 | // The MIT License (MIT) 4 | 5 | // Copyright (c) 2014 Automattic, Inc. 6 | 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | const SPLIT_LENGTH = 1000; 26 | const JOIN_LENGTH = 500; 27 | const REBALANCE_RATIO = 1.2; 28 | 29 | const textEncoder = new TextEncoder(); 30 | function byteLength(str : string) : number { 31 | return textEncoder.encode(str).length; 32 | } 33 | 34 | /** 35 | * Creates a rope data structure 36 | * 37 | * @param {String} str - String to populate the rope. 38 | * @api public 39 | */ 40 | 41 | class Rope { 42 | private _value?: string; 43 | public length: number; 44 | public bytes: number; 45 | private _left?: Rope; 46 | private _right?: Rope; 47 | 48 | constructor(str: string) { 49 | this._value = str; 50 | this.length = str.length; 51 | this.bytes = byteLength(str); 52 | this.adjust(); 53 | } 54 | 55 | private adjust() { 56 | if (typeof this._value != 'undefined') { 57 | if (this.length > SPLIT_LENGTH) { 58 | const divide = Math.floor(this.length / 2); 59 | this._left = new Rope(this._value.substring(0, divide)); 60 | this._right = new Rope(this._value.substring(divide)); 61 | delete this._value; 62 | } 63 | } else if (this._left && this._right) { 64 | if (this.length < JOIN_LENGTH) { 65 | this._value = this._left.toString() + this._right.toString(); 66 | delete this._left; 67 | delete this._right; 68 | } 69 | } 70 | } 71 | 72 | public toString(): string { 73 | if (typeof this._value != 'undefined') { 74 | return this._value; 75 | } else if (this._left && this._right) { 76 | return this._left.toString() + this._right.toString(); 77 | } 78 | return ''; 79 | } 80 | 81 | public remove(start: number, end: number): void { 82 | if (start < 0 || start > this.length) throw new RangeError('Start is not within rope bounds.'); 83 | if (end < 0 || end > this.length) throw new RangeError('End is not within rope bounds.'); 84 | if (start > end) throw new RangeError('Start is greater than end.'); 85 | if (typeof this._value != 'undefined') { 86 | this._value = this._value.substring(0, start) + this._value.substring(end); 87 | this.length = this._value.length; 88 | this.bytes = byteLength(this._value); 89 | } else if (this._left && this._right) { 90 | const leftLength = this._left.length; 91 | const leftStart = Math.min(start, leftLength); 92 | const leftEnd = Math.min(end, leftLength); 93 | const rightLength = this._right.length; 94 | const rightStart = Math.max(0, Math.min(start - leftLength, rightLength)); 95 | const rightEnd = Math.max(0, Math.min(end - leftLength, rightLength)); 96 | if (leftStart < leftLength) { 97 | this._left.remove(leftStart, leftEnd); 98 | } 99 | if (rightEnd > 0) { 100 | this._right.remove(rightStart, rightEnd); 101 | } 102 | this.length = this._left.length + this._right.length; 103 | this.bytes = this._left.bytes + this._right.bytes; 104 | } 105 | this.adjust(); 106 | } 107 | 108 | public insert(position: number, value: string): void { 109 | if (position < 0 || position > this.length) throw new RangeError('position is not within rope bounds.'); 110 | if (typeof this._value != 'undefined') { 111 | this._value = this._value.substring(0, position) + value.toString() + this._value.substring(position); 112 | } else if (this._left && this._right) { 113 | const leftLength = this._left.length; 114 | if (position < leftLength) { 115 | this._left.insert(position, value); 116 | } else { 117 | this._right.insert(position - leftLength, value); 118 | } 119 | } 120 | this.length += value.length; 121 | this.bytes += byteLength(value); 122 | this.adjust(); 123 | } 124 | 125 | public rebuild(): void { 126 | if (this._left && this._right) { 127 | this._value = this._left.toString() + this._right.toString(); 128 | delete this._left; 129 | delete this._right; 130 | this.adjust(); 131 | } 132 | } 133 | 134 | public rebalance(): void { 135 | if (this._left && this._right) { 136 | if (this._left.length / this._right.length > REBALANCE_RATIO || 137 | this._right.length / this._left.length > REBALANCE_RATIO) { 138 | this.rebuild(); 139 | } else { 140 | this._left.rebalance(); 141 | this._right.rebalance(); 142 | } 143 | } 144 | } 145 | 146 | public byteOffset(position: number): number { 147 | if (typeof this._value != 'undefined') { 148 | return byteLength(this._value.substring(0, position)); 149 | } else if (this._left && this._right) { 150 | const leftLength = this._left.length; 151 | if (position < leftLength) { 152 | return this._left.byteOffset(position); 153 | } else { 154 | return this._left.bytes + this._right.byteOffset(position - leftLength); 155 | } 156 | } 157 | return 0; 158 | } 159 | } 160 | 161 | export default Rope; -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "lib": [ 6 | "ES2022" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "moduleResolution": "Node16", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; --------------------------------------------------------------------------------