├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── esbuild.js ├── eslint.config.mjs ├── images ├── banner.png ├── blame.gif ├── describe.png ├── diff.png ├── edit.gif ├── edit_description.png ├── logo.png ├── merge.gif ├── restore.png ├── squash.png ├── squash_range.webp └── undo.gif ├── languages └── jj-commit.language-configuration.json ├── package-lock.json ├── package.json ├── src ├── config.toml ├── decorationProvider.ts ├── fakeeditor │ ├── .gitignore │ ├── build.zig │ ├── build.zig.zon │ ├── build_all_platforms.sh │ └── src │ │ └── main.zig ├── fileSystemProvider.ts ├── graphWebview.ts ├── logger.ts ├── main.ts ├── operationLogTreeView.ts ├── repository.ts ├── test │ ├── all-tests.ts │ ├── fakeeditor.test.ts │ ├── main.test.ts │ ├── repository.test.ts │ ├── runTest.ts │ ├── runner.ts │ └── utils.ts ├── uri.ts ├── utils.ts ├── vendor │ ├── vscode │ │ ├── base │ │ │ └── common │ │ │ │ ├── arrays.ts │ │ │ │ ├── arraysFind.ts │ │ │ │ ├── assert.ts │ │ │ │ ├── charCode.ts │ │ │ │ ├── diff │ │ │ │ ├── diff.ts │ │ │ │ └── diffChange.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── map.ts │ │ │ │ ├── strings.ts │ │ │ │ └── uint.ts │ │ └── editor │ │ │ └── common │ │ │ ├── core │ │ │ ├── editOperation.ts │ │ │ ├── lineRange.ts │ │ │ ├── offsetEdit.ts │ │ │ ├── offsetRange.ts │ │ │ ├── position.ts │ │ │ ├── positionToOffset.ts │ │ │ ├── range.ts │ │ │ ├── textEdit.ts │ │ │ └── textLength.ts │ │ │ └── diff │ │ │ ├── defaultLinesDiffComputer │ │ │ ├── algorithms │ │ │ │ ├── diffAlgorithm.ts │ │ │ │ ├── dynamicProgrammingDiffing.ts │ │ │ │ └── myersDiffAlgorithm.ts │ │ │ ├── computeMovedLines.ts │ │ │ ├── defaultLinesDiffComputer.ts │ │ │ ├── heuristicSequenceOptimizations.ts │ │ │ ├── lineSequence.ts │ │ │ ├── linesSliceCharSequence.ts │ │ │ └── utils.ts │ │ │ ├── legacyLinesDiffComputer.ts │ │ │ ├── linesDiffComputer.ts │ │ │ ├── linesDiffComputers.ts │ │ │ └── rangeMapping.ts │ └── winston-transport-vscode │ │ └── logOutputChannelTransport.ts └── webview │ ├── graph.css │ └── graph.html ├── syntaxes └── jj-commit.tmLanguage.json ├── tsconfig.json └── vsc-extension-quickstart.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | node_modules/ 4 | dist/ 5 | out/ 6 | .vscode-test/ 7 | .vscode/settings.json 8 | *.vsix 9 | src/**/*.js 10 | src/**/*.js.map 11 | .jj -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/vendor -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": [ 9 | "npm: watch:tsc", 10 | "npm: watch:esbuild" 11 | ], 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch:esbuild", 23 | "group": "build", 24 | "problemMatcher": "$esbuild-watch", 25 | "isBackground": true, 26 | "label": "npm: watch:esbuild", 27 | "presentation": { 28 | "group": "watch", 29 | "reveal": "never" 30 | } 31 | }, 32 | { 33 | "type": "npm", 34 | "script": "watch:tsc", 35 | "group": "build", 36 | "problemMatcher": "$tsc-watch", 37 | "isBackground": true, 38 | "label": "npm: watch:tsc", 39 | "presentation": { 40 | "group": "watch", 41 | "reveal": "never" 42 | } 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "watch-tests", 47 | "problemMatcher": "$tsc-watch", 48 | "isBackground": true, 49 | "presentation": { 50 | "reveal": "never", 51 | "group": "watchers" 52 | }, 53 | "group": "build" 54 | }, 55 | { 56 | "label": "tasks: watch-tests", 57 | "dependsOn": [ 58 | "npm: watch", 59 | "npm: watch-tests" 60 | ], 61 | "problemMatcher": [] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | !dist/** 7 | !images/** 8 | .gitignore 9 | .yarnrc 10 | esbuild.js 11 | vsc-extension-quickstart.md 12 | **/tsconfig.json 13 | **/eslint.config.mjs 14 | **/*.map 15 | **/*.ts 16 | **/.vscode-test.* 17 | .jj 18 | *.vsix 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Please see [https://github.com/keanemind/jjk/releases](https://github.com/keanemind/jjk/releases) for detailed release notes! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 Keane Nguyen, Kevin Lin 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 | # Jujutsu Kaizen 2 | 3 | ![banner](images/banner.png) 4 | 5 | > A Visual Studio Code extension for the [Jujutsu (jj) version control system](https://github.com/jj-vcs/jj). 6 | 7 | [![VS Code Extension](https://img.shields.io/visual-studio-marketplace/v/jjk.jjk)](https://marketplace.visualstudio.com/items?itemName=jjk.jjk) 8 | [![Discord](https://img.shields.io/discord/968932220549103686?color=5865F2&label=Discord&logo=discord&logoColor=white)](https://discord.gg/FV8qcSZS) 9 | 10 | ## 🚀 Features 11 | 12 | The goal of this extension is to bring the great UX of Jujutsu into the VS Code UI. We are currently focused on achieving parity for commonly used features of VS Code's built-in Git extension, such as the various operations possible via the Source Control view. 13 | 14 | Here's what you can do so far: 15 | 16 | ### 📁 File Management 17 | 18 | - Track file statuses in the Working Copy 19 | - Monitor file statuses across all parent changes 20 | - View detailed file diffs for Working Copy and parent modifications 21 | ![view file diff](images/diff.png) 22 | - View line-by-line blame 23 | view blame 24 | 25 | ### 💫 Change Management 26 | 27 | - Create new changes with optional descriptions 28 | - Edit descriptions for Working Copy and parent changes 29 | ![edit description](images/describe.png) 30 | - Move changes between Working Copy and parents 31 | ![squash](images/squash.png) 32 | - Move specific lines from the Working Copy to its parent changes 33 | ![squash range](images/squash_range.webp) 34 | - Discard changes 35 | ![restore](images/restore.png) 36 | - Browse and navigate revision history 37 | revision history 38 | - Create merge changes 39 | revision history 40 | 41 | ### 🔄 Operation Management 42 | 43 | - Undo jj operations or restore to a previous state 44 | undo 45 | 46 | ## 📋 Prerequisites 47 | 48 | - Ensure `jj` is installed and available in your system's `$PATH` 49 | 50 | ## 🐛 Known Issues 51 | 52 | If you encounter any problems, please [report them on GitHub](https://github.com/keanemind/jjk/issues/)! 53 | 54 | ## 📝 License 55 | 56 | This project is licensed under the [MIT License](LICENSE). 57 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | const production = process.argv.includes("--production"); 4 | const watch = process.argv.includes("--watch"); 5 | const isTest = process.argv.includes("--test"); 6 | 7 | /** 8 | * @type {import('esbuild').Plugin} 9 | */ 10 | const esbuildProblemMatcherPlugin = { 11 | name: "esbuild-problem-matcher", 12 | 13 | setup(build) { 14 | build.onStart(() => { 15 | console.log("[watch] build started"); 16 | }); 17 | build.onEnd((result) => { 18 | result.errors.forEach(({ text, location }) => { 19 | console.error(`✘ [ERROR] ${text}`); 20 | console.error( 21 | ` ${location.file}:${location.line}:${location.column}:`, 22 | ); 23 | }); 24 | console.log("[watch] build finished"); 25 | }); 26 | }, 27 | }; 28 | 29 | async function main() { 30 | if (isTest) { 31 | // 1. Build the test launcher (runTest.ts) 32 | const launcherCtx = await esbuild.context({ 33 | entryPoints: ["src/test/runTest.ts"], 34 | bundle: true, 35 | format: "cjs", 36 | platform: "node", 37 | outfile: "out/test/runTest.js", 38 | external: ["@vscode/test-electron"], 39 | logLevel: "silent", 40 | plugins: [esbuildProblemMatcherPlugin], 41 | }); 42 | await launcherCtx.rebuild(); 43 | await launcherCtx.dispose(); 44 | console.log("Test launcher built: out/test/runTest.js"); 45 | 46 | // 2. Build the actual test suite bundle (all-tests.ts) 47 | // This bundles all *.test.ts files (via imports in all-tests.ts) 48 | // and their src/ dependencies (like uri.ts and its dependency arktype). 49 | const allTestsBundleCtx = await esbuild.context({ 50 | entryPoints: ["src/test/all-tests.ts"], 51 | bundle: true, 52 | format: "cjs", 53 | platform: "node", // Runs in VS Code extension host 54 | outfile: "out/test/all-tests.js", 55 | external: ["vscode", "mocha"], 56 | sourcemap: true, 57 | logLevel: "silent", 58 | plugins: [esbuildProblemMatcherPlugin], 59 | }); 60 | await allTestsBundleCtx.rebuild(); 61 | await allTestsBundleCtx.dispose(); 62 | console.log("All tests bundle built: out/test/all-tests.js"); 63 | 64 | // 3. Build the runner (runner.ts) 65 | // This script will load and run the all-tests.js bundle using Mocha. 66 | const suiteRunnerCtx = await esbuild.context({ 67 | entryPoints: ["src/test/runner.ts"], 68 | bundle: true, 69 | format: "cjs", 70 | platform: "node", // Runs in VS Code extension host 71 | outfile: "out/test/runner.js", 72 | external: ["vscode", "mocha"], 73 | logLevel: "silent", 74 | plugins: [esbuildProblemMatcherPlugin], 75 | }); 76 | await suiteRunnerCtx.rebuild(); 77 | await suiteRunnerCtx.dispose(); 78 | console.log("Test suite runner built: out/test/runner.js"); 79 | } else { 80 | // Production/watch build for src/main.ts (extension code) 81 | const ctx = await esbuild.context({ 82 | entryPoints: ["src/main.ts"], 83 | bundle: true, 84 | format: "cjs", 85 | minify: production, 86 | sourcemap: !production, 87 | sourcesContent: false, 88 | platform: "node", 89 | outfile: "dist/main.js", 90 | external: ["vscode"], 91 | logLevel: "silent", 92 | plugins: [esbuildProblemMatcherPlugin], 93 | }); 94 | if (watch) { 95 | await ctx.watch(); 96 | } else { 97 | await ctx.rebuild(); 98 | await ctx.dispose(); 99 | } 100 | } 101 | } 102 | 103 | main().catch((e) => { 104 | console.error(e); 105 | process.exit(1); 106 | }); 107 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import globals from "globals"; 5 | 6 | export default tseslint.config( 7 | { 8 | ignores: ["dist/", "src/vendor"], 9 | }, 10 | eslint.configs.recommended, 11 | { 12 | languageOptions: { 13 | globals: { 14 | ...globals.node, 15 | }, 16 | }, 17 | }, 18 | { 19 | files: ["**/*.ts"], 20 | extends: [tseslint.configs.recommendedTypeChecked], 21 | plugins: { 22 | "@typescript-eslint": tseslint.plugin, 23 | }, 24 | 25 | languageOptions: { 26 | parser: tseslint.parser, 27 | ecmaVersion: 2022, 28 | sourceType: "module", 29 | parserOptions: { 30 | projectService: true, 31 | tsconfigRootDir: import.meta.dirname, 32 | }, 33 | }, 34 | 35 | rules: { 36 | "@typescript-eslint/naming-convention": [ 37 | "warn", 38 | { 39 | selector: "import", 40 | format: ["camelCase", "PascalCase"], 41 | }, 42 | ], 43 | "@typescript-eslint/prefer-promise-reject-errors": [ 44 | "error", 45 | { allowThrowingUnknown: true }, 46 | ], 47 | "@typescript-eslint/no-unused-vars": [ 48 | "error", 49 | { 50 | args: "all", 51 | argsIgnorePattern: "^_", 52 | caughtErrors: "all", 53 | caughtErrorsIgnorePattern: "^_", 54 | destructuredArrayIgnorePattern: "^_", 55 | varsIgnorePattern: "^_", 56 | ignoreRestSiblings: true, 57 | }, 58 | ], 59 | curly: "warn", 60 | eqeqeq: "warn", 61 | "no-throw-literal": "warn", 62 | semi: "warn", 63 | }, 64 | }, 65 | ); 66 | -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/banner.png -------------------------------------------------------------------------------- /images/blame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/blame.gif -------------------------------------------------------------------------------- /images/describe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/describe.png -------------------------------------------------------------------------------- /images/diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/diff.png -------------------------------------------------------------------------------- /images/edit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/edit.gif -------------------------------------------------------------------------------- /images/edit_description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/edit_description.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/logo.png -------------------------------------------------------------------------------- /images/merge.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/merge.gif -------------------------------------------------------------------------------- /images/restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/restore.png -------------------------------------------------------------------------------- /images/squash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/squash.png -------------------------------------------------------------------------------- /images/squash_range.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/squash_range.webp -------------------------------------------------------------------------------- /images/undo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keanemind/jjk/8ad101eec6bbc7b2f9d64f680bb62b8d6c44c89a/images/undo.gif -------------------------------------------------------------------------------- /languages/jj-commit.language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "JJ:", 4 | "blockComment": [ "JJ:", " " ] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | { "open": "{", "close": "}" }, 13 | { "open": "[", "close": "]" }, 14 | { "open": "(", "close": ")" }, 15 | { "open": "'", "close": "'", "notIn": ["string", "comment"] }, 16 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 17 | { "open": "`", "close": "`", "notIn": ["string", "comment"] }, 18 | ] 19 | } -------------------------------------------------------------------------------- /src/config.toml: -------------------------------------------------------------------------------- 1 | [ui] 2 | log-word-wrap = false 3 | paginate = "never" 4 | color = "never" 5 | 6 | [template-aliases] 7 | 'commit_timestamp(commit)' = 'commit.committer().timestamp()' 8 | 'format_short_id(id)' = 'id.shortest(8)' 9 | 'format_short_change_id(id)' = 'format_short_id(id)' 10 | 'format_short_commit_id(id)' = 'format_short_id(id)' 11 | 'format_short_operation_id(id)' = 'id.short()' 12 | 'format_short_signature(signature)' = ''' 13 | coalesce(signature.email(), email_placeholder)''' 14 | 'format_short_signature_oneline(signature)' = ''' 15 | coalesce(signature.email().local(), email_placeholder)''' 16 | 'format_timestamp(timestamp)' = 'timestamp.local().format("%Y-%m-%d %H:%M:%S")' 17 | -------------------------------------------------------------------------------- /src/decorationProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileDecorationProvider, 3 | FileDecoration, 4 | Uri, 5 | EventEmitter, 6 | Event, 7 | ThemeColor, 8 | } from "vscode"; 9 | import { FileStatus, FileStatusType } from "./repository"; 10 | import { getParams, toJJUri } from "./uri"; 11 | 12 | const colorOfType = (type: FileStatusType) => { 13 | switch (type) { 14 | case "A": 15 | return new ThemeColor("jjDecoration.addedResourceForeground"); 16 | case "M": 17 | return new ThemeColor("jjDecoration.modifiedResourceForeground"); 18 | case "D": 19 | return new ThemeColor("jjDecoration.deletedResourceForeground"); 20 | case "R": 21 | return new ThemeColor("jjDecoration.modifiedResourceForeground"); 22 | } 23 | }; 24 | 25 | export class JJDecorationProvider implements FileDecorationProvider { 26 | private readonly _onDidChangeDecorations = new EventEmitter(); 27 | readonly onDidChangeFileDecorations: Event = 28 | this._onDidChangeDecorations.event; 29 | private decorations = new Map(); 30 | private trackedFiles = new Set(); 31 | private hasData = false; 32 | 33 | /** 34 | * @param register Function that will register this provider with vscode. 35 | * This will be called lazily once the provider has data to show. 36 | */ 37 | constructor(private register: (provider: JJDecorationProvider) => void) {} 38 | 39 | /** 40 | * Updates the internal state of the provider with new decorations. If 41 | * being called for the first time, registers the provider with vscode. 42 | * Otherwise, fires an event to notify vscode of the updated decorations. 43 | */ 44 | onRefresh( 45 | fileStatusesByChange: Map, 46 | trackedFiles: Set, 47 | ) { 48 | if (process.platform === "win32") { 49 | trackedFiles = convertSetToLowercase(trackedFiles); 50 | } 51 | const nextDecorations = new Map(); 52 | for (const [changeId, fileStatuses] of fileStatusesByChange) { 53 | for (const fileStatus of fileStatuses) { 54 | const key = getKey(Uri.file(fileStatus.path).fsPath, changeId); 55 | nextDecorations.set(key, { 56 | badge: fileStatus.type, 57 | tooltip: fileStatus.file, 58 | color: colorOfType(fileStatus.type), 59 | }); 60 | } 61 | } 62 | 63 | const changedDecorationKeys = new Set(); 64 | for (const [key, fileDecoration] of nextDecorations) { 65 | if ( 66 | !this.decorations.has(key) || 67 | this.decorations.get(key)!.badge !== fileDecoration.badge 68 | ) { 69 | changedDecorationKeys.add(key); 70 | } 71 | } 72 | for (const key of this.decorations.keys()) { 73 | if (!nextDecorations.has(key)) { 74 | changedDecorationKeys.add(key); 75 | } 76 | } 77 | 78 | const changedTrackedFiles = new Set([ 79 | ...[...trackedFiles.values()].filter( 80 | (file) => !this.trackedFiles.has(file), 81 | ), 82 | ...[...this.trackedFiles.values()].filter( 83 | (file) => !trackedFiles.has(file), 84 | ), 85 | ]); 86 | 87 | this.decorations = nextDecorations; 88 | this.trackedFiles = trackedFiles; 89 | 90 | if (!this.hasData) { 91 | this.hasData = true; 92 | // Register the provider with vscode now that we have data to show. 93 | this.register(this); 94 | return; 95 | } 96 | 97 | const changedUris = [ 98 | ...[...changedDecorationKeys.keys()].map((key) => { 99 | const { fsPath, rev } = parseKey(key); 100 | return toJJUri(Uri.file(fsPath), { rev }); 101 | }), 102 | ...[...changedDecorationKeys.keys()] 103 | .filter((key) => { 104 | const { rev } = parseKey(key); 105 | return rev === "@"; 106 | }) 107 | .map((key) => { 108 | const { fsPath } = parseKey(key); 109 | return Uri.file(fsPath); 110 | }), 111 | ...[...changedTrackedFiles.values()].map((file) => Uri.file(file)), 112 | ]; 113 | 114 | this._onDidChangeDecorations.fire(changedUris); 115 | } 116 | 117 | provideFileDecoration(uri: Uri): FileDecoration | undefined { 118 | if (!this.hasData) { 119 | throw new Error( 120 | "provideFileDecoration was called before data was available", 121 | ); 122 | } 123 | let rev = "@"; 124 | if (uri.scheme === "jj") { 125 | const params = getParams(uri); 126 | if ("diffOriginalRev" in params) { 127 | // It doesn't make sense to show a decoration for the left side of a diff, even if that left side is a 128 | // single rev, because we never show the left side of a diff by itself; it'll always be part of a diff view. 129 | return undefined; 130 | } 131 | rev = params.rev; 132 | } 133 | const key = getKey(uri.fsPath, rev); 134 | if (rev === "@" && !this.decorations.has(key)) { 135 | const fsPath = 136 | process.platform === "win32" ? uri.fsPath.toLowerCase() : uri.fsPath; 137 | if (!this.trackedFiles.has(fsPath)) { 138 | return { 139 | color: new ThemeColor("jjDecoration.ignoredResourceForeground"), 140 | }; 141 | } 142 | } 143 | return this.decorations.get(key); 144 | } 145 | } 146 | 147 | function getKey(fsPath: string, rev: string) { 148 | fsPath = process.platform === "win32" ? fsPath.toLowerCase() : fsPath; 149 | return JSON.stringify({ fsPath, rev }); 150 | } 151 | 152 | function parseKey(key: string) { 153 | return JSON.parse(key) as { fsPath: string; rev: string }; 154 | } 155 | 156 | function convertSetToLowercase(originalSet: Set): Set { 157 | const lowercaseSet = new Set(); 158 | 159 | for (const item of originalSet) { 160 | if (typeof item === "string") { 161 | lowercaseSet.add(item.toLowerCase() as unknown as T); 162 | } else { 163 | lowercaseSet.add(item); 164 | } 165 | } 166 | 167 | return lowercaseSet; 168 | } 169 | -------------------------------------------------------------------------------- /src/fakeeditor/.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /src/fakeeditor/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | // Determine the output name based on target info 19 | var exe_name: []const u8 = "fakeeditor"; 20 | if (target.query.os_tag) |os| { 21 | const os_name = @tagName(os); 22 | if (target.query.cpu_arch) |arch| { 23 | const arch_name = @tagName(arch); 24 | 25 | // Create name in format: fakeeditor_{os}_{arch} 26 | exe_name = b.fmt("fakeeditor_{s}_{s}", .{ 27 | os_name, arch_name, 28 | }); 29 | } 30 | } 31 | 32 | const exe = b.addExecutable(.{ 33 | .name = exe_name, 34 | .root_source_file = b.path("src/main.zig"), 35 | .target = target, 36 | .optimize = optimize, 37 | .link_libc = true, 38 | }); 39 | 40 | b.installArtifact(exe); 41 | 42 | // This *creates* a Run step in the build graph, to be executed when another 43 | // step is evaluated that depends on it. The next line below will establish 44 | // such a dependency. 45 | const run_cmd = b.addRunArtifact(exe); 46 | 47 | // By making the run step depend on the install step, it will be run from the 48 | // installation directory rather than directly from within the cache directory. 49 | // This is not necessary, however, if the application depends on other installed 50 | // files, this ensures they will be present and in the expected location. 51 | run_cmd.step.dependOn(b.getInstallStep()); 52 | 53 | // This allows the user to pass arguments to the application in the build 54 | // command itself, like this: `zig build run -- arg1 arg2 etc` 55 | if (b.args) |args| { 56 | run_cmd.addArgs(args); 57 | } 58 | 59 | // This creates a build step. It will be visible in the `zig build --help` menu, 60 | // and can be selected like this: `zig build run` 61 | // This will evaluate the `run` step rather than the default, which is "install". 62 | const run_step = b.step("run", "Run the app"); 63 | run_step.dependOn(&run_cmd.step); 64 | } 65 | -------------------------------------------------------------------------------- /src/fakeeditor/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = .fakeeditor, 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.0.0", 14 | 15 | // Together with name, this represents a globally unique package 16 | // identifier. This field is generated by the Zig toolchain when the 17 | // package is first created, and then *never changes*. This allows 18 | // unambiguous detection of one package being an updated version of 19 | // another. 20 | // 21 | // When forking a Zig project, this id should be regenerated (delete the 22 | // field and run `zig build`) if the upstream project is still maintained. 23 | // Otherwise, the fork is *hostile*, attempting to take control over the 24 | // original project's identity. Thus it is recommended to leave the comment 25 | // on the following line intact, so that it shows up in code reviews that 26 | // modify the field. 27 | .fingerprint = 0x5d5706100ec4cb50, // Changing this has security and trust implications. 28 | 29 | // Tracks the earliest Zig version that the package considers to be a 30 | // supported use case. 31 | .minimum_zig_version = "0.14.0", 32 | 33 | // This field is optional. 34 | // Each dependency must either provide a `url` and `hash`, or a `path`. 35 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 36 | // Once all dependencies are fetched, `zig build` no longer requires 37 | // internet connectivity. 38 | .dependencies = .{ 39 | // See `zig fetch --save ` for a command-line interface for adding dependencies. 40 | //.example = .{ 41 | // // When updating this field to a new URL, be sure to delete the corresponding 42 | // // `hash`, otherwise you are communicating that you expect to find the old hash at 43 | // // the new URL. If the contents of a URL change this will result in a hash mismatch 44 | // // which will prevent zig from using it. 45 | // .url = "https://example.com/foo.tar.gz", 46 | // 47 | // // This is computed from the file contents of the directory of files that is 48 | // // obtained after fetching `url` and applying the inclusion rules given by 49 | // // `paths`. 50 | // // 51 | // // This field is the source of truth; packages do not come from a `url`; they 52 | // // come from a `hash`. `url` is just one of many possible mirrors for how to 53 | // // obtain a package matching this `hash`. 54 | // // 55 | // // Uses the [multihash](https://multiformats.io/multihash/) format. 56 | // .hash = "...", 57 | // 58 | // // When this is provided, the package is found in a directory relative to the 59 | // // build root. In this case the package's hash is irrelevant and therefore not 60 | // // computed. This field and `url` are mutually exclusive. 61 | // .path = "foo", 62 | // 63 | // // When this is set to `true`, a package is declared to be lazily 64 | // // fetched. This makes the dependency only get fetched if it is 65 | // // actually used. 66 | // .lazy = false, 67 | //}, 68 | }, 69 | 70 | // Specifies the set of files and directories that are included in this package. 71 | // Only files and directories listed here are included in the `hash` that 72 | // is computed for this package. Only files listed here will remain on disk 73 | // when using the zig package manager. As a rule of thumb, one should list 74 | // files required for compilation plus any license(s). 75 | // Paths are relative to the build root. Use the empty string (`""`) to refer to 76 | // the build root itself. 77 | // A directory listed here means that all files within, recursively, are included. 78 | .paths = .{ 79 | "build.zig", 80 | "build.zig.zon", 81 | "src", 82 | // For example... 83 | //"LICENSE", 84 | //"README.md", 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/fakeeditor/build_all_platforms.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | zig build -Doptimize=ReleaseSmall -Dtarget=aarch64-macos --release=small --summary all 3 | zig build -Doptimize=ReleaseSmall -Dtarget=x86_64-macos --release=small --summary all 4 | zig build -Doptimize=ReleaseSmall -Dtarget=arm-linux --release=small --summary all 5 | zig build -Doptimize=ReleaseSmall -Dtarget=aarch64-linux --release=small --summary all 6 | zig build -Doptimize=ReleaseSmall -Dtarget=x86_64-linux --release=small --summary all 7 | zig build -Doptimize=ReleaseSmall -Dtarget=aarch64-windows --release=small --summary all 8 | zig build -Doptimize=ReleaseSmall -Dtarget=x86_64-windows --release=small --summary all 9 | -------------------------------------------------------------------------------- /src/fakeeditor/src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const c = @cImport({ 4 | @cInclude("stdlib.h"); 5 | // For getppid / kill on POSIX 6 | if (builtin.os.tag != .windows) { 7 | @cInclude("unistd.h"); // For getppid 8 | @cInclude("signal.h"); // For kill 9 | } 10 | // Windows-specific includes 11 | if (builtin.os.tag == .windows) { 12 | @cInclude("windows.h"); 13 | @cInclude("tlhelp32.h"); // For CreateToolhelp32Snapshot 14 | } 15 | }); 16 | 17 | const POLLING_INTERVAL_MS: u64 = 50; 18 | const TOTAL_TIMEOUT_MS: u64 = 5000; 19 | 20 | // Helper function to check if a process exists on Windows 21 | fn parentProcessExistsWindows(parent_pid: c.DWORD) !bool { 22 | const stderr = std.io.getStdErr().writer(); 23 | 24 | const hParentProcess = c.OpenProcess(c.SYNCHRONIZE, 0, parent_pid); 25 | if (hParentProcess == null) { 26 | // If we can't open the process, it might have already exited or we lack permissions. 27 | // For our purpose, if OpenProcess fails, we assume the parent is gone or inaccessible. 28 | return false; 29 | } 30 | defer _ = c.CloseHandle(hParentProcess); 31 | 32 | // Check if the parent process object is signaled (i.e., terminated) 33 | // WaitForSingleObject with 0 timeout is a non-blocking check. 34 | const wait_status = c.WaitForSingleObject(hParentProcess, 0); 35 | if (wait_status == c.WAIT_OBJECT_0) { 36 | return false; // Parent process terminated 37 | } else if (wait_status == c.WAIT_TIMEOUT) { 38 | return true; // Parent process still running 39 | } else { 40 | // WAIT_FAILED or other error 41 | try stderr.print("WaitForSingleObject failed: {}\n", .{c.GetLastError()}); 42 | return false; // Assume parent is gone on error 43 | } 44 | } 45 | 46 | // Helper function to get parent PID on Windows 47 | fn getParentPidWindows() !c.DWORD { 48 | const stderr = std.io.getStdErr().writer(); 49 | 50 | const current_pid = c.GetCurrentProcessId(); 51 | const hSnapshot = c.CreateToolhelp32Snapshot(c.TH32CS_SNAPPROCESS, 0); 52 | if (hSnapshot == c.INVALID_HANDLE_VALUE) { 53 | try stderr.print("CreateToolhelp32Snapshot failed: {}\n", .{c.GetLastError()}); 54 | return error.SnapshotFailed; 55 | } 56 | defer _ = c.CloseHandle(hSnapshot); 57 | 58 | var pe32: c.PROCESSENTRY32 = undefined; 59 | pe32.dwSize = @sizeOf(c.PROCESSENTRY32); 60 | 61 | if (c.Process32First(hSnapshot, &pe32) == 0) { // BOOL is 0 for FALSE 62 | try stderr.print("Process32First failed: {}\n", .{c.GetLastError()}); 63 | return error.Process32FirstFailed; 64 | } 65 | 66 | while (true) { 67 | if (pe32.th32ProcessID == current_pid) { 68 | if (pe32.th32ParentProcessID == 0) { 69 | try stderr.print("Error: Retrieved parent PID is 0 for process {}. This is unexpected.\n", .{current_pid}); 70 | return error.ParentIsSystemIdleProcess; 71 | } 72 | return pe32.th32ParentProcessID; 73 | } 74 | if (c.Process32Next(hSnapshot, &pe32) == 0) { // BOOL is 0 for FALSE 75 | if (c.GetLastError() == c.ERROR_NO_MORE_FILES) { 76 | break; // Reached end of process list 77 | } 78 | try stderr.print("Process32Next failed: {}\n", .{c.GetLastError()}); 79 | return error.Process32NextFailed; 80 | } 81 | } 82 | return error.ParentNotFound; 83 | } 84 | 85 | pub fn main() !void { 86 | const stdout = std.io.getStdOut().writer(); 87 | const stderr = std.io.getStdErr().writer(); 88 | const allocator = std.heap.page_allocator; 89 | 90 | const pid = switch (builtin.os.tag) { 91 | .linux => std.os.linux.getpid(), 92 | .windows => c.GetCurrentProcessId(), 93 | .macos, .freebsd, .netbsd, .openbsd, .dragonfly => c.getpid(), 94 | else => @compileError("Unsupported OS"), 95 | }; 96 | try stdout.print("{}\n", .{pid}); 97 | 98 | const args = try std.process.argsAlloc(allocator); 99 | defer std.process.argsFree(allocator, args); 100 | 101 | for (args) |arg| { 102 | try stdout.print("{s}\n", .{arg}); 103 | } 104 | 105 | const envVarName = "JJ_FAKEEDITOR_SIGNAL_DIR"; 106 | const signal_dir_path_owned = std.process.getEnvVarOwned(allocator, envVarName) catch |err| { 107 | try stderr.print("Error getting environment variable '{s}': {any}\n", .{ envVarName, err }); 108 | std.process.exit(1); 109 | }; 110 | defer allocator.free(signal_dir_path_owned); 111 | 112 | try stdout.print("FAKEEDITOR_OUTPUT_END\n", .{}); 113 | 114 | const start_time = std.time.nanoTimestamp(); 115 | 116 | const signal_file_path = std.fs.path.join(allocator, &.{ signal_dir_path_owned, "0" }) catch |e| { 117 | try stderr.print("Critical Error: Failed to construct signal file path '{s}{c}{s}': {any}. Exiting fakeeditor.\n", .{ signal_dir_path_owned, std.fs.path.sep, "0", e }); 118 | std.process.exit(1); 119 | }; 120 | 121 | var ppid: if (builtin.os.tag != .windows) c.pid_t else void = 122 | if (builtin.os.tag != .windows) 0 else {}; 123 | var win_ppid: if (builtin.os.tag == .windows) c.DWORD else void = 124 | if (builtin.os.tag == .windows) 0 else {}; 125 | var parent_monitoring_active: bool = true; 126 | 127 | if (builtin.os.tag == .windows) { 128 | win_ppid = getParentPidWindows() catch |err| blk: { 129 | try stderr.print("Warning: Failed to get parent PID on Windows: {any}. Parent process monitoring will be disabled.\n", .{err}); 130 | parent_monitoring_active = false; 131 | break :blk 0; 132 | }; 133 | } else { 134 | ppid = c.getppid(); 135 | if (ppid == 1) { // Reparented to init/launchd 136 | try stderr.print("Info: Parent process is init/launchd (PID 1), original parent likely exited. Exiting fakeeditor.\n", .{}); 137 | std.process.exit(1); // Exit immediately if reparented 138 | } 139 | } 140 | 141 | while (true) { 142 | const current_time = std.time.nanoTimestamp(); 143 | const elapsed_ms = @divTrunc((current_time - start_time), std.time.ns_per_ms); 144 | 145 | if (elapsed_ms >= TOTAL_TIMEOUT_MS) { 146 | try stderr.print("Error: Timeout ({}ms) reached in fakeeditor. Exiting.\n", .{TOTAL_TIMEOUT_MS}); 147 | std.process.exit(1); 148 | } 149 | 150 | // Parent Process Check 151 | if (parent_monitoring_active) { 152 | if (builtin.os.tag == .windows) { 153 | if (!try parentProcessExistsWindows(win_ppid)) { 154 | try stderr.print("Parent process (PID: {}) no longer exists (Windows). Exiting.\n", .{win_ppid}); 155 | std.process.exit(1); 156 | } 157 | } else { 158 | if (std.posix.kill(ppid, 0)) |_| { 159 | // kill succeeded, parent process still exists 160 | } else |err| { 161 | if (err == error.NoSuchProcess) { 162 | try stderr.print("Parent process (PID: {}) no longer exists (POSIX). Exiting.\n", .{ppid}); 163 | std.process.exit(1); 164 | } 165 | // Other errors with kill could also indicate an issue, but NoSuchProcess is the key one. 166 | // If kill fails for other reasons, we might want to log it to stderr but not necessarily exit immediately, 167 | // relying on the main timeout or signal file. 168 | } 169 | } 170 | } 171 | 172 | // Check for signal file "0" 173 | if (std.fs.accessAbsolute(signal_file_path, .{})) |_| { 174 | // File "0" exists 175 | std.process.exit(0); 176 | } else |err| { 177 | if (err != error.FileNotFound) { 178 | // Some other error accessing the file, log it but continue polling 179 | try stderr.print("Error checking for signal file '0' in fakeeditor: {any}\n", .{err}); 180 | } 181 | } 182 | 183 | std.time.sleep(POLLING_INTERVAL_MS * std.time.ns_per_ms); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/fileSystemProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileSystemProvider, 3 | FileSystemError, 4 | EventEmitter, 5 | Event, 6 | FileChangeEvent, 7 | Disposable, 8 | Uri, 9 | FileStat, 10 | FileType, 11 | window, 12 | FileChangeType, 13 | workspace, 14 | } from "vscode"; 15 | import { getParams } from "./uri"; 16 | import type { WorkspaceSourceControlManager } from "./repository"; 17 | import { 18 | createThrottledAsyncFn, 19 | eventToPromise, 20 | filterEvent, 21 | isDescendant, 22 | pathEquals, 23 | } from "./utils"; 24 | 25 | interface CacheRow { 26 | uri: Uri; 27 | timestamp: number; 28 | } 29 | 30 | const THREE_MINUTES = 1000 * 60 * 3; 31 | const FIVE_MINUTES = 1000 * 60 * 5; 32 | 33 | export class JJFileSystemProvider implements FileSystemProvider { 34 | private _onDidChangeFile = new EventEmitter(); 35 | readonly onDidChangeFile: Event = 36 | this._onDidChangeFile.event; 37 | 38 | private changedRepositoryRoots = new Set(); 39 | private cache = new Map(); 40 | private mtime = Date.now(); 41 | private disposables: Disposable[] = []; 42 | 43 | constructor(private repositories: WorkspaceSourceControlManager) { 44 | setInterval(() => this.cleanup(), FIVE_MINUTES); 45 | } 46 | 47 | dispose() {} 48 | 49 | onDidChangeRepository({ 50 | repositoryRoot, 51 | }: { 52 | uri: Uri; 53 | repositoryRoot: string; 54 | }): void { 55 | this.changedRepositoryRoots.add(repositoryRoot); 56 | void this.fireChangeEvents(); 57 | } 58 | 59 | fireChangeEvents = createThrottledAsyncFn(this._fireChangeEvents.bind(this)); 60 | private async _fireChangeEvents(): Promise { 61 | if (!window.state.focused) { 62 | const onDidFocusWindow = filterEvent( 63 | window.onDidChangeWindowState, 64 | (e) => e.focused, 65 | ); 66 | await eventToPromise(onDidFocusWindow); 67 | } 68 | 69 | const events: FileChangeEvent[] = []; 70 | 71 | for (const { uri } of this.cache.values()) { 72 | for (const root of this.changedRepositoryRoots) { 73 | if (isDescendant(root, uri.fsPath)) { 74 | events.push({ type: FileChangeType.Changed, uri }); 75 | break; 76 | } 77 | } 78 | } 79 | 80 | if (events.length > 0) { 81 | this.mtime = new Date().getTime(); 82 | this._onDidChangeFile.fire(events); 83 | } 84 | 85 | this.changedRepositoryRoots.clear(); 86 | } 87 | 88 | private cleanup(): void { 89 | const now = new Date().getTime(); 90 | const cache = new Map(); 91 | 92 | for (const row of this.cache.values()) { 93 | const path = row.uri.fsPath; 94 | const isOpen = workspace.textDocuments 95 | .filter((d) => d.uri.scheme === "file") 96 | .some((d) => pathEquals(d.uri.fsPath, path)); 97 | 98 | if (isOpen || now - row.timestamp < THREE_MINUTES) { 99 | cache.set(row.uri.toString(), row); 100 | } else { 101 | // TODO: should fire delete events? 102 | } 103 | } 104 | 105 | this.cache = cache; 106 | } 107 | 108 | watch(): Disposable { 109 | return new Disposable(() => {}); 110 | } 111 | 112 | async stat(uri: Uri): Promise { 113 | return { 114 | type: FileType.File, 115 | size: (await this.readFile(uri)).length, 116 | mtime: this.mtime, 117 | ctime: 0, 118 | }; 119 | } 120 | 121 | readDirectory(): Thenable<[string, FileType][]> { 122 | throw new Error("Method not implemented."); 123 | } 124 | 125 | createDirectory(): void { 126 | throw new Error("Method not implemented."); 127 | } 128 | 129 | async readFile(uri: Uri): Promise { 130 | const params = getParams(uri); 131 | 132 | const repository = this.repositories.getRepositoryFromUri(uri); 133 | if (!repository) { 134 | throw FileSystemError.FileNotFound(); 135 | } 136 | 137 | const timestamp = new Date().getTime(); 138 | const cacheValue: CacheRow = { uri, timestamp }; 139 | 140 | this.cache.set(uri.toString(), cacheValue); 141 | 142 | if ("diffOriginalRev" in params) { 143 | const originalContent = await repository.getDiffOriginal( 144 | params.diffOriginalRev, 145 | uri.fsPath, 146 | ); 147 | if (!originalContent) { 148 | try { 149 | const data = await repository.readFile( 150 | params.diffOriginalRev, 151 | uri.fsPath, 152 | ); 153 | return data; 154 | } catch (e) { 155 | if (e instanceof Error && e.message.includes("No such path")) { 156 | throw FileSystemError.FileNotFound(); 157 | } 158 | throw e; 159 | } 160 | } 161 | return originalContent; 162 | } else { 163 | try { 164 | const data = await repository.readFile(params.rev, uri.fsPath); 165 | return data; 166 | } catch (e) { 167 | if (e instanceof Error && e.message.includes("No such path")) { 168 | throw FileSystemError.FileNotFound(); 169 | } 170 | throw e; 171 | } 172 | } 173 | } 174 | 175 | writeFile(): void { 176 | throw new Error("Method not implemented."); 177 | } 178 | 179 | delete(): void { 180 | throw new Error("Method not implemented."); 181 | } 182 | 183 | rename(): void { 184 | throw new Error("Method not implemented."); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/graphWebview.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import type { JJRepository } from "./repository"; 4 | import path from "path"; 5 | 6 | type Message = { 7 | command: string; 8 | changeId: string; 9 | selectedNodes: string[]; 10 | }; 11 | 12 | export type RefreshArgs = { 13 | preserveScroll: boolean; 14 | }; 15 | 16 | export class ChangeNode { 17 | label: string; 18 | description: string; 19 | tooltip: string; 20 | contextValue: string; 21 | parentChangeIds?: string[]; 22 | branchType?: string; 23 | constructor( 24 | label: string, 25 | description: string, 26 | tooltip: string, 27 | contextValue: string, 28 | parentChangeIds?: string[], 29 | branchType?: string, 30 | ) { 31 | this.label = label; 32 | this.description = description; 33 | this.tooltip = tooltip; 34 | this.contextValue = contextValue; 35 | this.parentChangeIds = parentChangeIds; 36 | this.branchType = branchType; 37 | } 38 | } 39 | 40 | export class JJGraphWebview implements vscode.WebviewViewProvider { 41 | subscriptions: { 42 | dispose(): unknown; 43 | }[] = []; 44 | 45 | public panel?: vscode.WebviewView; 46 | public repository: JJRepository; 47 | public logData: ChangeNode[] = []; 48 | public selectedNodes: Set = new Set(); 49 | 50 | constructor( 51 | private readonly extensionUri: vscode.Uri, 52 | repo: JJRepository, 53 | private readonly context: vscode.ExtensionContext, 54 | ) { 55 | this.repository = repo; 56 | 57 | // Register the webview provider 58 | context.subscriptions.push( 59 | vscode.window.registerWebviewViewProvider("jjGraphWebview", this, { 60 | webviewOptions: { 61 | retainContextWhenHidden: true, 62 | }, 63 | }), 64 | ); 65 | } 66 | 67 | public async resolveWebviewView( 68 | webviewView: vscode.WebviewView, 69 | ): Promise { 70 | this.panel = webviewView; 71 | this.panel.title = `Source Control Graph (${path.basename(this.repository.repositoryRoot)})`; 72 | 73 | webviewView.webview.options = { 74 | enableScripts: true, 75 | localResourceRoots: [this.extensionUri], 76 | }; 77 | 78 | webviewView.webview.html = this.getWebviewContent(webviewView.webview); 79 | 80 | await new Promise((resolve) => { 81 | const messageListener = webviewView.webview.onDidReceiveMessage( 82 | (message: Message) => { 83 | if (message.command === "webviewReady") { 84 | messageListener.dispose(); 85 | resolve(); 86 | } 87 | }, 88 | ); 89 | }); 90 | 91 | webviewView.webview.onDidReceiveMessage(async (message: Message) => { 92 | switch (message.command) { 93 | case "editChange": 94 | try { 95 | await this.repository.edit(message.changeId); 96 | 97 | await vscode.commands.executeCommand("jj.refresh", { 98 | preserveScroll: true, 99 | }); 100 | } catch (error: unknown) { 101 | vscode.window.showErrorMessage( 102 | `Failed to switch to change: ${error as string}`, 103 | ); 104 | } 105 | break; 106 | case "selectChange": 107 | this.selectedNodes = new Set(message.selectedNodes); 108 | vscode.commands.executeCommand( 109 | "setContext", 110 | "jjGraphView.nodesSelected", 111 | message.selectedNodes.length, 112 | ); 113 | break; 114 | } 115 | }); 116 | 117 | await this.refresh(); 118 | } 119 | 120 | public setSelectedRepository(repo: JJRepository) { 121 | this.repository = repo; 122 | if (this.panel) { 123 | this.panel.title = `Source Control Graph (${path.basename(this.repository.repositoryRoot)})`; 124 | } 125 | } 126 | 127 | public async refresh( 128 | preserveScroll: boolean = false, 129 | force: boolean = false, 130 | ) { 131 | if (!this.panel) { 132 | return; 133 | } 134 | const currChanges = this.logData; 135 | 136 | let changes = parseJJLog(await this.repository.log()); 137 | changes = await this.getChangeNodesWithParents(changes); 138 | this.logData = changes; 139 | 140 | // Get the old status from cache before fetching new status 141 | const oldStatus = this.repository.statusCache; 142 | const status = await this.repository.getStatus(); 143 | const workingCopyId = status.workingCopy.changeId; 144 | 145 | if ( 146 | force || 147 | !oldStatus || // Handle first run when cache is empty 148 | status.workingCopy.changeId !== oldStatus.workingCopy.changeId || 149 | !this.areChangeNodesEqual(currChanges, changes) 150 | ) { 151 | this.selectedNodes.clear(); 152 | this.panel.webview.postMessage({ 153 | command: "updateGraph", 154 | changes: changes, 155 | workingCopyId, 156 | preserveScroll, 157 | }); 158 | } 159 | } 160 | 161 | private getWebviewContent(webview: vscode.Webview) { 162 | // In development, files are in src/webview 163 | // In production (bundled extension), files are in dist/webview 164 | const webviewPath = this.extensionUri.fsPath.includes("extensions") 165 | ? "dist" 166 | : "src"; 167 | 168 | const cssPath = vscode.Uri.joinPath( 169 | this.extensionUri, 170 | webviewPath, 171 | "webview", 172 | "graph.css", 173 | ); 174 | const cssUri = webview.asWebviewUri(cssPath); 175 | 176 | const codiconPath = vscode.Uri.joinPath( 177 | this.extensionUri, 178 | webviewPath === "dist" 179 | ? "dist/codicons" 180 | : "node_modules/@vscode/codicons/dist", 181 | "codicon.css", 182 | ); 183 | const codiconUri = webview.asWebviewUri(codiconPath); 184 | 185 | const htmlPath = vscode.Uri.joinPath( 186 | this.extensionUri, 187 | webviewPath, 188 | "webview", 189 | "graph.html", 190 | ); 191 | let html = fs.readFileSync(htmlPath.fsPath, "utf8"); 192 | 193 | // Replace placeholders in the HTML 194 | html = html.replace("${cssUri}", cssUri.toString()); 195 | html = html.replace("${codiconUri}", codiconUri.toString()); 196 | 197 | return html; 198 | } 199 | 200 | private async getChangeNodesWithParents( 201 | changeNodes: ChangeNode[], 202 | ): Promise { 203 | const output = await this.repository.log( 204 | "::", // get all changes 205 | ` 206 | if(root, 207 | "root()", 208 | concat( 209 | self.change_id().short(), 210 | " ", 211 | parents.map(|p| p.change_id().short()).join(" "), 212 | "\n" 213 | ) 214 | ) 215 | `, 216 | 50, 217 | false, 218 | ); 219 | 220 | const lines = output.split("\n"); 221 | 222 | // Build a map of change IDs to their parent IDs 223 | const parentMap = new Map(); 224 | 225 | for (const line of lines) { 226 | // Extract only alphanumeric strings from the line 227 | const ids = line.match(/[a-zA-Z0-9]+/g) || []; 228 | if (ids.length < 1) { 229 | continue; 230 | } 231 | 232 | // Check for root() after cleaning up symbols 233 | if (ids[0] === "root") { 234 | continue; 235 | } 236 | 237 | const [changeId, ...parentIds] = ids; 238 | if (!changeId) { 239 | continue; 240 | } 241 | 242 | // Take only the first 8 characters of each ID 243 | parentMap.set( 244 | changeId.substring(0, 8), 245 | parentIds.map((id) => id.substring(0, 8)), 246 | ); 247 | } 248 | 249 | // Assign parents to nodes using the map 250 | const res = changeNodes.map((node) => { 251 | if (node.contextValue) { 252 | node.parentChangeIds = parentMap.get(node.contextValue) || []; 253 | } 254 | return node; 255 | }); 256 | 257 | return res; 258 | } 259 | 260 | areChangeNodesEqual(a: ChangeNode[], b: ChangeNode[]): boolean { 261 | if (a.length !== b.length) { 262 | return false; 263 | } 264 | 265 | return a.every((nodeA, index) => { 266 | const nodeB = b[index]; 267 | return ( 268 | nodeA.label === nodeB.label && 269 | nodeA.tooltip === nodeB.tooltip && 270 | nodeA.description === nodeB.description && 271 | nodeA.contextValue === nodeB.contextValue 272 | ); 273 | }); 274 | } 275 | 276 | dispose() { 277 | this.subscriptions.forEach((s) => s.dispose()); 278 | } 279 | } 280 | 281 | export function parseJJLog(output: string): ChangeNode[] { 282 | const lines = output.split("\n"); 283 | const changeNodes: ChangeNode[] = []; 284 | 285 | for (let i = 0; i < lines.length; i += 2) { 286 | const oddLine = lines[i]; 287 | let evenLine = lines[i + 1] || ""; 288 | 289 | let changeId = ""; 290 | if (i % 2 === 0) { 291 | // Check if the line is odd-numbered (0-based index, so 0, 2, 4... are odd lines) 292 | const match = oddLine.match(/\b([a-zA-Z0-9]+)\b/); // Match the first group of alphanumeric characters 293 | if (match) { 294 | changeId = match[1]; 295 | } 296 | } 297 | 298 | // Match the first alphanumeric character or opening parenthesis and everything after it 299 | const match = evenLine.match(/([a-zA-Z0-9(].*)/); 300 | const description = match ? match[1] : ""; 301 | 302 | // Remove the description from the even line 303 | if (description) { 304 | evenLine = evenLine.replace(description, ""); 305 | } 306 | 307 | const emailMatch = oddLine.match( 308 | /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, 309 | ); 310 | const timestampMatch = oddLine.match( 311 | /\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\b/, 312 | ); 313 | const symbolsMatch = oddLine.match(/^[^a-zA-Z0-9(]+/); 314 | const commitIdMatch = oddLine.match(/([a-zA-Z0-9]{8})$/); 315 | 316 | // Add this: Find first occurrence of @, ○, or ◆ 317 | const branchTypeMatch = symbolsMatch 318 | ? symbolsMatch[0].match(/[@○◆]/) 319 | : null; 320 | const branchType = branchTypeMatch ? branchTypeMatch[0] : undefined; 321 | const formattedLine = `${description}${changeId === "zzzzzzzz" ? "root()" : ""} • ${changeId} • ${commitIdMatch ? commitIdMatch[0] : ""}`; 322 | 323 | // Create a ChangeNode for the odd line with the appended description 324 | changeNodes.push( 325 | new ChangeNode( 326 | formattedLine, 327 | `${emailMatch ? emailMatch[0] : ""} ${timestampMatch ? timestampMatch[0] : ""}`, 328 | changeId, 329 | changeId, 330 | undefined, 331 | branchType, 332 | ), 333 | ); 334 | } 335 | return changeNodes; 336 | } 337 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | import { config } from "./vendor/winston-transport-vscode/logOutputChannelTransport"; 3 | 4 | export const logger = winston.createLogger({ 5 | level: "trace", 6 | transports: [ 7 | new winston.transports.Console({ 8 | format: winston.format.combine( 9 | winston.format.colorize(), 10 | winston.format.simple(), 11 | ), 12 | }), 13 | ], 14 | levels: config.levels, 15 | }); 16 | -------------------------------------------------------------------------------- /src/operationLogTreeView.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventEmitter, 3 | TreeDataProvider, 4 | TreeItem, 5 | Event, 6 | TreeView, 7 | window, 8 | MarkdownString, 9 | } from "vscode"; 10 | import { JJRepository, Operation } from "./repository"; 11 | import path from "path"; 12 | 13 | export class OperationLogManager { 14 | subscriptions: { 15 | dispose(): unknown; 16 | }[] = []; 17 | operationLogTreeView: TreeView; 18 | 19 | constructor( 20 | public operationLogTreeDataProvider: OperationLogTreeDataProvider, 21 | ) { 22 | this.operationLogTreeView = window.createTreeView( 23 | "jjOperationLog", 24 | { 25 | treeDataProvider: operationLogTreeDataProvider, 26 | }, 27 | ); 28 | this.operationLogTreeView.title = `Operation Log (${path.basename( 29 | operationLogTreeDataProvider.getSelectedRepo().repositoryRoot, 30 | )})`; 31 | this.subscriptions.push(this.operationLogTreeView); 32 | } 33 | 34 | async setSelectedRepo(repo: JJRepository) { 35 | await this.operationLogTreeDataProvider.setSelectedRepo(repo); 36 | this.operationLogTreeView.title = `Operation Log (${path.basename( 37 | repo.repositoryRoot, 38 | )})`; 39 | } 40 | 41 | dispose() { 42 | this.subscriptions.forEach((s) => s.dispose()); 43 | } 44 | } 45 | 46 | export class OperationTreeItem extends TreeItem { 47 | constructor( 48 | public readonly operation: Operation, 49 | public readonly repositoryRoot: string, 50 | ) { 51 | super( 52 | operation.tags.startsWith("args: ") 53 | ? operation.tags.slice(6) 54 | : operation.tags, 55 | ); 56 | this.id = operation.id; 57 | this.description = operation.description; 58 | this.tooltip = new MarkdownString( 59 | `**${operation.start}** \n${operation.tags} \n${operation.description}`, 60 | ); 61 | } 62 | } 63 | 64 | export class OperationLogTreeDataProvider implements TreeDataProvider { 65 | _onDidChangeTreeData: EventEmitter< 66 | OperationTreeItem | undefined | null | void 67 | > = new EventEmitter(); 68 | onDidChangeTreeData: Event = 69 | this._onDidChangeTreeData.event; 70 | 71 | operationTreeItems: OperationTreeItem[] = []; 72 | 73 | constructor(private selectedRepository: JJRepository) {} 74 | 75 | getTreeItem(element: TreeItem): TreeItem { 76 | return element; 77 | } 78 | 79 | getChildren(): OperationTreeItem[] { 80 | return this.operationTreeItems; 81 | } 82 | 83 | async refresh() { 84 | const prev = this.operationTreeItems; 85 | const operations = await this.selectedRepository.operationLog(); 86 | this.operationTreeItems = operations.map( 87 | (op) => new OperationTreeItem(op, this.selectedRepository.repositoryRoot), 88 | ); 89 | if ( 90 | prev.length !== this.operationTreeItems.length || 91 | !prev.every((op, i) => op.id === this.operationTreeItems[i].operation.id) 92 | ) { 93 | this._onDidChangeTreeData.fire(); 94 | } 95 | } 96 | 97 | async setSelectedRepo(repo: JJRepository) { 98 | this.selectedRepository = repo; 99 | await this.refresh(); 100 | } 101 | 102 | getSelectedRepo() { 103 | return this.selectedRepository; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/all-tests.ts: -------------------------------------------------------------------------------- 1 | // This file is responsible for importing all test files, 2 | // so they are included in the single bundle that Mocha will run. 3 | 4 | import "./main.test"; 5 | import "./repository.test"; 6 | import "./fakeeditor.test"; 7 | -------------------------------------------------------------------------------- /src/test/fakeeditor.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as os from "os"; 4 | import * as fs from "fs"; 5 | import { execPromise } from "./utils"; 6 | import { fakeEditorPath, initExtensionDir } from "../repository"; 7 | import * as vscode from "vscode"; 8 | import { ExecException, spawn } from "child_process"; 9 | 10 | // Helper to check if a process is running 11 | function isProcessRunning(pid: number): boolean { 12 | try { 13 | process.kill(pid, 0); // Just check, don't actually send a signal 14 | return true; 15 | } catch { 16 | return false; 17 | } 18 | } 19 | 20 | function isExecException(e: unknown): e is ExecException { 21 | return typeof e === "object" && e !== null && "code" in e; 22 | } 23 | 24 | suite("fakeeditor", () => { 25 | initExtensionDir(vscode.extensions.getExtension("jjk.jjk")!.extensionUri); 26 | 27 | test("fails when JJ_FAKEEDITOR_SIGNAL_DIR is missing", async () => { 28 | await assert.rejects( 29 | async () => execPromise(fakeEditorPath, { timeout: 6000 }), 30 | (err: unknown) => { 31 | assert.ok( 32 | isExecException(err), 33 | "Expected error to be an ExecException", 34 | ); 35 | assert.ok(err.code !== undefined, "Expected error to have a code"); 36 | assert.strictEqual(err.code, 1); 37 | return true; 38 | }, 39 | ); 40 | }); 41 | 42 | test("fails when JJ_FAKEEDITOR_SIGNAL_DIR is invalid", async () => { 43 | await assert.rejects( 44 | async () => 45 | execPromise(fakeEditorPath, { 46 | env: { ...process.env, JJ_FAKEEDITOR_SIGNAL_DIR: "/no/such/dir" }, 47 | timeout: 6000, 48 | }), 49 | (err: unknown) => { 50 | assert.ok( 51 | isExecException(err), 52 | "Expected error to be an ExecException", 53 | ); 54 | assert.ok(err.code !== undefined, "Expected error to have a code"); 55 | assert.strictEqual(err.code, 1); 56 | return true; 57 | }, 58 | ); 59 | }); 60 | 61 | test("exits 0 immediately if signal file exists", async () => { 62 | const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fakeeditor-")); 63 | const signalPath = path.join(tmp, "0"); 64 | fs.writeFileSync(signalPath, ""); 65 | const result = await execPromise(fakeEditorPath, { 66 | env: { ...process.env, JJ_FAKEEDITOR_SIGNAL_DIR: tmp }, 67 | timeout: 6000, 68 | }); 69 | assert.strictEqual(result.stderr, ""); 70 | assert.ok(result.stdout.includes("FAKEEDITOR_OUTPUT_END")); 71 | }); 72 | 73 | test("exits 0 when signal file is created after delay", async () => { 74 | const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fakeeditor-")); 75 | const signalPath = path.join(tmp, "0"); 76 | const delayMs = 150; // 3 × POLLING_INTERVAL 77 | 78 | const child = spawn(fakeEditorPath, [], { 79 | env: { ...process.env, JJ_FAKEEDITOR_SIGNAL_DIR: tmp }, 80 | }); 81 | 82 | if (!child.pid) { 83 | throw new Error("Failed to spawn process"); 84 | } 85 | 86 | let stdout = ""; 87 | child.stdout?.on("data", (data: Buffer) => { 88 | stdout += data.toString("utf8"); 89 | }); 90 | 91 | const exitPromise = new Promise((resolve, reject) => { 92 | child.on("exit", (code, signal) => { 93 | if (code === null) { 94 | reject(new Error(`Process exited from signal: ${signal}`)); 95 | } else { 96 | resolve(code); 97 | } 98 | }); 99 | }); 100 | 101 | await new Promise((resolve) => setTimeout(resolve, delayMs)); 102 | 103 | assert.ok( 104 | isProcessRunning(child.pid), 105 | "Process should still be running before file is created", 106 | ); 107 | 108 | const beforeWrite = Date.now(); 109 | fs.writeFileSync(signalPath, ""); 110 | 111 | const exitCode = await exitPromise; 112 | const responseTime = Date.now() - beforeWrite; 113 | 114 | assert.strictEqual(exitCode, 0); 115 | assert.ok(stdout.includes("FAKEEDITOR_OUTPUT_END")); 116 | // Should detect and respond to the file within about 2 polling intervals 117 | assert.ok( 118 | responseTime <= 150, 119 | `Should exit quickly after file creation (took ${responseTime}ms)`, 120 | ); 121 | }); 122 | 123 | test("exits with error after timeout when no signal file", async () => { 124 | const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fakeeditor-")); 125 | const startTime = Date.now(); 126 | 127 | await assert.rejects( 128 | async () => 129 | execPromise(fakeEditorPath, { 130 | env: { ...process.env, JJ_FAKEEDITOR_SIGNAL_DIR: tmp }, 131 | timeout: 6000, 132 | }), 133 | (err: unknown) => { 134 | const elapsed = Date.now() - startTime; 135 | assert.ok( 136 | isExecException(err), 137 | "Expected error to be an ExecException", 138 | ); 139 | assert.ok(err.code !== undefined, "Expected error to have a code"); 140 | assert.strictEqual(err.code, 1); 141 | assert.ok(elapsed >= 5000, "Should wait for TOTAL_TIMEOUT"); 142 | assert.ok(elapsed < 5500, "Should not wait much longer than timeout"); 143 | return true; 144 | }, 145 | ); 146 | }); 147 | 148 | test("exits with error when signal directory has bad permissions", async () => { 149 | // Skip on Windows as chmod behaves differently 150 | if (process.platform === "win32") { 151 | return; 152 | } 153 | 154 | const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fakeeditor-")); 155 | fs.chmodSync(tmp, 0o000); // No read/write/execute for anyone 156 | 157 | await assert.rejects( 158 | async () => 159 | execPromise(fakeEditorPath, { 160 | env: { ...process.env, JJ_FAKEEDITOR_SIGNAL_DIR: tmp }, 161 | timeout: 6000, 162 | }), 163 | (err: unknown) => { 164 | assert.ok( 165 | isExecException(err), 166 | "Expected error to be an ExecException", 167 | ); 168 | assert.ok(err.code !== undefined, "Expected error to have a code"); 169 | assert.strictEqual(err.code, 1); 170 | return true; 171 | }, 172 | ); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/test/main.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 { execPromise } from "./utils"; 7 | // import * as myExtension from '../../extension'; 8 | 9 | suite("Extension Test Suite", () => { 10 | vscode.window.showInformationMessage("Start all tests."); 11 | 12 | let originalOperation: string; 13 | suiteSetup(async () => { 14 | // Wait for a refresh so the repo is detected 15 | await new Promise((resolve) => { 16 | setTimeout(resolve, 5000); 17 | }); 18 | 19 | const output = await execPromise( 20 | 'jj operation log --limit 1 --no-graph --template "self.id()"', 21 | ); 22 | originalOperation = output.stdout.trim(); 23 | }); 24 | 25 | teardown(async () => { 26 | await execPromise(`jj operation restore ${originalOperation}`); 27 | }); 28 | 29 | test("Sample test", () => { 30 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 31 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 32 | }); 33 | 34 | test("Sanity check: `jj status` succeeds", async () => { 35 | await assert.doesNotReject(execPromise("jj status")); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/test/repository.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { parseRenamePaths } from "../repository"; // Adjust path as needed 3 | 4 | suite("parseRenamePaths", () => { 5 | test("should handle rename with no prefix or suffix", () => { 6 | const input = "{old => new}"; 7 | const expected = { 8 | fromPath: "old", 9 | toPath: "new", 10 | }; 11 | assert.deepStrictEqual(parseRenamePaths(input), expected); 12 | }); 13 | 14 | test("should handle rename with only suffix", () => { 15 | const input = "{old => new}.txt"; 16 | const expected = { 17 | fromPath: "old.txt", 18 | toPath: "new.txt", 19 | }; 20 | assert.deepStrictEqual(parseRenamePaths(input), expected); 21 | }); 22 | 23 | test("should handle rename with only prefix", () => { 24 | const input = "prefix/{old => new}"; 25 | const expected = { 26 | fromPath: "prefix/old", 27 | toPath: "prefix/new", 28 | }; 29 | assert.deepStrictEqual(parseRenamePaths(input), expected); 30 | }); 31 | 32 | test("should handle empty fromPart", () => { 33 | const input = "src/test/{ => basic-suite}/main.test.ts"; 34 | const expected = { 35 | fromPath: "src/test/main.test.ts", 36 | toPath: "src/test/basic-suite/main.test.ts", 37 | }; 38 | assert.deepStrictEqual(parseRenamePaths(input), expected); 39 | }); 40 | 41 | test("should handle empty toPart", () => { 42 | const input = "src/{old => }/file.ts"; 43 | const expected = { 44 | fromPath: "src/old/file.ts", 45 | toPath: "src/file.ts", 46 | }; 47 | assert.deepStrictEqual(parseRenamePaths(input), expected); 48 | }); 49 | 50 | test("should parse rename with leading and trailing directories", () => { 51 | const input = "a/b/{c => d}/e/f.txt"; 52 | const expected = { 53 | fromPath: "a/b/c/e/f.txt", 54 | toPath: "a/b/d/e/f.txt", 55 | }; 56 | assert.deepStrictEqual(parseRenamePaths(input), expected); 57 | }); 58 | 59 | test("should handle extra spaces within curly braces", () => { 60 | const input = "src/test/{ => basic-suite }/main.test.ts"; 61 | const expected = { 62 | fromPath: "src/test/main.test.ts", 63 | toPath: "src/test/basic-suite/main.test.ts", 64 | }; 65 | assert.deepStrictEqual(parseRenamePaths(input), expected); 66 | }); 67 | 68 | test("should handle paths with dots in segments", () => { 69 | const input = "src/my.component/{old.module => new.module}/index.ts"; 70 | const expected = { 71 | fromPath: "src/my.component/old.module/index.ts", 72 | toPath: "src/my.component/new.module/index.ts", 73 | }; 74 | assert.deepStrictEqual(parseRenamePaths(input), expected); 75 | }); 76 | 77 | test("should handle paths with spaces", () => { 78 | // This test depends on how robust the regex is to special path characters. 79 | // The current regex is simple and might fail with complex characters. 80 | const input = "src folder/{a b => c d}/file name with spaces.txt"; 81 | const expected = { 82 | fromPath: "src folder/a b/file name with spaces.txt", 83 | toPath: "src folder/c d/file name with spaces.txt", 84 | }; 85 | assert.deepStrictEqual(parseRenamePaths(input), expected); 86 | }); 87 | 88 | test("should return null for simple rename without curly braces", () => { 89 | const input = "old.txt => new.txt"; 90 | assert.strictEqual(parseRenamePaths(input), null); 91 | }); 92 | 93 | test("should return null for non-rename lines", () => { 94 | const input = "M src/some/file.ts"; 95 | assert.strictEqual(parseRenamePaths(input), null); 96 | }); 97 | 98 | test("should return null for empty input", () => { 99 | const input = ""; 100 | assert.strictEqual(parseRenamePaths(input), null); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import os from "os"; 4 | 5 | import { runTests } from "@vscode/test-electron"; 6 | import { execPromise } from "./utils"; 7 | 8 | async function main() { 9 | try { 10 | // The folder containing the Extension Manifest package.json 11 | // Passed to `--extensionDevelopmentPath` 12 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 13 | 14 | // The path to the extension test runner script (output from esbuild) 15 | // Passed to --extensionTestsPath 16 | const extensionTestsPath = path.resolve(__dirname, "./runner.js"); 17 | 18 | const testRepoPath = await fs.mkdtemp(path.join(os.tmpdir(), "jjk-test-")); 19 | 20 | console.log(`Creating test repo in ${testRepoPath}`); 21 | await execPromise("jj init --git", { 22 | cwd: testRepoPath, 23 | }); 24 | 25 | // Download VS Code, unzip it and run the integration test 26 | await runTests({ 27 | extensionDevelopmentPath, 28 | extensionTestsPath, 29 | launchArgs: [testRepoPath], 30 | }); 31 | } catch (err) { 32 | console.error(err); 33 | console.error("Failed to run tests"); 34 | process.exit(1); 35 | } 36 | } 37 | 38 | void main(); 39 | -------------------------------------------------------------------------------- /src/test/runner.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import Mocha from "mocha"; 3 | 4 | export function run( 5 | testsRoot: string, // This will be out/test/runner.js 6 | cb: (error: unknown, failures?: number) => void, 7 | ): void { 8 | const mocha = new Mocha({ 9 | ui: "tdd", 10 | timeout: 30_000, 11 | }); 12 | 13 | // Path to the bundled file containing all tests 14 | const allTestsBundlePath = path.resolve( 15 | path.dirname(testsRoot), 16 | "all-tests.js", 17 | ); 18 | 19 | mocha.addFile(allTestsBundlePath); 20 | 21 | try { 22 | mocha.run((failures) => { 23 | cb(null, failures); 24 | }); 25 | } catch (err) { 26 | console.error("Error running Mocha tests:", err); 27 | cb(err); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | 3 | export function execPromise( 4 | command: string, 5 | options?: Parameters["1"], 6 | ): Promise<{ stdout: string; stderr: string }> { 7 | return new Promise((resolve, reject) => { 8 | exec(command, { timeout: 1000, ...options }, (error, stdout, stderr) => { 9 | if (error) { 10 | reject(error); 11 | } else { 12 | resolve({ stdout: stdout.toString(), stderr: stderr.toString() }); 13 | } 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/uri.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { type } from "arktype"; 3 | 4 | const RevUriParams = type({ rev: "string" }); 5 | const DiffOriginalRevUriParams = type({ 6 | diffOriginalRev: "string", 7 | }); 8 | const JJUriParams = type(RevUriParams, "|", DiffOriginalRevUriParams); 9 | 10 | export type JJUriParams = typeof JJUriParams.infer; 11 | 12 | /** 13 | * Use this for any URI that will go to JJFileSystemProvider. 14 | */ 15 | export function toJJUri(uri: Uri, params: JJUriParams): Uri { 16 | return uri.with({ 17 | scheme: "jj", 18 | query: JSON.stringify(params), 19 | }); 20 | } 21 | 22 | export function getParams(uri: Uri) { 23 | if (uri.query === "") { 24 | throw new Error("URI has no query"); 25 | } 26 | const parsed = JJUriParams(JSON.parse(uri.query)); 27 | if (parsed instanceof type.errors) { 28 | throw new Error("URI query is not JJUriParams"); 29 | } 30 | return parsed; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { sep } from "path"; 2 | import { Event, Disposable, window, TabInputTextDiff } from "vscode"; 3 | 4 | export const isMacintosh = process.platform === "darwin"; 5 | export const isWindows = process.platform === "win32"; 6 | 7 | export function dispose(disposables: T[]): T[] { 8 | disposables.forEach((d) => void d.dispose()); 9 | return []; 10 | } 11 | 12 | export function toDisposable(dispose: () => void): Disposable { 13 | return { dispose }; 14 | } 15 | 16 | export function combinedDisposable(disposables: Disposable[]): Disposable { 17 | return toDisposable(() => dispose(disposables)); 18 | } 19 | 20 | export function filterEvent( 21 | event: Event, 22 | filter: (e: T) => boolean, 23 | ): Event { 24 | return ( 25 | listener: (e: T) => any, // eslint-disable-line @typescript-eslint/no-explicit-any 26 | thisArgs?: any, // eslint-disable-line @typescript-eslint/no-explicit-any 27 | disposables?: Disposable[], 28 | ) => event((e) => filter(e) && listener.call(thisArgs, e), null, disposables); // eslint-disable-line @typescript-eslint/no-unsafe-return 29 | } 30 | 31 | export function anyEvent(...events: Event[]): Event { 32 | return ( 33 | listener: (e: T) => unknown, 34 | thisArgs?: unknown, 35 | disposables?: Disposable[], 36 | ) => { 37 | const result = combinedDisposable( 38 | events.map((event) => event((i) => listener.call(thisArgs, i))), 39 | ); 40 | 41 | disposables?.push(result); 42 | 43 | return result; 44 | }; 45 | } 46 | 47 | export function onceEvent(event: Event): Event { 48 | return ( 49 | listener: (e: T) => unknown, 50 | thisArgs?: unknown, 51 | disposables?: Disposable[], 52 | ) => { 53 | const result = event( 54 | (e) => { 55 | result.dispose(); 56 | return listener.call(thisArgs, e); 57 | }, 58 | null, 59 | disposables, 60 | ); 61 | 62 | return result; 63 | }; 64 | } 65 | 66 | export function eventToPromise(event: Event): Promise { 67 | return new Promise((c) => onceEvent(event)(c)); 68 | } 69 | 70 | function normalizePath(path: string): string { 71 | // Windows & Mac are currently being handled 72 | // as case insensitive file systems in VS Code. 73 | if (isWindows || isMacintosh) { 74 | return path.toLowerCase(); 75 | } 76 | 77 | return path; 78 | } 79 | 80 | export function isDescendant(parent: string, descendant: string): boolean { 81 | if (parent === descendant) { 82 | return true; 83 | } 84 | 85 | if (parent.charAt(parent.length - 1) !== sep) { 86 | parent += sep; 87 | } 88 | 89 | return normalizePath(descendant).startsWith(normalizePath(parent)); 90 | } 91 | 92 | export function pathEquals(a: string, b: string): boolean { 93 | return normalizePath(a) === normalizePath(b); 94 | } 95 | 96 | /** 97 | * Creates a throttled version of an async function that ensures the underlying 98 | * function (`fn`) is called at most once concurrently. 99 | * 100 | * If the throttled function is called while `fn` is already running: 101 | * - It schedules `fn` to run again immediately after the current run finishes. 102 | * - Only one run can be scheduled this way. 103 | * - If called multiple times while a run is active and another is scheduled, 104 | * the arguments for the scheduled run are updated to the latest arguments provided. 105 | * - The promise returned by calls made while active/scheduled will resolve or 106 | * reject with the result of the *next* scheduled run. 107 | * 108 | * @template T The return type of the async function's Promise. 109 | * @template A The argument types of the async function. 110 | * @param fn The async function to throttle. 111 | * @returns A new function that throttles calls to `fn`. 112 | */ 113 | export function createThrottledAsyncFn( 114 | fn: (...args: A) => Promise, 115 | ): (...args: A) => Promise { 116 | enum State { 117 | Idle, 118 | Running, 119 | Queued, 120 | } 121 | let state = State.Idle; 122 | let queuedArgs: A | null = null; 123 | // Promise returned to callers who triggered the queued run 124 | let queuedRunPromise: Promise | null = null; 125 | let queuedRunResolver: ((value: T) => void) | null = null; 126 | let queuedRunRejector: 127 | | Parameters["0"]>["1"] 128 | | null = null; 129 | 130 | const throttledFn = (...args: A): Promise => { 131 | queuedArgs = args; // Always store the latest args for a potential queued run 132 | 133 | if (state === State.Running || state === State.Queued) { 134 | // If already running or queued, ensure we are in Queued state 135 | // and return the promise for the queued run. 136 | if (state !== State.Queued) { 137 | state = State.Queued; 138 | queuedRunPromise = new Promise((resolve, reject) => { 139 | queuedRunResolver = resolve; 140 | queuedRunRejector = reject; 141 | }); 142 | } 143 | // This assertion is safe because we ensure queuedRunPromise is set when state becomes Queued. 144 | return queuedRunPromise!; 145 | } 146 | 147 | // State is Idle, transition to Running 148 | state = State.Running; 149 | // Execute with current args. Capture the promise for this specific run. 150 | const runPromise = fn(...args); 151 | 152 | // Set up the logic to handle completion of the current run 153 | runPromise.then( 154 | (_result) => { 155 | // --- Success path --- 156 | if (state === State.Queued) { 157 | // A run was queued while this one was running. 158 | const resolver = queuedRunResolver!; 159 | const rejector = queuedRunRejector!; 160 | const nextArgs = queuedArgs!; // Use the last stored args 161 | 162 | // Reset queue state *before* starting the next run 163 | queuedRunPromise = null; 164 | queuedRunResolver = null; 165 | queuedRunRejector = null; 166 | queuedArgs = null; 167 | state = State.Idle; // Temporarily Idle, the recursive call below will set it back to Running 168 | 169 | // Start the next run recursively. 170 | // Link its result back to the promise we returned to the queued caller(s). 171 | throttledFn(...nextArgs).then(resolver, rejector); 172 | } else { 173 | // No run was queued, simply return to Idle state. 174 | state = State.Idle; 175 | } 176 | // Note: We don't return the result here; the original runPromise already holds it. 177 | }, 178 | (error) => { 179 | // --- Error path --- 180 | if (state === State.Queued) { 181 | // A run was queued, but the current one failed. 182 | // Reject the promise that was returned to the queued caller(s). 183 | const rejector = queuedRunRejector!; 184 | 185 | // Reset queue state 186 | queuedRunPromise = null; 187 | queuedRunResolver = null; 188 | queuedRunRejector = null; 189 | queuedArgs = null; 190 | state = State.Idle; 191 | 192 | rejector(error); // Reject the queued promise 193 | } else { 194 | // No run was queued, simply return to Idle state. 195 | state = State.Idle; 196 | } 197 | // Note: We don't re-throw the error here; the original runPromise already handles rejection. 198 | }, 199 | ); 200 | 201 | // Return the promise for the *current* execution immediately. 202 | return runPromise; 203 | }; 204 | 205 | return throttledFn; 206 | } 207 | 208 | export function getActiveTextEditorDiff(): TabInputTextDiff | undefined { 209 | const activeTextEditor = window.activeTextEditor; 210 | if (!activeTextEditor) { 211 | return undefined; 212 | } 213 | 214 | const activeTab = window.tabGroups.activeTabGroup.activeTab; 215 | if (!activeTab) { 216 | return undefined; 217 | } 218 | 219 | // detecting a diff editor: https://github.com/microsoft/vscode/issues/15513 220 | const isDiff = 221 | activeTab.input instanceof TabInputTextDiff && 222 | (activeTab.input.modified?.toString() === 223 | activeTextEditor.document.uri.toString() || 224 | activeTab.input.original?.toString() === 225 | activeTextEditor.document.uri.toString()); 226 | 227 | if (!isDiff) { 228 | return undefined; 229 | } 230 | 231 | return activeTab.input; 232 | } 233 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/arraysFind.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Comparator } from './arrays.js'; 7 | 8 | export function findLast(array: readonly T[], predicate: (item: T) => unknown, fromIndex = array.length - 1): T | undefined { 9 | const idx = findLastIdx(array, predicate, fromIndex); 10 | if (idx === -1) { 11 | return undefined; 12 | } 13 | return array[idx]; 14 | } 15 | 16 | export function findLastIdx(array: readonly T[], predicate: (item: T) => unknown, fromIndex = array.length - 1): number { 17 | for (let i = fromIndex; i >= 0; i--) { 18 | const element = array[i]; 19 | 20 | if (predicate(element)) { 21 | return i; 22 | } 23 | } 24 | 25 | return -1; 26 | } 27 | 28 | /** 29 | * Finds the last item where predicate is true using binary search. 30 | * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`! 31 | * 32 | * @returns `undefined` if no item matches, otherwise the last item that matches the predicate. 33 | */ 34 | export function findLastMonotonous(array: readonly T[], predicate: (item: T) => boolean): T | undefined { 35 | const idx = findLastIdxMonotonous(array, predicate); 36 | return idx === -1 ? undefined : array[idx]; 37 | } 38 | 39 | /** 40 | * Finds the last item where predicate is true using binary search. 41 | * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`! 42 | * 43 | * @returns `startIdx - 1` if predicate is false for all items, otherwise the index of the last item that matches the predicate. 44 | */ 45 | export function findLastIdxMonotonous(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number { 46 | let i = startIdx; 47 | let j = endIdxEx; 48 | while (i < j) { 49 | const k = Math.floor((i + j) / 2); 50 | if (predicate(array[k])) { 51 | i = k + 1; 52 | } else { 53 | j = k; 54 | } 55 | } 56 | return i - 1; 57 | } 58 | 59 | /** 60 | * Finds the first item where predicate is true using binary search. 61 | * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`! 62 | * 63 | * @returns `undefined` if no item matches, otherwise the first item that matches the predicate. 64 | */ 65 | export function findFirstMonotonous(array: readonly T[], predicate: (item: T) => boolean): T | undefined { 66 | const idx = findFirstIdxMonotonousOrArrLen(array, predicate); 67 | return idx === array.length ? undefined : array[idx]; 68 | } 69 | 70 | /** 71 | * Finds the first item where predicate is true using binary search. 72 | * `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`! 73 | * 74 | * @returns `endIdxEx` if predicate is false for all items, otherwise the index of the first item that matches the predicate. 75 | */ 76 | export function findFirstIdxMonotonousOrArrLen(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number { 77 | let i = startIdx; 78 | let j = endIdxEx; 79 | while (i < j) { 80 | const k = Math.floor((i + j) / 2); 81 | if (predicate(array[k])) { 82 | j = k; 83 | } else { 84 | i = k + 1; 85 | } 86 | } 87 | return i; 88 | } 89 | 90 | export function findFirstIdxMonotonous(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number { 91 | const idx = findFirstIdxMonotonousOrArrLen(array, predicate, startIdx, endIdxEx); 92 | return idx === array.length ? -1 : idx; 93 | } 94 | 95 | /** 96 | * Use this when 97 | * * You have a sorted array 98 | * * You query this array with a monotonous predicate to find the last item that has a certain property. 99 | * * You query this array multiple times with monotonous predicates that get weaker and weaker. 100 | */ 101 | export class MonotonousArray { 102 | public static assertInvariants = false; 103 | 104 | private _findLastMonotonousLastIdx = 0; 105 | private _prevFindLastPredicate: ((item: T) => boolean) | undefined; 106 | 107 | constructor(private readonly _array: readonly T[]) { 108 | } 109 | 110 | /** 111 | * The predicate must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`! 112 | * For subsequent calls, current predicate must be weaker than (or equal to) the previous predicate, i.e. more entries must be `true`. 113 | */ 114 | findLastMonotonous(predicate: (item: T) => boolean): T | undefined { 115 | if (MonotonousArray.assertInvariants) { 116 | if (this._prevFindLastPredicate) { 117 | for (const item of this._array) { 118 | if (this._prevFindLastPredicate(item) && !predicate(item)) { 119 | throw new Error('MonotonousArray: current predicate must be weaker than (or equal to) the previous predicate.'); 120 | } 121 | } 122 | } 123 | this._prevFindLastPredicate = predicate; 124 | } 125 | 126 | const idx = findLastIdxMonotonous(this._array, predicate, this._findLastMonotonousLastIdx); 127 | this._findLastMonotonousLastIdx = idx + 1; 128 | return idx === -1 ? undefined : this._array[idx]; 129 | } 130 | } 131 | 132 | /** 133 | * Returns the first item that is equal to or greater than every other item. 134 | */ 135 | export function findFirstMax(array: readonly T[], comparator: Comparator): T | undefined { 136 | if (array.length === 0) { 137 | return undefined; 138 | } 139 | 140 | let max = array[0]; 141 | for (let i = 1; i < array.length; i++) { 142 | const item = array[i]; 143 | if (comparator(item, max) > 0) { 144 | max = item; 145 | } 146 | } 147 | return max; 148 | } 149 | 150 | /** 151 | * Returns the last item that is equal to or greater than every other item. 152 | */ 153 | export function findLastMax(array: readonly T[], comparator: Comparator): T | undefined { 154 | if (array.length === 0) { 155 | return undefined; 156 | } 157 | 158 | let max = array[0]; 159 | for (let i = 1; i < array.length; i++) { 160 | const item = array[i]; 161 | if (comparator(item, max) >= 0) { 162 | max = item; 163 | } 164 | } 165 | return max; 166 | } 167 | 168 | /** 169 | * Returns the first item that is equal to or less than every other item. 170 | */ 171 | export function findFirstMin(array: readonly T[], comparator: Comparator): T | undefined { 172 | return findFirstMax(array, (a, b) => -comparator(a, b)); 173 | } 174 | 175 | export function findMaxIdx(array: readonly T[], comparator: Comparator): number { 176 | if (array.length === 0) { 177 | return -1; 178 | } 179 | 180 | let maxIdx = 0; 181 | for (let i = 1; i < array.length; i++) { 182 | const item = array[i]; 183 | if (comparator(item, array[maxIdx]) > 0) { 184 | maxIdx = i; 185 | } 186 | } 187 | return maxIdx; 188 | } 189 | 190 | /** 191 | * Returns the first mapped value of the array which is not undefined. 192 | */ 193 | export function mapFindFirst(items: Iterable, mapFn: (value: T) => R | undefined): R | undefined { 194 | for (const value of items) { 195 | const mapped = mapFn(value); 196 | if (mapped !== undefined) { 197 | return mapped; 198 | } 199 | } 200 | 201 | return undefined; 202 | } 203 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/assert.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { BugIndicatingError, onUnexpectedError } from './errors.js'; 7 | 8 | /** 9 | * Throws an error with the provided message if the provided value does not evaluate to a true Javascript value. 10 | * 11 | * @deprecated Use `assert(...)` instead. 12 | * This method is usually used like this: 13 | * ```ts 14 | * import * as assert from 'vs/base/common/assert'; 15 | * assert.ok(...); 16 | * ``` 17 | * 18 | * However, `assert` in that example is a user chosen name. 19 | * There is no tooling for generating such an import statement. 20 | * Thus, the `assert(...)` function should be used instead. 21 | */ 22 | export function ok(value?: unknown, message?: string) { 23 | if (!value) { 24 | throw new Error(message ? `Assertion failed (${message})` : 'Assertion Failed'); 25 | } 26 | } 27 | 28 | export function assertNever(value: never, message = 'Unreachable'): never { 29 | throw new Error(message); 30 | } 31 | 32 | /** 33 | * Asserts that a condition is `truthy`. 34 | * 35 | * @throws provided {@linkcode messageOrError} if the {@linkcode condition} is `falsy`. 36 | * 37 | * @param condition The condition to assert. 38 | * @param messageOrError An error message or error object to throw if condition is `falsy`. 39 | */ 40 | export function assert( 41 | condition: boolean, 42 | messageOrError: string | Error = 'unexpected state', 43 | ): asserts condition { 44 | if (!condition) { 45 | // if error instance is provided, use it, otherwise create a new one 46 | const errorToThrow = typeof messageOrError === 'string' 47 | ? new BugIndicatingError(`Assertion Failed: ${messageOrError}`) 48 | : messageOrError; 49 | 50 | throw errorToThrow; 51 | } 52 | } 53 | 54 | /** 55 | * Like assert, but doesn't throw. 56 | */ 57 | export function softAssert(condition: boolean, message = 'Soft Assertion Failed'): void { 58 | if (!condition) { 59 | onUnexpectedError(new BugIndicatingError(message)); 60 | } 61 | } 62 | 63 | /** 64 | * condition must be side-effect free! 65 | */ 66 | export function assertFn(condition: () => boolean): void { 67 | if (!condition()) { 68 | // eslint-disable-next-line no-debugger 69 | debugger; 70 | // Reevaluate `condition` again to make debugging easier 71 | condition(); 72 | onUnexpectedError(new BugIndicatingError('Assertion Failed')); 73 | } 74 | } 75 | 76 | export function checkAdjacentItems(items: readonly T[], predicate: (item1: T, item2: T) => boolean): boolean { 77 | let i = 0; 78 | while (i < items.length - 1) { 79 | const a = items[i]; 80 | const b = items[i + 1]; 81 | if (!predicate(a, b)) { 82 | return false; 83 | } 84 | i++; 85 | } 86 | return true; 87 | } 88 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/diff/diffChange.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * Represents information about a specific difference between two sequences. 8 | */ 9 | export class DiffChange { 10 | 11 | /** 12 | * The position of the first element in the original sequence which 13 | * this change affects. 14 | */ 15 | public originalStart: number; 16 | 17 | /** 18 | * The number of elements from the original sequence which were 19 | * affected. 20 | */ 21 | public originalLength: number; 22 | 23 | /** 24 | * The position of the first element in the modified sequence which 25 | * this change affects. 26 | */ 27 | public modifiedStart: number; 28 | 29 | /** 30 | * The number of elements from the modified sequence which were 31 | * affected (added). 32 | */ 33 | public modifiedLength: number; 34 | 35 | /** 36 | * Constructs a new DiffChange with the given sequence information 37 | * and content. 38 | */ 39 | constructor(originalStart: number, originalLength: number, modifiedStart: number, modifiedLength: number) { 40 | //Debug.Assert(originalLength > 0 || modifiedLength > 0, "originalLength and modifiedLength cannot both be <= 0"); 41 | this.originalStart = originalStart; 42 | this.originalLength = originalLength; 43 | this.modifiedStart = modifiedStart; 44 | this.modifiedLength = modifiedLength; 45 | } 46 | 47 | /** 48 | * The end point (exclusive) of the change in the original sequence. 49 | */ 50 | public getOriginalEnd() { 51 | return this.originalStart + this.originalLength; 52 | } 53 | 54 | /** 55 | * The end point (exclusive) of the change in the modified sequence. 56 | */ 57 | public getModifiedEnd() { 58 | return this.modifiedStart + this.modifiedLength; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/errors.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface ErrorListenerCallback { 7 | (error: any): void; 8 | } 9 | 10 | export interface ErrorListenerUnbind { 11 | (): void; 12 | } 13 | 14 | // Avoid circular dependency on EventEmitter by implementing a subset of the interface. 15 | export class ErrorHandler { 16 | private unexpectedErrorHandler: (e: any) => void; 17 | private listeners: ErrorListenerCallback[]; 18 | 19 | constructor() { 20 | 21 | this.listeners = []; 22 | 23 | this.unexpectedErrorHandler = function (e: any) { 24 | setTimeout(() => { 25 | if (e.stack) { 26 | if (ErrorNoTelemetry.isErrorNoTelemetry(e)) { 27 | throw new ErrorNoTelemetry(e.message + '\n\n' + e.stack); 28 | } 29 | 30 | throw new Error(e.message + '\n\n' + e.stack); 31 | } 32 | 33 | throw e; 34 | }, 0); 35 | }; 36 | } 37 | 38 | addListener(listener: ErrorListenerCallback): ErrorListenerUnbind { 39 | this.listeners.push(listener); 40 | 41 | return () => { 42 | this._removeListener(listener); 43 | }; 44 | } 45 | 46 | private emit(e: any): void { 47 | this.listeners.forEach((listener) => { 48 | listener(e); 49 | }); 50 | } 51 | 52 | private _removeListener(listener: ErrorListenerCallback): void { 53 | this.listeners.splice(this.listeners.indexOf(listener), 1); 54 | } 55 | 56 | setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void { 57 | this.unexpectedErrorHandler = newUnexpectedErrorHandler; 58 | } 59 | 60 | getUnexpectedErrorHandler(): (e: any) => void { 61 | return this.unexpectedErrorHandler; 62 | } 63 | 64 | onUnexpectedError(e: any): void { 65 | this.unexpectedErrorHandler(e); 66 | this.emit(e); 67 | } 68 | 69 | // For external errors, we don't want the listeners to be called 70 | onUnexpectedExternalError(e: any): void { 71 | this.unexpectedErrorHandler(e); 72 | } 73 | } 74 | 75 | export const errorHandler = new ErrorHandler(); 76 | 77 | /** @skipMangle */ 78 | export function setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void { 79 | errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler); 80 | } 81 | 82 | /** 83 | * Returns if the error is a SIGPIPE error. SIGPIPE errors should generally be 84 | * logged at most once, to avoid a loop. 85 | * 86 | * @see https://github.com/microsoft/vscode-remote-release/issues/6481 87 | */ 88 | export function isSigPipeError(e: unknown): e is Error { 89 | if (!e || typeof e !== 'object') { 90 | return false; 91 | } 92 | 93 | const cast = e as Record; 94 | return cast.code === 'EPIPE' && cast.syscall?.toUpperCase() === 'WRITE'; 95 | } 96 | 97 | /** 98 | * This function should only be called with errors that indicate a bug in the product. 99 | * E.g. buggy extensions/invalid user-input/network issues should not be able to trigger this code path. 100 | * If they are, this indicates there is also a bug in the product. 101 | */ 102 | export function onBugIndicatingError(e: any): undefined { 103 | errorHandler.onUnexpectedError(e); 104 | return undefined; 105 | } 106 | 107 | export function onUnexpectedError(e: any): undefined { 108 | // ignore errors from cancelled promises 109 | if (!isCancellationError(e)) { 110 | errorHandler.onUnexpectedError(e); 111 | } 112 | return undefined; 113 | } 114 | 115 | export function onUnexpectedExternalError(e: any): undefined { 116 | // ignore errors from cancelled promises 117 | if (!isCancellationError(e)) { 118 | errorHandler.onUnexpectedExternalError(e); 119 | } 120 | return undefined; 121 | } 122 | 123 | export interface SerializedError { 124 | readonly $isError: true; 125 | readonly name: string; 126 | readonly message: string; 127 | readonly stack: string; 128 | readonly noTelemetry: boolean; 129 | readonly code?: string; 130 | readonly cause?: SerializedError; 131 | } 132 | 133 | type ErrorWithCode = Error & { 134 | code: string | undefined; 135 | }; 136 | 137 | export function transformErrorForSerialization(error: Error): SerializedError; 138 | export function transformErrorForSerialization(error: any): any; 139 | export function transformErrorForSerialization(error: any): any { 140 | if (error instanceof Error) { 141 | const { name, message, cause } = error; 142 | const stack: string = (error).stacktrace || (error).stack; 143 | return { 144 | $isError: true, 145 | name, 146 | message, 147 | stack, 148 | noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error), 149 | cause: cause ? transformErrorForSerialization(cause) : undefined, 150 | code: (error).code 151 | }; 152 | } 153 | 154 | // return as is 155 | return error; 156 | } 157 | 158 | export function transformErrorFromSerialization(data: SerializedError): Error { 159 | let error: Error; 160 | if (data.noTelemetry) { 161 | error = new ErrorNoTelemetry(); 162 | } else { 163 | error = new Error(); 164 | error.name = data.name; 165 | } 166 | error.message = data.message; 167 | error.stack = data.stack; 168 | if (data.code) { 169 | (error).code = data.code; 170 | } 171 | if (data.cause) { 172 | error.cause = transformErrorFromSerialization(data.cause); 173 | } 174 | return error; 175 | } 176 | 177 | // see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces 178 | export interface V8CallSite { 179 | getThis(): unknown; 180 | getTypeName(): string | null; 181 | getFunction(): Function | undefined; 182 | getFunctionName(): string | null; 183 | getMethodName(): string | null; 184 | getFileName(): string | null; 185 | getLineNumber(): number | null; 186 | getColumnNumber(): number | null; 187 | getEvalOrigin(): string | undefined; 188 | isToplevel(): boolean; 189 | isEval(): boolean; 190 | isNative(): boolean; 191 | isConstructor(): boolean; 192 | toString(): string; 193 | } 194 | 195 | const canceledName = 'Canceled'; 196 | 197 | /** 198 | * Checks if the given error is a promise in canceled state 199 | */ 200 | export function isCancellationError(error: any): boolean { 201 | if (error instanceof CancellationError) { 202 | return true; 203 | } 204 | return error instanceof Error && error.name === canceledName && error.message === canceledName; 205 | } 206 | 207 | // !!!IMPORTANT!!! 208 | // Do NOT change this class because it is also used as an API-type. 209 | export class CancellationError extends Error { 210 | constructor() { 211 | super(canceledName); 212 | this.name = this.message; 213 | } 214 | } 215 | 216 | /** 217 | * @deprecated use {@link CancellationError `new CancellationError()`} instead 218 | */ 219 | export function canceled(): Error { 220 | const error = new Error(canceledName); 221 | error.name = error.message; 222 | return error; 223 | } 224 | 225 | export function illegalArgument(name?: string): Error { 226 | if (name) { 227 | return new Error(`Illegal argument: ${name}`); 228 | } else { 229 | return new Error('Illegal argument'); 230 | } 231 | } 232 | 233 | export function illegalState(name?: string): Error { 234 | if (name) { 235 | return new Error(`Illegal state: ${name}`); 236 | } else { 237 | return new Error('Illegal state'); 238 | } 239 | } 240 | 241 | export class ReadonlyError extends TypeError { 242 | constructor(name?: string) { 243 | super(name ? `${name} is read-only and cannot be changed` : 'Cannot change read-only property'); 244 | } 245 | } 246 | 247 | export function getErrorMessage(err: any): string { 248 | if (!err) { 249 | return 'Error'; 250 | } 251 | 252 | if (err.message) { 253 | return err.message; 254 | } 255 | 256 | if (err.stack) { 257 | return err.stack.split('\n')[0]; 258 | } 259 | 260 | return String(err); 261 | } 262 | 263 | export class NotImplementedError extends Error { 264 | constructor(message?: string) { 265 | super('NotImplemented'); 266 | if (message) { 267 | this.message = message; 268 | } 269 | } 270 | } 271 | 272 | export class NotSupportedError extends Error { 273 | constructor(message?: string) { 274 | super('NotSupported'); 275 | if (message) { 276 | this.message = message; 277 | } 278 | } 279 | } 280 | 281 | export class ExpectedError extends Error { 282 | readonly isExpected = true; 283 | } 284 | 285 | /** 286 | * Error that when thrown won't be logged in telemetry as an unhandled error. 287 | */ 288 | export class ErrorNoTelemetry extends Error { 289 | override readonly name: string; 290 | 291 | constructor(msg?: string) { 292 | super(msg); 293 | this.name = 'CodeExpectedError'; 294 | } 295 | 296 | public static fromError(err: Error): ErrorNoTelemetry { 297 | if (err instanceof ErrorNoTelemetry) { 298 | return err; 299 | } 300 | 301 | const result = new ErrorNoTelemetry(); 302 | result.message = err.message; 303 | result.stack = err.stack; 304 | return result; 305 | } 306 | 307 | public static isErrorNoTelemetry(err: Error): err is ErrorNoTelemetry { 308 | return err.name === 'CodeExpectedError'; 309 | } 310 | } 311 | 312 | /** 313 | * This error indicates a bug. 314 | * Do not throw this for invalid user input. 315 | * Only catch this error to recover gracefully from bugs. 316 | */ 317 | export class BugIndicatingError extends Error { 318 | constructor(message?: string) { 319 | super(message || 'An unexpected bug occurred.'); 320 | Object.setPrototypeOf(this, BugIndicatingError.prototype); 321 | 322 | // Because we know for sure only buggy code throws this, 323 | // we definitely want to break here and fix the bug. 324 | // debugger; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/hash.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as strings from './strings.js'; 7 | 8 | type NotSyncHashable = ArrayBufferLike | ArrayBufferView; 9 | 10 | /** 11 | * Return a hash value for an object. 12 | * 13 | * Note that this should not be used for binary data types. Instead, 14 | * prefer {@link hashAsync}. 15 | */ 16 | export function hash(obj: T extends NotSyncHashable ? never : T): number { 17 | return doHash(obj, 0); 18 | } 19 | 20 | export function doHash(obj: any, hashVal: number): number { 21 | switch (typeof obj) { 22 | case 'object': 23 | if (obj === null) { 24 | return numberHash(349, hashVal); 25 | } else if (Array.isArray(obj)) { 26 | return arrayHash(obj, hashVal); 27 | } 28 | return objectHash(obj, hashVal); 29 | case 'string': 30 | return stringHash(obj, hashVal); 31 | case 'boolean': 32 | return booleanHash(obj, hashVal); 33 | case 'number': 34 | return numberHash(obj, hashVal); 35 | case 'undefined': 36 | return numberHash(937, hashVal); 37 | default: 38 | return numberHash(617, hashVal); 39 | } 40 | } 41 | 42 | export function numberHash(val: number, initialHashVal: number): number { 43 | return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32 44 | } 45 | 46 | function booleanHash(b: boolean, initialHashVal: number): number { 47 | return numberHash(b ? 433 : 863, initialHashVal); 48 | } 49 | 50 | export function stringHash(s: string, hashVal: number) { 51 | hashVal = numberHash(149417, hashVal); 52 | for (let i = 0, length = s.length; i < length; i++) { 53 | hashVal = numberHash(s.charCodeAt(i), hashVal); 54 | } 55 | return hashVal; 56 | } 57 | 58 | function arrayHash(arr: any[], initialHashVal: number): number { 59 | initialHashVal = numberHash(104579, initialHashVal); 60 | return arr.reduce((hashVal, item) => doHash(item, hashVal), initialHashVal); 61 | } 62 | 63 | function objectHash(obj: any, initialHashVal: number): number { 64 | initialHashVal = numberHash(181387, initialHashVal); 65 | return Object.keys(obj).sort().reduce((hashVal, key) => { 66 | hashVal = stringHash(key, hashVal); 67 | return doHash(obj[key], hashVal); 68 | }, initialHashVal); 69 | } 70 | 71 | const enum SHA1Constant { 72 | BLOCK_SIZE = 64, // 512 / 8 73 | UNICODE_REPLACEMENT = 0xFFFD, 74 | } 75 | 76 | function leftRotate(value: number, bits: number, totalBits: number = 32): number { 77 | // delta + bits = totalBits 78 | const delta = totalBits - bits; 79 | 80 | // All ones, expect `delta` zeros aligned to the right 81 | const mask = ~((1 << delta) - 1); 82 | 83 | // Join (value left-shifted `bits` bits) with (masked value right-shifted `delta` bits) 84 | return ((value << bits) | ((mask & value) >>> delta)) >>> 0; 85 | } 86 | 87 | function toHexString(buffer: ArrayBuffer): string; 88 | function toHexString(value: number, bitsize?: number): string; 89 | function toHexString(bufferOrValue: ArrayBuffer | number, bitsize: number = 32): string { 90 | if (bufferOrValue instanceof ArrayBuffer) { 91 | return Array.from(new Uint8Array(bufferOrValue)).map(b => b.toString(16).padStart(2, '0')).join(''); 92 | } 93 | 94 | return (bufferOrValue >>> 0).toString(16).padStart(bitsize / 4, '0'); 95 | } 96 | 97 | /** 98 | * A SHA1 implementation that works with strings and does not allocate. 99 | * 100 | * Prefer to use {@link hashAsync} in async contexts 101 | */ 102 | export class StringSHA1 { 103 | private static _bigBlock32 = new DataView(new ArrayBuffer(320)); // 80 * 4 = 320 104 | 105 | private _h0 = 0x67452301; 106 | private _h1 = 0xEFCDAB89; 107 | private _h2 = 0x98BADCFE; 108 | private _h3 = 0x10325476; 109 | private _h4 = 0xC3D2E1F0; 110 | 111 | private readonly _buff: Uint8Array; 112 | private readonly _buffDV: DataView; 113 | private _buffLen: number; 114 | private _totalLen: number; 115 | private _leftoverHighSurrogate: number; 116 | private _finished: boolean; 117 | 118 | constructor() { 119 | this._buff = new Uint8Array(SHA1Constant.BLOCK_SIZE + 3 /* to fit any utf-8 */); 120 | this._buffDV = new DataView(this._buff.buffer); 121 | this._buffLen = 0; 122 | this._totalLen = 0; 123 | this._leftoverHighSurrogate = 0; 124 | this._finished = false; 125 | } 126 | 127 | public update(str: string): void { 128 | const strLen = str.length; 129 | if (strLen === 0) { 130 | return; 131 | } 132 | 133 | const buff = this._buff; 134 | let buffLen = this._buffLen; 135 | let leftoverHighSurrogate = this._leftoverHighSurrogate; 136 | let charCode: number; 137 | let offset: number; 138 | 139 | if (leftoverHighSurrogate !== 0) { 140 | charCode = leftoverHighSurrogate; 141 | offset = -1; 142 | leftoverHighSurrogate = 0; 143 | } else { 144 | charCode = str.charCodeAt(0); 145 | offset = 0; 146 | } 147 | 148 | while (true) { 149 | let codePoint = charCode; 150 | if (strings.isHighSurrogate(charCode)) { 151 | if (offset + 1 < strLen) { 152 | const nextCharCode = str.charCodeAt(offset + 1); 153 | if (strings.isLowSurrogate(nextCharCode)) { 154 | offset++; 155 | codePoint = strings.computeCodePoint(charCode, nextCharCode); 156 | } else { 157 | // illegal => unicode replacement character 158 | codePoint = SHA1Constant.UNICODE_REPLACEMENT; 159 | } 160 | } else { 161 | // last character is a surrogate pair 162 | leftoverHighSurrogate = charCode; 163 | break; 164 | } 165 | } else if (strings.isLowSurrogate(charCode)) { 166 | // illegal => unicode replacement character 167 | codePoint = SHA1Constant.UNICODE_REPLACEMENT; 168 | } 169 | 170 | buffLen = this._push(buff, buffLen, codePoint); 171 | offset++; 172 | if (offset < strLen) { 173 | charCode = str.charCodeAt(offset); 174 | } else { 175 | break; 176 | } 177 | } 178 | 179 | this._buffLen = buffLen; 180 | this._leftoverHighSurrogate = leftoverHighSurrogate; 181 | } 182 | 183 | private _push(buff: Uint8Array, buffLen: number, codePoint: number): number { 184 | if (codePoint < 0x0080) { 185 | buff[buffLen++] = codePoint; 186 | } else if (codePoint < 0x0800) { 187 | buff[buffLen++] = 0b11000000 | ((codePoint & 0b00000000000000000000011111000000) >>> 6); 188 | buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0); 189 | } else if (codePoint < 0x10000) { 190 | buff[buffLen++] = 0b11100000 | ((codePoint & 0b00000000000000001111000000000000) >>> 12); 191 | buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6); 192 | buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0); 193 | } else { 194 | buff[buffLen++] = 0b11110000 | ((codePoint & 0b00000000000111000000000000000000) >>> 18); 195 | buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000111111000000000000) >>> 12); 196 | buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6); 197 | buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0); 198 | } 199 | 200 | if (buffLen >= SHA1Constant.BLOCK_SIZE) { 201 | this._step(); 202 | buffLen -= SHA1Constant.BLOCK_SIZE; 203 | this._totalLen += SHA1Constant.BLOCK_SIZE; 204 | // take last 3 in case of UTF8 overflow 205 | buff[0] = buff[SHA1Constant.BLOCK_SIZE + 0]; 206 | buff[1] = buff[SHA1Constant.BLOCK_SIZE + 1]; 207 | buff[2] = buff[SHA1Constant.BLOCK_SIZE + 2]; 208 | } 209 | 210 | return buffLen; 211 | } 212 | 213 | public digest(): string { 214 | if (!this._finished) { 215 | this._finished = true; 216 | if (this._leftoverHighSurrogate) { 217 | // illegal => unicode replacement character 218 | this._leftoverHighSurrogate = 0; 219 | this._buffLen = this._push(this._buff, this._buffLen, SHA1Constant.UNICODE_REPLACEMENT); 220 | } 221 | this._totalLen += this._buffLen; 222 | this._wrapUp(); 223 | } 224 | 225 | return toHexString(this._h0) + toHexString(this._h1) + toHexString(this._h2) + toHexString(this._h3) + toHexString(this._h4); 226 | } 227 | 228 | private _wrapUp(): void { 229 | this._buff[this._buffLen++] = 0x80; 230 | this._buff.subarray(this._buffLen).fill(0); 231 | 232 | if (this._buffLen > 56) { 233 | this._step(); 234 | this._buff.fill(0); 235 | } 236 | 237 | // this will fit because the mantissa can cover up to 52 bits 238 | const ml = 8 * this._totalLen; 239 | 240 | this._buffDV.setUint32(56, Math.floor(ml / 4294967296), false); 241 | this._buffDV.setUint32(60, ml % 4294967296, false); 242 | 243 | this._step(); 244 | } 245 | 246 | private _step(): void { 247 | const bigBlock32 = StringSHA1._bigBlock32; 248 | const data = this._buffDV; 249 | 250 | for (let j = 0; j < 64 /* 16*4 */; j += 4) { 251 | bigBlock32.setUint32(j, data.getUint32(j, false), false); 252 | } 253 | 254 | for (let j = 64; j < 320 /* 80*4 */; j += 4) { 255 | bigBlock32.setUint32(j, leftRotate((bigBlock32.getUint32(j - 12, false) ^ bigBlock32.getUint32(j - 32, false) ^ bigBlock32.getUint32(j - 56, false) ^ bigBlock32.getUint32(j - 64, false)), 1), false); 256 | } 257 | 258 | let a = this._h0; 259 | let b = this._h1; 260 | let c = this._h2; 261 | let d = this._h3; 262 | let e = this._h4; 263 | 264 | let f: number, k: number; 265 | let temp: number; 266 | 267 | for (let j = 0; j < 80; j++) { 268 | if (j < 20) { 269 | f = (b & c) | ((~b) & d); 270 | k = 0x5A827999; 271 | } else if (j < 40) { 272 | f = b ^ c ^ d; 273 | k = 0x6ED9EBA1; 274 | } else if (j < 60) { 275 | f = (b & c) | (b & d) | (c & d); 276 | k = 0x8F1BBCDC; 277 | } else { 278 | f = b ^ c ^ d; 279 | k = 0xCA62C1D6; 280 | } 281 | 282 | temp = (leftRotate(a, 5) + f + e + k + bigBlock32.getUint32(j * 4, false)) & 0xffffffff; 283 | e = d; 284 | d = c; 285 | c = leftRotate(b, 30); 286 | b = a; 287 | a = temp; 288 | } 289 | 290 | this._h0 = (this._h0 + a) & 0xffffffff; 291 | this._h1 = (this._h1 + b) & 0xffffffff; 292 | this._h2 = (this._h2 + c) & 0xffffffff; 293 | this._h3 = (this._h3 + d) & 0xffffffff; 294 | this._h4 = (this._h4 + e) & 0xffffffff; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/map.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export class SetMap { 7 | 8 | private map = new Map>(); 9 | 10 | add(key: K, value: V): void { 11 | let values = this.map.get(key); 12 | 13 | if (!values) { 14 | values = new Set(); 15 | this.map.set(key, values); 16 | } 17 | 18 | values.add(value); 19 | } 20 | 21 | delete(key: K, value: V): void { 22 | const values = this.map.get(key); 23 | 24 | if (!values) { 25 | return; 26 | } 27 | 28 | values.delete(value); 29 | 30 | if (values.size === 0) { 31 | this.map.delete(key); 32 | } 33 | } 34 | 35 | forEach(key: K, fn: (value: V) => void): void { 36 | const values = this.map.get(key); 37 | 38 | if (!values) { 39 | return; 40 | } 41 | 42 | values.forEach(fn); 43 | } 44 | 45 | get(key: K): ReadonlySet { 46 | const values = this.map.get(key); 47 | if (!values) { 48 | return new Set(); 49 | } 50 | return values; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/vendor/vscode/base/common/uint.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export const enum Constants { 7 | /** 8 | * MAX SMI (SMall Integer) as defined in v8. 9 | * one bit is lost for boxing/unboxing flag. 10 | * one bit is lost for sign flag. 11 | * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values 12 | */ 13 | MAX_SAFE_SMALL_INTEGER = 1 << 30, 14 | 15 | /** 16 | * MIN SMI (SMall Integer) as defined in v8. 17 | * one bit is lost for boxing/unboxing flag. 18 | * one bit is lost for sign flag. 19 | * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values 20 | */ 21 | MIN_SAFE_SMALL_INTEGER = -(1 << 30), 22 | 23 | /** 24 | * Max unsigned integer that fits on 8 bits. 25 | */ 26 | MAX_UINT_8 = 255, // 2^8 - 1 27 | 28 | /** 29 | * Max unsigned integer that fits on 16 bits. 30 | */ 31 | MAX_UINT_16 = 65535, // 2^16 - 1 32 | 33 | /** 34 | * Max unsigned integer that fits on 32 bits. 35 | */ 36 | MAX_UINT_32 = 4294967295, // 2^32 - 1 37 | 38 | UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000 39 | } 40 | 41 | export function toUint8(v: number): number { 42 | if (v < 0) { 43 | return 0; 44 | } 45 | if (v > Constants.MAX_UINT_8) { 46 | return Constants.MAX_UINT_8; 47 | } 48 | return v | 0; 49 | } 50 | 51 | export function toUint32(v: number): number { 52 | if (v < 0) { 53 | return 0; 54 | } 55 | if (v > Constants.MAX_UINT_32) { 56 | return Constants.MAX_UINT_32; 57 | } 58 | return v | 0; 59 | } 60 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/core/editOperation.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Position } from './position.js'; 7 | import { IRange, Range } from './range.js'; 8 | 9 | /** 10 | * A single edit operation, that acts as a simple replace. 11 | * i.e. Replace text at `range` with `text` in model. 12 | */ 13 | export interface ISingleEditOperation { 14 | /** 15 | * The range to replace. This can be empty to emulate a simple insert. 16 | */ 17 | range: IRange; 18 | /** 19 | * The text to replace with. This can be null to emulate a simple delete. 20 | */ 21 | text: string | null; 22 | /** 23 | * This indicates that this operation has "insert" semantics. 24 | * i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved. 25 | */ 26 | forceMoveMarkers?: boolean; 27 | } 28 | 29 | export class EditOperation { 30 | 31 | public static insert(position: Position, text: string): ISingleEditOperation { 32 | return { 33 | range: new Range(position.lineNumber, position.column, position.lineNumber, position.column), 34 | text: text, 35 | forceMoveMarkers: true 36 | }; 37 | } 38 | 39 | public static delete(range: Range): ISingleEditOperation { 40 | return { 41 | range: range, 42 | text: null 43 | }; 44 | } 45 | 46 | public static replace(range: Range, text: string | null): ISingleEditOperation { 47 | return { 48 | range: range, 49 | text: text 50 | }; 51 | } 52 | 53 | public static replaceMove(range: Range, text: string | null): ISingleEditOperation { 54 | return { 55 | range: range, 56 | text: text, 57 | forceMoveMarkers: true 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/core/offsetEdit.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { BugIndicatingError } from '../../../base/common/errors.js'; 7 | import { OffsetRange } from './offsetRange.js'; 8 | 9 | /** 10 | * Describes an edit to a (0-based) string. 11 | * Use `TextEdit` to describe edits for a 1-based line/column text. 12 | */ 13 | export class OffsetEdit { 14 | public static readonly empty = new OffsetEdit([]); 15 | 16 | public static fromJson(data: IOffsetEdit): OffsetEdit { 17 | return new OffsetEdit(data.map(SingleOffsetEdit.fromJson)); 18 | } 19 | 20 | public static replace( 21 | range: OffsetRange, 22 | newText: string, 23 | ): OffsetEdit { 24 | return new OffsetEdit([new SingleOffsetEdit(range, newText)]); 25 | } 26 | 27 | public static insert( 28 | offset: number, 29 | insertText: string, 30 | ): OffsetEdit { 31 | return OffsetEdit.replace(OffsetRange.emptyAt(offset), insertText); 32 | } 33 | 34 | constructor( 35 | public readonly edits: readonly SingleOffsetEdit[], 36 | ) { 37 | let lastEndEx = -1; 38 | for (const edit of edits) { 39 | if (!(edit.replaceRange.start >= lastEndEx)) { 40 | throw new BugIndicatingError(`Edits must be disjoint and sorted. Found ${edit} after ${lastEndEx}`); 41 | } 42 | lastEndEx = edit.replaceRange.endExclusive; 43 | } 44 | } 45 | 46 | normalize(): OffsetEdit { 47 | const edits: SingleOffsetEdit[] = []; 48 | let lastEdit: SingleOffsetEdit | undefined; 49 | for (const edit of this.edits) { 50 | if (edit.newText.length === 0 && edit.replaceRange.length === 0) { 51 | continue; 52 | } 53 | if (lastEdit && lastEdit.replaceRange.endExclusive === edit.replaceRange.start) { 54 | lastEdit = new SingleOffsetEdit( 55 | lastEdit.replaceRange.join(edit.replaceRange), 56 | lastEdit.newText + edit.newText, 57 | ); 58 | } else { 59 | if (lastEdit) { 60 | edits.push(lastEdit); 61 | } 62 | lastEdit = edit; 63 | } 64 | } 65 | if (lastEdit) { 66 | edits.push(lastEdit); 67 | } 68 | return new OffsetEdit(edits); 69 | } 70 | 71 | toString() { 72 | const edits = this.edits.map(e => e.toString()).join(', '); 73 | return `[${edits}]`; 74 | } 75 | 76 | apply(str: string): string { 77 | const resultText: string[] = []; 78 | let pos = 0; 79 | for (const edit of this.edits) { 80 | resultText.push(str.substring(pos, edit.replaceRange.start)); 81 | resultText.push(edit.newText); 82 | pos = edit.replaceRange.endExclusive; 83 | } 84 | resultText.push(str.substring(pos)); 85 | return resultText.join(''); 86 | } 87 | 88 | compose(other: OffsetEdit): OffsetEdit { 89 | return joinEdits(this, other); 90 | } 91 | 92 | /** 93 | * Creates an edit that reverts this edit. 94 | */ 95 | inverse(originalStr: string): OffsetEdit { 96 | const edits: SingleOffsetEdit[] = []; 97 | let offset = 0; 98 | for (const e of this.edits) { 99 | edits.push(new SingleOffsetEdit( 100 | OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length), 101 | originalStr.substring(e.replaceRange.start, e.replaceRange.endExclusive), 102 | )); 103 | offset += e.newText.length - e.replaceRange.length; 104 | } 105 | return new OffsetEdit(edits); 106 | } 107 | 108 | getNewTextRanges(): OffsetRange[] { 109 | const ranges: OffsetRange[] = []; 110 | let offset = 0; 111 | for (const e of this.edits) { 112 | ranges.push(OffsetRange.ofStartAndLength(e.replaceRange.start + offset, e.newText.length),); 113 | offset += e.newText.length - e.replaceRange.length; 114 | } 115 | return ranges; 116 | } 117 | 118 | get isEmpty(): boolean { 119 | return this.edits.length === 0; 120 | } 121 | 122 | /** 123 | * Consider `t1 := text o base` and `t2 := text o this`. 124 | * We are interested in `tm := tryMerge(t1, t2, base: text)`. 125 | * For that, we compute `tm' := t1 o base o this.rebase(base)` 126 | * such that `tm' === tm`. 127 | */ 128 | tryRebase(base: OffsetEdit): OffsetEdit; 129 | tryRebase(base: OffsetEdit, noOverlap: true): OffsetEdit | undefined; 130 | tryRebase(base: OffsetEdit, noOverlap?: true): OffsetEdit | undefined { 131 | const newEdits: SingleOffsetEdit[] = []; 132 | 133 | let baseIdx = 0; 134 | let ourIdx = 0; 135 | let offset = 0; 136 | 137 | while (ourIdx < this.edits.length || baseIdx < base.edits.length) { 138 | // take the edit that starts first 139 | const baseEdit = base.edits[baseIdx]; 140 | const ourEdit = this.edits[ourIdx]; 141 | 142 | if (!ourEdit) { 143 | // We processed all our edits 144 | break; 145 | } else if (!baseEdit) { 146 | // no more edits from base 147 | newEdits.push(new SingleOffsetEdit( 148 | ourEdit.replaceRange.delta(offset), 149 | ourEdit.newText, 150 | )); 151 | ourIdx++; 152 | } else if (ourEdit.replaceRange.intersectsOrTouches(baseEdit.replaceRange)) { 153 | ourIdx++; // Don't take our edit, as it is conflicting -> skip 154 | if (noOverlap) { 155 | return undefined; 156 | } 157 | } else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start) { 158 | // Our edit starts first 159 | newEdits.push(new SingleOffsetEdit( 160 | ourEdit.replaceRange.delta(offset), 161 | ourEdit.newText, 162 | )); 163 | ourIdx++; 164 | } else { 165 | baseIdx++; 166 | offset += baseEdit.newText.length - baseEdit.replaceRange.length; 167 | } 168 | } 169 | 170 | return new OffsetEdit(newEdits); 171 | } 172 | 173 | applyToOffset(originalOffset: number): number { 174 | let accumulatedDelta = 0; 175 | for (const edit of this.edits) { 176 | if (edit.replaceRange.start <= originalOffset) { 177 | if (originalOffset < edit.replaceRange.endExclusive) { 178 | // the offset is in the replaced range 179 | return edit.replaceRange.start + accumulatedDelta; 180 | } 181 | accumulatedDelta += edit.newText.length - edit.replaceRange.length; 182 | } else { 183 | break; 184 | } 185 | } 186 | return originalOffset + accumulatedDelta; 187 | } 188 | 189 | applyToOffsetRange(originalRange: OffsetRange): OffsetRange { 190 | return new OffsetRange( 191 | this.applyToOffset(originalRange.start), 192 | this.applyToOffset(originalRange.endExclusive) 193 | ); 194 | } 195 | 196 | applyInverseToOffset(postEditsOffset: number): number { 197 | let accumulatedDelta = 0; 198 | for (const edit of this.edits) { 199 | const editLength = edit.newText.length; 200 | if (edit.replaceRange.start <= postEditsOffset - accumulatedDelta) { 201 | if (postEditsOffset - accumulatedDelta < edit.replaceRange.start + editLength) { 202 | // the offset is in the replaced range 203 | return edit.replaceRange.start; 204 | } 205 | accumulatedDelta += editLength - edit.replaceRange.length; 206 | } else { 207 | break; 208 | } 209 | } 210 | return postEditsOffset - accumulatedDelta; 211 | } 212 | 213 | equals(other: OffsetEdit): boolean { 214 | if (this.edits.length !== other.edits.length) { 215 | return false; 216 | } 217 | for (let i = 0; i < this.edits.length; i++) { 218 | if (!this.edits[i].equals(other.edits[i])) { 219 | return false; 220 | } 221 | 222 | } 223 | return true; 224 | } 225 | } 226 | 227 | export type IOffsetEdit = ISingleOffsetEdit[]; 228 | 229 | export interface ISingleOffsetEdit { 230 | txt: string; 231 | pos: number; 232 | len: number; 233 | } 234 | 235 | export class SingleOffsetEdit { 236 | public static fromJson(data: ISingleOffsetEdit): SingleOffsetEdit { 237 | return new SingleOffsetEdit(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt); 238 | } 239 | 240 | public static insert(offset: number, text: string): SingleOffsetEdit { 241 | return new SingleOffsetEdit(OffsetRange.emptyAt(offset), text); 242 | } 243 | 244 | public static replace(range: OffsetRange, text: string): SingleOffsetEdit { 245 | return new SingleOffsetEdit(range, text); 246 | } 247 | 248 | constructor( 249 | public readonly replaceRange: OffsetRange, 250 | public readonly newText: string, 251 | ) { } 252 | 253 | toString(): string { 254 | return `${this.replaceRange} -> "${this.newText}"`; 255 | } 256 | 257 | get isEmpty() { 258 | return this.newText.length === 0 && this.replaceRange.length === 0; 259 | } 260 | 261 | apply(str: string): string { 262 | return str.substring(0, this.replaceRange.start) + this.newText + str.substring(this.replaceRange.endExclusive); 263 | } 264 | 265 | getRangeAfterApply(): OffsetRange { 266 | return new OffsetRange(this.replaceRange.start, this.replaceRange.start + this.newText.length); 267 | } 268 | 269 | equals(other: SingleOffsetEdit): boolean { 270 | return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText; 271 | } 272 | } 273 | 274 | /** 275 | * Invariant: 276 | * ``` 277 | * edits2.apply(edits1.apply(str)) = join(edits1, edits2).apply(str) 278 | * ``` 279 | */ 280 | function joinEdits(edits1: OffsetEdit, edits2: OffsetEdit): OffsetEdit { 281 | edits1 = edits1.normalize(); 282 | edits2 = edits2.normalize(); 283 | 284 | if (edits1.isEmpty) { return edits2; } 285 | if (edits2.isEmpty) { return edits1; } 286 | 287 | const edit1Queue = [...edits1.edits]; 288 | const result: SingleOffsetEdit[] = []; 289 | 290 | let edit1ToEdit2 = 0; 291 | 292 | for (const edit2 of edits2.edits) { 293 | // Copy over edit1 unmodified until it touches edit2. 294 | while (true) { 295 | const edit1 = edit1Queue[0]!; 296 | if (!edit1 || edit1.replaceRange.start + edit1ToEdit2 + edit1.newText.length >= edit2.replaceRange.start) { 297 | break; 298 | } 299 | edit1Queue.shift(); 300 | 301 | result.push(edit1); 302 | edit1ToEdit2 += edit1.newText.length - edit1.replaceRange.length; 303 | } 304 | 305 | const firstEdit1ToEdit2 = edit1ToEdit2; 306 | let firstIntersecting: SingleOffsetEdit | undefined; // or touching 307 | let lastIntersecting: SingleOffsetEdit | undefined; // or touching 308 | 309 | while (true) { 310 | const edit1 = edit1Queue[0]; 311 | if (!edit1 || edit1.replaceRange.start + edit1ToEdit2 > edit2.replaceRange.endExclusive) { 312 | break; 313 | } 314 | // else we intersect, because the new end of edit1 is after or equal to our start 315 | 316 | if (!firstIntersecting) { 317 | firstIntersecting = edit1; 318 | } 319 | lastIntersecting = edit1; 320 | edit1Queue.shift(); 321 | 322 | edit1ToEdit2 += edit1.newText.length - edit1.replaceRange.length; 323 | } 324 | 325 | if (!firstIntersecting) { 326 | result.push(new SingleOffsetEdit(edit2.replaceRange.delta(-edit1ToEdit2), edit2.newText)); 327 | } else { 328 | let prefix = ''; 329 | const prefixLength = edit2.replaceRange.start - (firstIntersecting.replaceRange.start + firstEdit1ToEdit2); 330 | if (prefixLength > 0) { 331 | prefix = firstIntersecting.newText.slice(0, prefixLength); 332 | } 333 | const suffixLength = (lastIntersecting!.replaceRange.endExclusive + edit1ToEdit2) - edit2.replaceRange.endExclusive; 334 | if (suffixLength > 0) { 335 | const e = new SingleOffsetEdit(OffsetRange.ofStartAndLength(lastIntersecting!.replaceRange.endExclusive, 0), lastIntersecting!.newText.slice(-suffixLength)); 336 | edit1Queue.unshift(e); 337 | edit1ToEdit2 -= e.newText.length - e.replaceRange.length; 338 | } 339 | const newText = prefix + edit2.newText; 340 | 341 | const newReplaceRange = new OffsetRange( 342 | Math.min(firstIntersecting.replaceRange.start, edit2.replaceRange.start - firstEdit1ToEdit2), 343 | edit2.replaceRange.endExclusive - edit1ToEdit2 344 | ); 345 | result.push(new SingleOffsetEdit(newReplaceRange, newText)); 346 | } 347 | } 348 | 349 | while (true) { 350 | const item = edit1Queue.shift(); 351 | if (!item) { break; } 352 | result.push(item); 353 | } 354 | 355 | return new OffsetEdit(result).normalize(); 356 | } 357 | 358 | export function applyEditsToRanges(sortedRanges: OffsetRange[], edits: OffsetEdit): OffsetRange[] { 359 | sortedRanges = sortedRanges.slice(); 360 | 361 | // treat edits as deletion of the replace range and then as insertion that extends the first range 362 | const result: OffsetRange[] = []; 363 | 364 | let offset = 0; 365 | 366 | for (const e of edits.edits) { 367 | while (true) { 368 | // ranges before the current edit 369 | const r = sortedRanges[0]; 370 | if (!r || r.endExclusive >= e.replaceRange.start) { 371 | break; 372 | } 373 | sortedRanges.shift(); 374 | result.push(r.delta(offset)); 375 | } 376 | 377 | const intersecting: OffsetRange[] = []; 378 | while (true) { 379 | const r = sortedRanges[0]; 380 | if (!r || !r.intersectsOrTouches(e.replaceRange)) { 381 | break; 382 | } 383 | sortedRanges.shift(); 384 | intersecting.push(r); 385 | } 386 | 387 | for (let i = intersecting.length - 1; i >= 0; i--) { 388 | let r = intersecting[i]; 389 | 390 | const overlap = r.intersect(e.replaceRange)!.length; 391 | r = r.deltaEnd(-overlap + (i === 0 ? e.newText.length : 0)); 392 | 393 | const rangeAheadOfReplaceRange = r.start - e.replaceRange.start; 394 | if (rangeAheadOfReplaceRange > 0) { 395 | r = r.delta(-rangeAheadOfReplaceRange); 396 | } 397 | 398 | if (i !== 0) { 399 | r = r.delta(e.newText.length); 400 | } 401 | 402 | // We already took our offset into account. 403 | // Because we add r back to the queue (which then adds offset again), 404 | // we have to remove it here. 405 | r = r.delta(-(e.newText.length - e.replaceRange.length)); 406 | 407 | sortedRanges.unshift(r); 408 | } 409 | 410 | offset += e.newText.length - e.replaceRange.length; 411 | } 412 | 413 | while (true) { 414 | const r = sortedRanges[0]; 415 | if (!r) { 416 | break; 417 | } 418 | sortedRanges.shift(); 419 | result.push(r.delta(offset)); 420 | } 421 | 422 | return result; 423 | } 424 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/core/offsetRange.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { BugIndicatingError } from '../../../base/common/errors.js'; 7 | 8 | export interface IOffsetRange { 9 | readonly start: number; 10 | readonly endExclusive: number; 11 | } 12 | 13 | /** 14 | * A range of offsets (0-based). 15 | */ 16 | export class OffsetRange implements IOffsetRange { 17 | public static fromTo(start: number, endExclusive: number): OffsetRange { 18 | return new OffsetRange(start, endExclusive); 19 | } 20 | 21 | public static addRange(range: OffsetRange, sortedRanges: OffsetRange[]): void { 22 | let i = 0; 23 | while (i < sortedRanges.length && sortedRanges[i].endExclusive < range.start) { 24 | i++; 25 | } 26 | let j = i; 27 | while (j < sortedRanges.length && sortedRanges[j].start <= range.endExclusive) { 28 | j++; 29 | } 30 | if (i === j) { 31 | sortedRanges.splice(i, 0, range); 32 | } else { 33 | const start = Math.min(range.start, sortedRanges[i].start); 34 | const end = Math.max(range.endExclusive, sortedRanges[j - 1].endExclusive); 35 | sortedRanges.splice(i, j - i, new OffsetRange(start, end)); 36 | } 37 | } 38 | 39 | public static tryCreate(start: number, endExclusive: number): OffsetRange | undefined { 40 | if (start > endExclusive) { 41 | return undefined; 42 | } 43 | return new OffsetRange(start, endExclusive); 44 | } 45 | 46 | public static ofLength(length: number): OffsetRange { 47 | return new OffsetRange(0, length); 48 | } 49 | 50 | public static ofStartAndLength(start: number, length: number): OffsetRange { 51 | return new OffsetRange(start, start + length); 52 | } 53 | 54 | public static emptyAt(offset: number): OffsetRange { 55 | return new OffsetRange(offset, offset); 56 | } 57 | 58 | constructor(public readonly start: number, public readonly endExclusive: number) { 59 | if (start > endExclusive) { 60 | throw new BugIndicatingError(`Invalid range: ${this.toString()}`); 61 | } 62 | } 63 | 64 | get isEmpty(): boolean { 65 | return this.start === this.endExclusive; 66 | } 67 | 68 | public delta(offset: number): OffsetRange { 69 | return new OffsetRange(this.start + offset, this.endExclusive + offset); 70 | } 71 | 72 | public deltaStart(offset: number): OffsetRange { 73 | return new OffsetRange(this.start + offset, this.endExclusive); 74 | } 75 | 76 | public deltaEnd(offset: number): OffsetRange { 77 | return new OffsetRange(this.start, this.endExclusive + offset); 78 | } 79 | 80 | public get length(): number { 81 | return this.endExclusive - this.start; 82 | } 83 | 84 | public toString() { 85 | return `[${this.start}, ${this.endExclusive})`; 86 | } 87 | 88 | public equals(other: OffsetRange): boolean { 89 | return this.start === other.start && this.endExclusive === other.endExclusive; 90 | } 91 | 92 | public containsRange(other: OffsetRange): boolean { 93 | return this.start <= other.start && other.endExclusive <= this.endExclusive; 94 | } 95 | 96 | public contains(offset: number): boolean { 97 | return this.start <= offset && offset < this.endExclusive; 98 | } 99 | 100 | /** 101 | * for all numbers n: range1.contains(n) or range2.contains(n) => range1.join(range2).contains(n) 102 | * The joined range is the smallest range that contains both ranges. 103 | */ 104 | public join(other: OffsetRange): OffsetRange { 105 | return new OffsetRange(Math.min(this.start, other.start), Math.max(this.endExclusive, other.endExclusive)); 106 | } 107 | 108 | /** 109 | * for all numbers n: range1.contains(n) and range2.contains(n) <=> range1.intersect(range2).contains(n) 110 | * 111 | * The resulting range is empty if the ranges do not intersect, but touch. 112 | * If the ranges don't even touch, the result is undefined. 113 | */ 114 | public intersect(other: OffsetRange): OffsetRange | undefined { 115 | const start = Math.max(this.start, other.start); 116 | const end = Math.min(this.endExclusive, other.endExclusive); 117 | if (start <= end) { 118 | return new OffsetRange(start, end); 119 | } 120 | return undefined; 121 | } 122 | 123 | public intersectionLength(range: OffsetRange): number { 124 | const start = Math.max(this.start, range.start); 125 | const end = Math.min(this.endExclusive, range.endExclusive); 126 | return Math.max(0, end - start); 127 | } 128 | 129 | public intersects(other: OffsetRange): boolean { 130 | const start = Math.max(this.start, other.start); 131 | const end = Math.min(this.endExclusive, other.endExclusive); 132 | return start < end; 133 | } 134 | 135 | public intersectsOrTouches(other: OffsetRange): boolean { 136 | const start = Math.max(this.start, other.start); 137 | const end = Math.min(this.endExclusive, other.endExclusive); 138 | return start <= end; 139 | } 140 | 141 | public isBefore(other: OffsetRange): boolean { 142 | return this.endExclusive <= other.start; 143 | } 144 | 145 | public isAfter(other: OffsetRange): boolean { 146 | return this.start >= other.endExclusive; 147 | } 148 | 149 | public slice(arr: T[]): T[] { 150 | return arr.slice(this.start, this.endExclusive); 151 | } 152 | 153 | public substring(str: string): string { 154 | return str.substring(this.start, this.endExclusive); 155 | } 156 | 157 | /** 158 | * Returns the given value if it is contained in this instance, otherwise the closest value that is contained. 159 | * The range must not be empty. 160 | */ 161 | public clip(value: number): number { 162 | if (this.isEmpty) { 163 | throw new BugIndicatingError(`Invalid clipping range: ${this.toString()}`); 164 | } 165 | return Math.max(this.start, Math.min(this.endExclusive - 1, value)); 166 | } 167 | 168 | /** 169 | * Returns `r := value + k * length` such that `r` is contained in this range. 170 | * The range must not be empty. 171 | * 172 | * E.g. `[5, 10).clipCyclic(10) === 5`, `[5, 10).clipCyclic(11) === 6` and `[5, 10).clipCyclic(4) === 9`. 173 | */ 174 | public clipCyclic(value: number): number { 175 | if (this.isEmpty) { 176 | throw new BugIndicatingError(`Invalid clipping range: ${this.toString()}`); 177 | } 178 | if (value < this.start) { 179 | return this.endExclusive - ((this.start - value) % this.length); 180 | } 181 | if (value >= this.endExclusive) { 182 | return this.start + ((value - this.start) % this.length); 183 | } 184 | return value; 185 | } 186 | 187 | public map(f: (offset: number) => T): T[] { 188 | const result: T[] = []; 189 | for (let i = this.start; i < this.endExclusive; i++) { 190 | result.push(f(i)); 191 | } 192 | return result; 193 | } 194 | 195 | public forEach(f: (offset: number) => void): void { 196 | for (let i = this.start; i < this.endExclusive; i++) { 197 | f(i); 198 | } 199 | } 200 | } 201 | 202 | export class OffsetRangeSet { 203 | private readonly _sortedRanges: OffsetRange[] = []; 204 | 205 | public addRange(range: OffsetRange): void { 206 | let i = 0; 207 | while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive < range.start) { 208 | i++; 209 | } 210 | let j = i; 211 | while (j < this._sortedRanges.length && this._sortedRanges[j].start <= range.endExclusive) { 212 | j++; 213 | } 214 | if (i === j) { 215 | this._sortedRanges.splice(i, 0, range); 216 | } else { 217 | const start = Math.min(range.start, this._sortedRanges[i].start); 218 | const end = Math.max(range.endExclusive, this._sortedRanges[j - 1].endExclusive); 219 | this._sortedRanges.splice(i, j - i, new OffsetRange(start, end)); 220 | } 221 | } 222 | 223 | public toString(): string { 224 | return this._sortedRanges.map(r => r.toString()).join(', '); 225 | } 226 | 227 | /** 228 | * Returns of there is a value that is contained in this instance and the given range. 229 | */ 230 | public intersectsStrict(other: OffsetRange): boolean { 231 | // TODO use binary search 232 | let i = 0; 233 | while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive <= other.start) { 234 | i++; 235 | } 236 | return i < this._sortedRanges.length && this._sortedRanges[i].start < other.endExclusive; 237 | } 238 | 239 | public intersectWithRange(other: OffsetRange): OffsetRangeSet { 240 | // TODO use binary search + slice 241 | const result = new OffsetRangeSet(); 242 | for (const range of this._sortedRanges) { 243 | const intersection = range.intersect(other); 244 | if (intersection) { 245 | result.addRange(intersection); 246 | } 247 | } 248 | return result; 249 | } 250 | 251 | public intersectWithRangeLength(other: OffsetRange): number { 252 | return this.intersectWithRange(other).length; 253 | } 254 | 255 | public get length(): number { 256 | return this._sortedRanges.reduce((prev, cur) => prev + cur.length, 0); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/core/position.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * A position in the editor. This interface is suitable for serialization. 8 | */ 9 | export interface IPosition { 10 | /** 11 | * line number (starts at 1) 12 | */ 13 | readonly lineNumber: number; 14 | /** 15 | * column (the first character in a line is between column 1 and column 2) 16 | */ 17 | readonly column: number; 18 | } 19 | 20 | /** 21 | * A position in the editor. 22 | */ 23 | export class Position { 24 | /** 25 | * line number (starts at 1) 26 | */ 27 | public readonly lineNumber: number; 28 | /** 29 | * column (the first character in a line is between column 1 and column 2) 30 | */ 31 | public readonly column: number; 32 | 33 | constructor(lineNumber: number, column: number) { 34 | this.lineNumber = lineNumber; 35 | this.column = column; 36 | } 37 | 38 | /** 39 | * Create a new position from this position. 40 | * 41 | * @param newLineNumber new line number 42 | * @param newColumn new column 43 | */ 44 | with(newLineNumber: number = this.lineNumber, newColumn: number = this.column): Position { 45 | if (newLineNumber === this.lineNumber && newColumn === this.column) { 46 | return this; 47 | } else { 48 | return new Position(newLineNumber, newColumn); 49 | } 50 | } 51 | 52 | /** 53 | * Derive a new position from this position. 54 | * 55 | * @param deltaLineNumber line number delta 56 | * @param deltaColumn column delta 57 | */ 58 | delta(deltaLineNumber: number = 0, deltaColumn: number = 0): Position { 59 | return this.with(Math.max(1, this.lineNumber + deltaLineNumber), Math.max(1, this.column + deltaColumn)); 60 | } 61 | 62 | /** 63 | * Test if this position equals other position 64 | */ 65 | public equals(other: IPosition): boolean { 66 | return Position.equals(this, other); 67 | } 68 | 69 | /** 70 | * Test if position `a` equals position `b` 71 | */ 72 | public static equals(a: IPosition | null, b: IPosition | null): boolean { 73 | if (!a && !b) { 74 | return true; 75 | } 76 | return ( 77 | !!a && 78 | !!b && 79 | a.lineNumber === b.lineNumber && 80 | a.column === b.column 81 | ); 82 | } 83 | 84 | /** 85 | * Test if this position is before other position. 86 | * If the two positions are equal, the result will be false. 87 | */ 88 | public isBefore(other: IPosition): boolean { 89 | return Position.isBefore(this, other); 90 | } 91 | 92 | /** 93 | * Test if position `a` is before position `b`. 94 | * If the two positions are equal, the result will be false. 95 | */ 96 | public static isBefore(a: IPosition, b: IPosition): boolean { 97 | if (a.lineNumber < b.lineNumber) { 98 | return true; 99 | } 100 | if (b.lineNumber < a.lineNumber) { 101 | return false; 102 | } 103 | return a.column < b.column; 104 | } 105 | 106 | /** 107 | * Test if this position is before other position. 108 | * If the two positions are equal, the result will be true. 109 | */ 110 | public isBeforeOrEqual(other: IPosition): boolean { 111 | return Position.isBeforeOrEqual(this, other); 112 | } 113 | 114 | /** 115 | * Test if position `a` is before position `b`. 116 | * If the two positions are equal, the result will be true. 117 | */ 118 | public static isBeforeOrEqual(a: IPosition, b: IPosition): boolean { 119 | if (a.lineNumber < b.lineNumber) { 120 | return true; 121 | } 122 | if (b.lineNumber < a.lineNumber) { 123 | return false; 124 | } 125 | return a.column <= b.column; 126 | } 127 | 128 | /** 129 | * A function that compares positions, useful for sorting 130 | */ 131 | public static compare(a: IPosition, b: IPosition): number { 132 | const aLineNumber = a.lineNumber | 0; 133 | const bLineNumber = b.lineNumber | 0; 134 | 135 | if (aLineNumber === bLineNumber) { 136 | const aColumn = a.column | 0; 137 | const bColumn = b.column | 0; 138 | return aColumn - bColumn; 139 | } 140 | 141 | return aLineNumber - bLineNumber; 142 | } 143 | 144 | /** 145 | * Clone this position. 146 | */ 147 | public clone(): Position { 148 | return new Position(this.lineNumber, this.column); 149 | } 150 | 151 | /** 152 | * Convert to a human-readable representation. 153 | */ 154 | public toString(): string { 155 | return '(' + this.lineNumber + ',' + this.column + ')'; 156 | } 157 | 158 | // --- 159 | 160 | /** 161 | * Create a `Position` from an `IPosition`. 162 | */ 163 | public static lift(pos: IPosition): Position { 164 | return new Position(pos.lineNumber, pos.column); 165 | } 166 | 167 | /** 168 | * Test if `obj` is an `IPosition`. 169 | */ 170 | public static isIPosition(obj: any): obj is IPosition { 171 | return ( 172 | obj 173 | && (typeof obj.lineNumber === 'number') 174 | && (typeof obj.column === 'number') 175 | ); 176 | } 177 | 178 | public toJSON(): IPosition { 179 | return { 180 | lineNumber: this.lineNumber, 181 | column: this.column 182 | }; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/core/positionToOffset.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { findLastIdxMonotonous } from '../../../base/common/arraysFind.js'; 7 | import { OffsetRange } from './offsetRange.js'; 8 | import { Position } from './position.js'; 9 | import { Range } from './range.js'; 10 | import { TextLength } from './textLength.js'; 11 | 12 | export class PositionOffsetTransformer { 13 | private readonly lineStartOffsetByLineIdx: number[]; 14 | private readonly lineEndOffsetByLineIdx: number[]; 15 | 16 | constructor(public readonly text: string) { 17 | this.lineStartOffsetByLineIdx = []; 18 | this.lineEndOffsetByLineIdx = []; 19 | 20 | this.lineStartOffsetByLineIdx.push(0); 21 | for (let i = 0; i < text.length; i++) { 22 | if (text.charAt(i) === '\n') { 23 | this.lineStartOffsetByLineIdx.push(i + 1); 24 | if (i > 0 && text.charAt(i - 1) === '\r') { 25 | this.lineEndOffsetByLineIdx.push(i - 1); 26 | } else { 27 | this.lineEndOffsetByLineIdx.push(i); 28 | } 29 | } 30 | } 31 | this.lineEndOffsetByLineIdx.push(text.length); 32 | } 33 | 34 | getOffset(position: Position): number { 35 | const valPos = this._validatePosition(position); 36 | return this.lineStartOffsetByLineIdx[valPos.lineNumber - 1] + valPos.column - 1; 37 | } 38 | 39 | private _validatePosition(position: Position): Position { 40 | if (position.lineNumber < 1) { 41 | return new Position(1, 1); 42 | } 43 | const lineCount = this.textLength.lineCount + 1; 44 | if (position.lineNumber > lineCount) { 45 | const lineLength = this.getLineLength(lineCount); 46 | return new Position(lineCount, lineLength + 1); 47 | } 48 | if (position.column < 1) { 49 | return new Position(position.lineNumber, 1); 50 | } 51 | const lineLength = this.getLineLength(position.lineNumber); 52 | if (position.column - 1 > lineLength) { 53 | return new Position(position.lineNumber, lineLength + 1); 54 | } 55 | return position; 56 | } 57 | 58 | getOffsetRange(range: Range): OffsetRange { 59 | return new OffsetRange( 60 | this.getOffset(range.getStartPosition()), 61 | this.getOffset(range.getEndPosition()) 62 | ); 63 | } 64 | 65 | getPosition(offset: number): Position { 66 | const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset); 67 | const lineNumber = idx + 1; 68 | const column = offset - this.lineStartOffsetByLineIdx[idx] + 1; 69 | return new Position(lineNumber, column); 70 | } 71 | 72 | getRange(offsetRange: OffsetRange): Range { 73 | return Range.fromPositions( 74 | this.getPosition(offsetRange.start), 75 | this.getPosition(offsetRange.endExclusive) 76 | ); 77 | } 78 | 79 | getTextLength(offsetRange: OffsetRange): TextLength { 80 | return TextLength.ofRange(this.getRange(offsetRange)); 81 | } 82 | 83 | get textLength(): TextLength { 84 | const lineIdx = this.lineStartOffsetByLineIdx.length - 1; 85 | return new TextLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]); 86 | } 87 | 88 | getLineLength(lineNumber: number): number { 89 | return this.lineEndOffsetByLineIdx[lineNumber - 1] - this.lineStartOffsetByLineIdx[lineNumber - 1]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/core/textLength.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { LineRange } from './lineRange.js'; 6 | import { Position } from './position.js'; 7 | import { Range } from './range.js'; 8 | 9 | /** 10 | * Represents a non-negative length of text in terms of line and column count. 11 | */ 12 | export class TextLength { 13 | public static zero = new TextLength(0, 0); 14 | 15 | public static lengthDiffNonNegative(start: TextLength, end: TextLength): TextLength { 16 | if (end.isLessThan(start)) { 17 | return TextLength.zero; 18 | } 19 | if (start.lineCount === end.lineCount) { 20 | return new TextLength(0, end.columnCount - start.columnCount); 21 | } else { 22 | return new TextLength(end.lineCount - start.lineCount, end.columnCount); 23 | } 24 | } 25 | 26 | public static betweenPositions(position1: Position, position2: Position): TextLength { 27 | if (position1.lineNumber === position2.lineNumber) { 28 | return new TextLength(0, position2.column - position1.column); 29 | } else { 30 | return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); 31 | } 32 | } 33 | 34 | public static fromPosition(pos: Position): TextLength { 35 | return new TextLength(pos.lineNumber - 1, pos.column - 1); 36 | } 37 | 38 | public static ofRange(range: Range) { 39 | return TextLength.betweenPositions(range.getStartPosition(), range.getEndPosition()); 40 | } 41 | 42 | public static ofText(text: string): TextLength { 43 | let line = 0; 44 | let column = 0; 45 | for (const c of text) { 46 | if (c === '\n') { 47 | line++; 48 | column = 0; 49 | } else { 50 | column++; 51 | } 52 | } 53 | return new TextLength(line, column); 54 | } 55 | 56 | constructor( 57 | public readonly lineCount: number, 58 | public readonly columnCount: number 59 | ) { } 60 | 61 | public isZero() { 62 | return this.lineCount === 0 && this.columnCount === 0; 63 | } 64 | 65 | public isLessThan(other: TextLength): boolean { 66 | if (this.lineCount !== other.lineCount) { 67 | return this.lineCount < other.lineCount; 68 | } 69 | return this.columnCount < other.columnCount; 70 | } 71 | 72 | public isGreaterThan(other: TextLength): boolean { 73 | if (this.lineCount !== other.lineCount) { 74 | return this.lineCount > other.lineCount; 75 | } 76 | return this.columnCount > other.columnCount; 77 | } 78 | 79 | public isGreaterThanOrEqualTo(other: TextLength): boolean { 80 | if (this.lineCount !== other.lineCount) { 81 | return this.lineCount > other.lineCount; 82 | } 83 | return this.columnCount >= other.columnCount; 84 | } 85 | 86 | public equals(other: TextLength): boolean { 87 | return this.lineCount === other.lineCount && this.columnCount === other.columnCount; 88 | } 89 | 90 | public compare(other: TextLength): number { 91 | if (this.lineCount !== other.lineCount) { 92 | return this.lineCount - other.lineCount; 93 | } 94 | return this.columnCount - other.columnCount; 95 | } 96 | 97 | public add(other: TextLength): TextLength { 98 | if (other.lineCount === 0) { 99 | return new TextLength(this.lineCount, this.columnCount + other.columnCount); 100 | } else { 101 | return new TextLength(this.lineCount + other.lineCount, other.columnCount); 102 | } 103 | } 104 | 105 | public createRange(startPosition: Position): Range { 106 | if (this.lineCount === 0) { 107 | return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column + this.columnCount); 108 | } else { 109 | return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber + this.lineCount, this.columnCount + 1); 110 | } 111 | } 112 | 113 | public toRange(): Range { 114 | return new Range(1, 1, this.lineCount + 1, this.columnCount + 1); 115 | } 116 | 117 | public toLineRange(): LineRange { 118 | return LineRange.ofLength(1, this.lineCount + 1); 119 | } 120 | 121 | public addToPosition(position: Position): Position { 122 | if (this.lineCount === 0) { 123 | return new Position(position.lineNumber, position.column + this.columnCount); 124 | } else { 125 | return new Position(position.lineNumber + this.lineCount, this.columnCount + 1); 126 | } 127 | } 128 | 129 | public addToRange(range: Range): Range { 130 | return Range.fromPositions( 131 | this.addToPosition(range.getStartPosition()), 132 | this.addToPosition(range.getEndPosition()) 133 | ); 134 | } 135 | 136 | toString() { 137 | return `${this.lineCount},${this.columnCount}`; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { forEachAdjacent } from '../../../../../base/common/arrays.js'; 7 | import { BugIndicatingError } from '../../../../../base/common/errors.js'; 8 | import { OffsetRange } from '../../../core/offsetRange.js'; 9 | 10 | /** 11 | * Represents a synchronous diff algorithm. Should be executed in a worker. 12 | */ 13 | export interface IDiffAlgorithm { 14 | compute(sequence1: ISequence, sequence2: ISequence, timeout?: ITimeout): DiffAlgorithmResult; 15 | } 16 | 17 | export class DiffAlgorithmResult { 18 | static trivial(seq1: ISequence, seq2: ISequence): DiffAlgorithmResult { 19 | return new DiffAlgorithmResult([new SequenceDiff(OffsetRange.ofLength(seq1.length), OffsetRange.ofLength(seq2.length))], false); 20 | } 21 | 22 | static trivialTimedOut(seq1: ISequence, seq2: ISequence): DiffAlgorithmResult { 23 | return new DiffAlgorithmResult([new SequenceDiff(OffsetRange.ofLength(seq1.length), OffsetRange.ofLength(seq2.length))], true); 24 | } 25 | 26 | constructor( 27 | public readonly diffs: SequenceDiff[], 28 | /** 29 | * Indicates if the time out was reached. 30 | * In that case, the diffs might be an approximation and the user should be asked to rerun the diff with more time. 31 | */ 32 | public readonly hitTimeout: boolean, 33 | ) { } 34 | } 35 | 36 | export class SequenceDiff { 37 | public static invert(sequenceDiffs: SequenceDiff[], doc1Length: number): SequenceDiff[] { 38 | const result: SequenceDiff[] = []; 39 | forEachAdjacent(sequenceDiffs, (a, b) => { 40 | result.push(SequenceDiff.fromOffsetPairs( 41 | a ? a.getEndExclusives() : OffsetPair.zero, 42 | b ? b.getStarts() : new OffsetPair(doc1Length, (a ? a.seq2Range.endExclusive - a.seq1Range.endExclusive : 0) + doc1Length) 43 | )); 44 | }); 45 | return result; 46 | } 47 | 48 | public static fromOffsetPairs(start: OffsetPair, endExclusive: OffsetPair): SequenceDiff { 49 | return new SequenceDiff( 50 | new OffsetRange(start.offset1, endExclusive.offset1), 51 | new OffsetRange(start.offset2, endExclusive.offset2), 52 | ); 53 | } 54 | 55 | public static assertSorted(sequenceDiffs: SequenceDiff[]): void { 56 | let last: SequenceDiff | undefined = undefined; 57 | for (const cur of sequenceDiffs) { 58 | if (last) { 59 | if (!(last.seq1Range.endExclusive <= cur.seq1Range.start && last.seq2Range.endExclusive <= cur.seq2Range.start)) { 60 | throw new BugIndicatingError('Sequence diffs must be sorted'); 61 | } 62 | } 63 | last = cur; 64 | } 65 | } 66 | 67 | constructor( 68 | public readonly seq1Range: OffsetRange, 69 | public readonly seq2Range: OffsetRange, 70 | ) { } 71 | 72 | public swap(): SequenceDiff { 73 | return new SequenceDiff(this.seq2Range, this.seq1Range); 74 | } 75 | 76 | public toString(): string { 77 | return `${this.seq1Range} <-> ${this.seq2Range}`; 78 | } 79 | 80 | public join(other: SequenceDiff): SequenceDiff { 81 | return new SequenceDiff(this.seq1Range.join(other.seq1Range), this.seq2Range.join(other.seq2Range)); 82 | } 83 | 84 | public delta(offset: number): SequenceDiff { 85 | if (offset === 0) { 86 | return this; 87 | } 88 | return new SequenceDiff(this.seq1Range.delta(offset), this.seq2Range.delta(offset)); 89 | } 90 | 91 | public deltaStart(offset: number): SequenceDiff { 92 | if (offset === 0) { 93 | return this; 94 | } 95 | return new SequenceDiff(this.seq1Range.deltaStart(offset), this.seq2Range.deltaStart(offset)); 96 | } 97 | 98 | public deltaEnd(offset: number): SequenceDiff { 99 | if (offset === 0) { 100 | return this; 101 | } 102 | return new SequenceDiff(this.seq1Range.deltaEnd(offset), this.seq2Range.deltaEnd(offset)); 103 | } 104 | 105 | public intersectsOrTouches(other: SequenceDiff): boolean { 106 | return this.seq1Range.intersectsOrTouches(other.seq1Range) || this.seq2Range.intersectsOrTouches(other.seq2Range); 107 | } 108 | 109 | public intersect(other: SequenceDiff): SequenceDiff | undefined { 110 | const i1 = this.seq1Range.intersect(other.seq1Range); 111 | const i2 = this.seq2Range.intersect(other.seq2Range); 112 | if (!i1 || !i2) { 113 | return undefined; 114 | } 115 | return new SequenceDiff(i1, i2); 116 | } 117 | 118 | public getStarts(): OffsetPair { 119 | return new OffsetPair(this.seq1Range.start, this.seq2Range.start); 120 | } 121 | 122 | public getEndExclusives(): OffsetPair { 123 | return new OffsetPair(this.seq1Range.endExclusive, this.seq2Range.endExclusive); 124 | } 125 | } 126 | 127 | export class OffsetPair { 128 | public static readonly zero = new OffsetPair(0, 0); 129 | public static readonly max = new OffsetPair(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); 130 | 131 | constructor( 132 | public readonly offset1: number, 133 | public readonly offset2: number, 134 | ) { 135 | } 136 | 137 | public toString(): string { 138 | return `${this.offset1} <-> ${this.offset2}`; 139 | } 140 | 141 | public delta(offset: number): OffsetPair { 142 | if (offset === 0) { 143 | return this; 144 | } 145 | return new OffsetPair(this.offset1 + offset, this.offset2 + offset); 146 | } 147 | 148 | public equals(other: OffsetPair): boolean { 149 | return this.offset1 === other.offset1 && this.offset2 === other.offset2; 150 | } 151 | } 152 | 153 | export interface ISequence { 154 | getElement(offset: number): number; 155 | get length(): number; 156 | 157 | /** 158 | * The higher the score, the better that offset can be used to split the sequence. 159 | * Is used to optimize insertions. 160 | * Must not be negative. 161 | */ 162 | getBoundaryScore?(length: number): number; 163 | 164 | /** 165 | * For line sequences, getElement returns a number representing trimmed lines. 166 | * This however checks equality for the original lines. 167 | * It prevents shifting to less matching lines. 168 | */ 169 | isStronglyEqual(offset1: number, offset2: number): boolean; 170 | } 171 | 172 | export interface ITimeout { 173 | isValid(): boolean; 174 | } 175 | 176 | export class InfiniteTimeout implements ITimeout { 177 | public static instance = new InfiniteTimeout(); 178 | 179 | isValid(): boolean { 180 | return true; 181 | } 182 | } 183 | 184 | export class DateTimeout implements ITimeout { 185 | private readonly startTime = Date.now(); 186 | private valid = true; 187 | 188 | constructor(private timeout: number) { 189 | if (timeout <= 0) { 190 | throw new BugIndicatingError('timeout must be positive'); 191 | } 192 | } 193 | 194 | // Recommendation: Set a log-point `{this.disable()}` in the body 195 | public isValid(): boolean { 196 | const valid = Date.now() - this.startTime < this.timeout; 197 | if (!valid && this.valid) { 198 | this.valid = false; // timeout reached 199 | } 200 | return this.valid; 201 | } 202 | 203 | public disable() { 204 | this.timeout = Number.MAX_SAFE_INTEGER; 205 | this.isValid = () => true; 206 | this.valid = true; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { OffsetRange } from '../../../core/offsetRange.js'; 7 | import { IDiffAlgorithm, SequenceDiff, ISequence, ITimeout, InfiniteTimeout, DiffAlgorithmResult } from './diffAlgorithm.js'; 8 | import { Array2D } from '../utils.js'; 9 | 10 | /** 11 | * A O(MN) diffing algorithm that supports a score function. 12 | * The algorithm can be improved by processing the 2d array diagonally. 13 | */ 14 | export class DynamicProgrammingDiffing implements IDiffAlgorithm { 15 | compute(sequence1: ISequence, sequence2: ISequence, timeout: ITimeout = InfiniteTimeout.instance, equalityScore?: (offset1: number, offset2: number) => number): DiffAlgorithmResult { 16 | if (sequence1.length === 0 || sequence2.length === 0) { 17 | return DiffAlgorithmResult.trivial(sequence1, sequence2); 18 | } 19 | 20 | /** 21 | * lcsLengths.get(i, j): Length of the longest common subsequence of sequence1.substring(0, i + 1) and sequence2.substring(0, j + 1). 22 | */ 23 | const lcsLengths = new Array2D(sequence1.length, sequence2.length); 24 | const directions = new Array2D(sequence1.length, sequence2.length); 25 | const lengths = new Array2D(sequence1.length, sequence2.length); 26 | 27 | // ==== Initializing lcsLengths ==== 28 | for (let s1 = 0; s1 < sequence1.length; s1++) { 29 | for (let s2 = 0; s2 < sequence2.length; s2++) { 30 | if (!timeout.isValid()) { 31 | return DiffAlgorithmResult.trivialTimedOut(sequence1, sequence2); 32 | } 33 | 34 | const horizontalLen = s1 === 0 ? 0 : lcsLengths.get(s1 - 1, s2); 35 | const verticalLen = s2 === 0 ? 0 : lcsLengths.get(s1, s2 - 1); 36 | 37 | let extendedSeqScore: number; 38 | if (sequence1.getElement(s1) === sequence2.getElement(s2)) { 39 | if (s1 === 0 || s2 === 0) { 40 | extendedSeqScore = 0; 41 | } else { 42 | extendedSeqScore = lcsLengths.get(s1 - 1, s2 - 1); 43 | } 44 | if (s1 > 0 && s2 > 0 && directions.get(s1 - 1, s2 - 1) === 3) { 45 | // Prefer consecutive diagonals 46 | extendedSeqScore += lengths.get(s1 - 1, s2 - 1); 47 | } 48 | extendedSeqScore += (equalityScore ? equalityScore(s1, s2) : 1); 49 | } else { 50 | extendedSeqScore = -1; 51 | } 52 | 53 | const newValue = Math.max(horizontalLen, verticalLen, extendedSeqScore); 54 | 55 | if (newValue === extendedSeqScore) { 56 | // Prefer diagonals 57 | const prevLen = s1 > 0 && s2 > 0 ? lengths.get(s1 - 1, s2 - 1) : 0; 58 | lengths.set(s1, s2, prevLen + 1); 59 | directions.set(s1, s2, 3); 60 | } else if (newValue === horizontalLen) { 61 | lengths.set(s1, s2, 0); 62 | directions.set(s1, s2, 1); 63 | } else if (newValue === verticalLen) { 64 | lengths.set(s1, s2, 0); 65 | directions.set(s1, s2, 2); 66 | } 67 | 68 | lcsLengths.set(s1, s2, newValue); 69 | } 70 | } 71 | 72 | // ==== Backtracking ==== 73 | const result: SequenceDiff[] = []; 74 | let lastAligningPosS1: number = sequence1.length; 75 | let lastAligningPosS2: number = sequence2.length; 76 | 77 | function reportDecreasingAligningPositions(s1: number, s2: number): void { 78 | if (s1 + 1 !== lastAligningPosS1 || s2 + 1 !== lastAligningPosS2) { 79 | result.push(new SequenceDiff( 80 | new OffsetRange(s1 + 1, lastAligningPosS1), 81 | new OffsetRange(s2 + 1, lastAligningPosS2), 82 | )); 83 | } 84 | lastAligningPosS1 = s1; 85 | lastAligningPosS2 = s2; 86 | } 87 | 88 | let s1 = sequence1.length - 1; 89 | let s2 = sequence2.length - 1; 90 | while (s1 >= 0 && s2 >= 0) { 91 | if (directions.get(s1, s2) === 3) { 92 | reportDecreasingAligningPositions(s1, s2); 93 | s1--; 94 | s2--; 95 | } else { 96 | if (directions.get(s1, s2) === 1) { 97 | s1--; 98 | } else { 99 | s2--; 100 | } 101 | } 102 | } 103 | reportDecreasingAligningPositions(-1, -1); 104 | result.reverse(); 105 | return new DiffAlgorithmResult(result, false); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { OffsetRange } from '../../../core/offsetRange.js'; 7 | import { DiffAlgorithmResult, IDiffAlgorithm, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from './diffAlgorithm.js'; 8 | 9 | /** 10 | * An O(ND) diff algorithm that has a quadratic space worst-case complexity. 11 | */ 12 | export class MyersDiffAlgorithm implements IDiffAlgorithm { 13 | compute(seq1: ISequence, seq2: ISequence, timeout: ITimeout = InfiniteTimeout.instance): DiffAlgorithmResult { 14 | // These are common special cases. 15 | // The early return improves performance dramatically. 16 | if (seq1.length === 0 || seq2.length === 0) { 17 | return DiffAlgorithmResult.trivial(seq1, seq2); 18 | } 19 | 20 | const seqX = seq1; // Text on the x axis 21 | const seqY = seq2; // Text on the y axis 22 | 23 | function getXAfterSnake(x: number, y: number): number { 24 | while (x < seqX.length && y < seqY.length && seqX.getElement(x) === seqY.getElement(y)) { 25 | x++; 26 | y++; 27 | } 28 | return x; 29 | } 30 | 31 | let d = 0; 32 | // V[k]: X value of longest d-line that ends in diagonal k. 33 | // d-line: path from (0,0) to (x,y) that uses exactly d non-diagonals. 34 | // diagonal k: Set of points (x,y) with x-y = k. 35 | // k=1 -> (1,0),(2,1) 36 | const V = new FastInt32Array(); 37 | V.set(0, getXAfterSnake(0, 0)); 38 | 39 | const paths = new FastArrayNegativeIndices(); 40 | paths.set(0, V.get(0) === 0 ? null : new SnakePath(null, 0, 0, V.get(0))); 41 | 42 | let k = 0; 43 | 44 | loop: while (true) { 45 | d++; 46 | if (!timeout.isValid()) { 47 | return DiffAlgorithmResult.trivialTimedOut(seqX, seqY); 48 | } 49 | // The paper has `for (k = -d; k <= d; k += 2)`, but we can ignore diagonals that cannot influence the result. 50 | const lowerBound = -Math.min(d, seqY.length + (d % 2)); 51 | const upperBound = Math.min(d, seqX.length + (d % 2)); 52 | for (k = lowerBound; k <= upperBound; k += 2) { 53 | let step = 0; 54 | // We can use the X values of (d-1)-lines to compute X value of the longest d-lines. 55 | const maxXofDLineTop = k === upperBound ? -1 : V.get(k + 1); // We take a vertical non-diagonal (add a symbol in seqX) 56 | const maxXofDLineLeft = k === lowerBound ? -1 : V.get(k - 1) + 1; // We take a horizontal non-diagonal (+1 x) (delete a symbol in seqX) 57 | step++; 58 | const x = Math.min(Math.max(maxXofDLineTop, maxXofDLineLeft), seqX.length); 59 | const y = x - k; 60 | step++; 61 | if (x > seqX.length || y > seqY.length) { 62 | // This diagonal is irrelevant for the result. 63 | // TODO: Don't pay the cost for this in the next iteration. 64 | continue; 65 | } 66 | const newMaxX = getXAfterSnake(x, y); 67 | V.set(k, newMaxX); 68 | const lastPath = x === maxXofDLineTop ? paths.get(k + 1) : paths.get(k - 1); 69 | paths.set(k, newMaxX !== x ? new SnakePath(lastPath, x, y, newMaxX - x) : lastPath); 70 | 71 | if (V.get(k) === seqX.length && V.get(k) - k === seqY.length) { 72 | break loop; 73 | } 74 | } 75 | } 76 | 77 | let path = paths.get(k); 78 | const result: SequenceDiff[] = []; 79 | let lastAligningPosS1: number = seqX.length; 80 | let lastAligningPosS2: number = seqY.length; 81 | 82 | while (true) { 83 | const endX = path ? path.x + path.length : 0; 84 | const endY = path ? path.y + path.length : 0; 85 | 86 | if (endX !== lastAligningPosS1 || endY !== lastAligningPosS2) { 87 | result.push(new SequenceDiff( 88 | new OffsetRange(endX, lastAligningPosS1), 89 | new OffsetRange(endY, lastAligningPosS2), 90 | )); 91 | } 92 | if (!path) { 93 | break; 94 | } 95 | lastAligningPosS1 = path.x; 96 | lastAligningPosS2 = path.y; 97 | 98 | path = path.prev; 99 | } 100 | 101 | result.reverse(); 102 | return new DiffAlgorithmResult(result, false); 103 | } 104 | } 105 | 106 | class SnakePath { 107 | constructor( 108 | public readonly prev: SnakePath | null, 109 | public readonly x: number, 110 | public readonly y: number, 111 | public readonly length: number 112 | ) { 113 | } 114 | } 115 | 116 | /** 117 | * An array that supports fast negative indices. 118 | */ 119 | class FastInt32Array { 120 | private positiveArr: Int32Array = new Int32Array(10); 121 | private negativeArr: Int32Array = new Int32Array(10); 122 | 123 | get(idx: number): number { 124 | if (idx < 0) { 125 | idx = -idx - 1; 126 | return this.negativeArr[idx]; 127 | } else { 128 | return this.positiveArr[idx]; 129 | } 130 | } 131 | 132 | set(idx: number, value: number): void { 133 | if (idx < 0) { 134 | idx = -idx - 1; 135 | if (idx >= this.negativeArr.length) { 136 | const arr = this.negativeArr; 137 | this.negativeArr = new Int32Array(arr.length * 2); 138 | this.negativeArr.set(arr); 139 | } 140 | this.negativeArr[idx] = value; 141 | } else { 142 | if (idx >= this.positiveArr.length) { 143 | const arr = this.positiveArr; 144 | this.positiveArr = new Int32Array(arr.length * 2); 145 | this.positiveArr.set(arr); 146 | } 147 | this.positiveArr[idx] = value; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * An array that supports fast negative indices. 154 | */ 155 | class FastArrayNegativeIndices { 156 | private readonly positiveArr: T[] = []; 157 | private readonly negativeArr: T[] = []; 158 | 159 | get(idx: number): T { 160 | if (idx < 0) { 161 | idx = -idx - 1; 162 | return this.negativeArr[idx]; 163 | } else { 164 | return this.positiveArr[idx]; 165 | } 166 | } 167 | 168 | set(idx: number, value: T): void { 169 | if (idx < 0) { 170 | idx = -idx - 1; 171 | this.negativeArr[idx] = value; 172 | } else { 173 | this.positiveArr[idx] = value; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { equals } from '../../../../base/common/arrays.js'; 7 | import { assertFn } from '../../../../base/common/assert.js'; 8 | import { LineRange } from '../../core/lineRange.js'; 9 | import { OffsetRange } from '../../core/offsetRange.js'; 10 | import { Position } from '../../core/position.js'; 11 | import { Range } from '../../core/range.js'; 12 | import { ArrayText } from '../../core/textEdit.js'; 13 | import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js'; 14 | import { DetailedLineRangeMapping, LineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../rangeMapping.js'; 15 | import { DateTimeout, InfiniteTimeout, ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js'; 16 | import { DynamicProgrammingDiffing } from './algorithms/dynamicProgrammingDiffing.js'; 17 | import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js'; 18 | import { computeMovedLines } from './computeMovedLines.js'; 19 | import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from './heuristicSequenceOptimizations.js'; 20 | import { LineSequence } from './lineSequence.js'; 21 | import { LinesSliceCharSequence } from './linesSliceCharSequence.js'; 22 | 23 | export class DefaultLinesDiffComputer implements ILinesDiffComputer { 24 | private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); 25 | private readonly myersDiffingAlgorithm = new MyersDiffAlgorithm(); 26 | 27 | computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): LinesDiff { 28 | if (originalLines.length <= 1 && equals(originalLines, modifiedLines, (a, b) => a === b)) { 29 | return new LinesDiff([], [], false); 30 | } 31 | 32 | if (originalLines.length === 1 && originalLines[0].length === 0 || modifiedLines.length === 1 && modifiedLines[0].length === 0) { 33 | return new LinesDiff([ 34 | new DetailedLineRangeMapping( 35 | new LineRange(1, originalLines.length + 1), 36 | new LineRange(1, modifiedLines.length + 1), 37 | [ 38 | new RangeMapping( 39 | new Range(1, 1, originalLines.length, originalLines[originalLines.length - 1].length + 1), 40 | new Range(1, 1, modifiedLines.length, modifiedLines[modifiedLines.length - 1].length + 1), 41 | ) 42 | ] 43 | ) 44 | ], [], false); 45 | } 46 | 47 | const timeout = options.maxComputationTimeMs === 0 ? InfiniteTimeout.instance : new DateTimeout(options.maxComputationTimeMs); 48 | const considerWhitespaceChanges = !options.ignoreTrimWhitespace; 49 | 50 | const perfectHashes = new Map(); 51 | function getOrCreateHash(text: string): number { 52 | let hash = perfectHashes.get(text); 53 | if (hash === undefined) { 54 | hash = perfectHashes.size; 55 | perfectHashes.set(text, hash); 56 | } 57 | return hash; 58 | } 59 | 60 | const originalLinesHashes = originalLines.map((l) => getOrCreateHash(l.trim())); 61 | const modifiedLinesHashes = modifiedLines.map((l) => getOrCreateHash(l.trim())); 62 | 63 | const sequence1 = new LineSequence(originalLinesHashes, originalLines); 64 | const sequence2 = new LineSequence(modifiedLinesHashes, modifiedLines); 65 | 66 | const lineAlignmentResult = (() => { 67 | if (sequence1.length + sequence2.length < 1700) { 68 | // Use the improved algorithm for small files 69 | return this.dynamicProgrammingDiffing.compute( 70 | sequence1, 71 | sequence2, 72 | timeout, 73 | (offset1, offset2) => 74 | originalLines[offset1] === modifiedLines[offset2] 75 | ? modifiedLines[offset2].length === 0 76 | ? 0.1 77 | : 1 + Math.log(1 + modifiedLines[offset2].length) 78 | : 0.99 79 | ); 80 | } 81 | 82 | return this.myersDiffingAlgorithm.compute( 83 | sequence1, 84 | sequence2, 85 | timeout 86 | ); 87 | })(); 88 | 89 | let lineAlignments = lineAlignmentResult.diffs; 90 | let hitTimeout = lineAlignmentResult.hitTimeout; 91 | lineAlignments = optimizeSequenceDiffs(sequence1, sequence2, lineAlignments); 92 | lineAlignments = removeVeryShortMatchingLinesBetweenDiffs(sequence1, sequence2, lineAlignments); 93 | 94 | const alignments: RangeMapping[] = []; 95 | 96 | const scanForWhitespaceChanges = (equalLinesCount: number) => { 97 | if (!considerWhitespaceChanges) { 98 | return; 99 | } 100 | 101 | for (let i = 0; i < equalLinesCount; i++) { 102 | const seq1Offset = seq1LastStart + i; 103 | const seq2Offset = seq2LastStart + i; 104 | if (originalLines[seq1Offset] !== modifiedLines[seq2Offset]) { 105 | // This is because of whitespace changes, diff these lines 106 | const characterDiffs = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( 107 | new OffsetRange(seq1Offset, seq1Offset + 1), 108 | new OffsetRange(seq2Offset, seq2Offset + 1), 109 | ), timeout, considerWhitespaceChanges, options); 110 | for (const a of characterDiffs.mappings) { 111 | alignments.push(a); 112 | } 113 | if (characterDiffs.hitTimeout) { 114 | hitTimeout = true; 115 | } 116 | } 117 | } 118 | }; 119 | 120 | let seq1LastStart = 0; 121 | let seq2LastStart = 0; 122 | 123 | for (const diff of lineAlignments) { 124 | assertFn(() => diff.seq1Range.start - seq1LastStart === diff.seq2Range.start - seq2LastStart); 125 | 126 | const equalLinesCount = diff.seq1Range.start - seq1LastStart; 127 | 128 | scanForWhitespaceChanges(equalLinesCount); 129 | 130 | seq1LastStart = diff.seq1Range.endExclusive; 131 | seq2LastStart = diff.seq2Range.endExclusive; 132 | 133 | const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges, options); 134 | if (characterDiffs.hitTimeout) { 135 | hitTimeout = true; 136 | } 137 | for (const a of characterDiffs.mappings) { 138 | alignments.push(a); 139 | } 140 | } 141 | 142 | scanForWhitespaceChanges(originalLines.length - seq1LastStart); 143 | 144 | const changes = lineRangeMappingFromRangeMappings(alignments, new ArrayText(originalLines), new ArrayText(modifiedLines)); 145 | 146 | let moves: MovedText[] = []; 147 | if (options.computeMoves) { 148 | moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges, options); 149 | } 150 | 151 | // Make sure all ranges are valid 152 | assertFn(() => { 153 | function validatePosition(pos: Position, lines: string[]): boolean { 154 | if (pos.lineNumber < 1 || pos.lineNumber > lines.length) { return false; } 155 | const line = lines[pos.lineNumber - 1]; 156 | if (pos.column < 1 || pos.column > line.length + 1) { return false; } 157 | return true; 158 | } 159 | 160 | function validateRange(range: LineRange, lines: string[]): boolean { 161 | if (range.startLineNumber < 1 || range.startLineNumber > lines.length + 1) { return false; } 162 | if (range.endLineNumberExclusive < 1 || range.endLineNumberExclusive > lines.length + 1) { return false; } 163 | return true; 164 | } 165 | 166 | for (const c of changes) { 167 | if (!c.innerChanges) { return false; } 168 | for (const ic of c.innerChanges) { 169 | const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) && 170 | validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines); 171 | if (!valid) { 172 | return false; 173 | } 174 | } 175 | if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) { 176 | return false; 177 | } 178 | } 179 | return true; 180 | }); 181 | 182 | return new LinesDiff(changes, moves, hitTimeout); 183 | } 184 | 185 | private computeMoves( 186 | changes: DetailedLineRangeMapping[], 187 | originalLines: string[], 188 | modifiedLines: string[], 189 | hashedOriginalLines: number[], 190 | hashedModifiedLines: number[], 191 | timeout: ITimeout, 192 | considerWhitespaceChanges: boolean, 193 | options: ILinesDiffComputerOptions, 194 | ): MovedText[] { 195 | const moves = computeMovedLines( 196 | changes, 197 | originalLines, 198 | modifiedLines, 199 | hashedOriginalLines, 200 | hashedModifiedLines, 201 | timeout, 202 | ); 203 | const movesWithDiffs = moves.map(m => { 204 | const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( 205 | m.original.toOffsetRange(), 206 | m.modified.toOffsetRange(), 207 | ), timeout, considerWhitespaceChanges, options); 208 | const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, new ArrayText(originalLines), new ArrayText(modifiedLines), true); 209 | return new MovedText(m, mappings); 210 | }); 211 | return movesWithDiffs; 212 | } 213 | 214 | private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean, options: ILinesDiffComputerOptions): { mappings: RangeMapping[]; hitTimeout: boolean } { 215 | const lineRangeMapping = toLineRangeMapping(diff); 216 | const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines); 217 | 218 | const slice1 = new LinesSliceCharSequence(originalLines, rangeMapping.originalRange, considerWhitespaceChanges); 219 | const slice2 = new LinesSliceCharSequence(modifiedLines, rangeMapping.modifiedRange, considerWhitespaceChanges); 220 | 221 | const diffResult = slice1.length + slice2.length < 500 222 | ? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout) 223 | : this.myersDiffingAlgorithm.compute(slice1, slice2, timeout); 224 | 225 | const check = false; 226 | 227 | let diffs = diffResult.diffs; 228 | if (check) { SequenceDiff.assertSorted(diffs); } 229 | diffs = optimizeSequenceDiffs(slice1, slice2, diffs); 230 | if (check) { SequenceDiff.assertSorted(diffs); } 231 | diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findWordContaining(idx)); 232 | if (check) { SequenceDiff.assertSorted(diffs); } 233 | 234 | if (options.extendToSubwords) { 235 | diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findSubWordContaining(idx), true); 236 | if (check) { SequenceDiff.assertSorted(diffs); } 237 | } 238 | 239 | diffs = removeShortMatches(slice1, slice2, diffs); 240 | if (check) { SequenceDiff.assertSorted(diffs); } 241 | diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs); 242 | if (check) { SequenceDiff.assertSorted(diffs); } 243 | 244 | const result = diffs.map( 245 | (d) => 246 | new RangeMapping( 247 | slice1.translateRange(d.seq1Range), 248 | slice2.translateRange(d.seq2Range) 249 | ) 250 | ); 251 | 252 | if (check) { RangeMapping.assertSorted(result); } 253 | 254 | // Assert: result applied on original should be the same as diff applied to original 255 | 256 | return { 257 | mappings: result, 258 | hitTimeout: diffResult.hitTimeout, 259 | }; 260 | } 261 | } 262 | 263 | function toLineRangeMapping(sequenceDiff: SequenceDiff) { 264 | return new LineRangeMapping( 265 | new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1), 266 | new LineRange(sequenceDiff.seq2Range.start + 1, sequenceDiff.seq2Range.endExclusive + 1), 267 | ); 268 | } 269 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/lineSequence.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CharCode } from '../../../../base/common/charCode.js'; 7 | import { OffsetRange } from '../../core/offsetRange.js'; 8 | import { ISequence } from './algorithms/diffAlgorithm.js'; 9 | 10 | export class LineSequence implements ISequence { 11 | constructor( 12 | private readonly trimmedHash: number[], 13 | private readonly lines: string[] 14 | ) { } 15 | 16 | getElement(offset: number): number { 17 | return this.trimmedHash[offset]; 18 | } 19 | 20 | get length(): number { 21 | return this.trimmedHash.length; 22 | } 23 | 24 | getBoundaryScore(length: number): number { 25 | const indentationBefore = length === 0 ? 0 : getIndentation(this.lines[length - 1]); 26 | const indentationAfter = length === this.lines.length ? 0 : getIndentation(this.lines[length]); 27 | return 1000 - (indentationBefore + indentationAfter); 28 | } 29 | 30 | getText(range: OffsetRange): string { 31 | return this.lines.slice(range.start, range.endExclusive).join('\n'); 32 | } 33 | 34 | isStronglyEqual(offset1: number, offset2: number): boolean { 35 | return this.lines[offset1] === this.lines[offset2]; 36 | } 37 | } 38 | 39 | function getIndentation(str: string): number { 40 | let i = 0; 41 | while (i < str.length && (str.charCodeAt(i) === CharCode.Space || str.charCodeAt(i) === CharCode.Tab)) { 42 | i++; 43 | } 44 | return i; 45 | } 46 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { findLastIdxMonotonous, findLastMonotonous, findFirstMonotonous } from '../../../../base/common/arraysFind.js'; 7 | import { CharCode } from '../../../../base/common/charCode.js'; 8 | import { OffsetRange } from '../../core/offsetRange.js'; 9 | import { Position } from '../../core/position.js'; 10 | import { Range } from '../../core/range.js'; 11 | import { ISequence } from './algorithms/diffAlgorithm.js'; 12 | import { isSpace } from './utils.js'; 13 | 14 | export class LinesSliceCharSequence implements ISequence { 15 | private readonly elements: number[] = []; 16 | private readonly firstElementOffsetByLineIdx: number[] = []; 17 | private readonly lineStartOffsets: number[] = []; 18 | private readonly trimmedWsLengthsByLineIdx: number[] = []; 19 | 20 | constructor(public readonly lines: string[], private readonly range: Range, public readonly considerWhitespaceChanges: boolean) { 21 | this.firstElementOffsetByLineIdx.push(0); 22 | for (let lineNumber = this.range.startLineNumber; lineNumber <= this.range.endLineNumber; lineNumber++) { 23 | let line = lines[lineNumber - 1]; 24 | let lineStartOffset = 0; 25 | if (lineNumber === this.range.startLineNumber && this.range.startColumn > 1) { 26 | lineStartOffset = this.range.startColumn - 1; 27 | line = line.substring(lineStartOffset); 28 | } 29 | this.lineStartOffsets.push(lineStartOffset); 30 | 31 | let trimmedWsLength = 0; 32 | if (!considerWhitespaceChanges) { 33 | const trimmedStartLine = line.trimStart(); 34 | trimmedWsLength = line.length - trimmedStartLine.length; 35 | line = trimmedStartLine.trimEnd(); 36 | } 37 | this.trimmedWsLengthsByLineIdx.push(trimmedWsLength); 38 | 39 | const lineLength = lineNumber === this.range.endLineNumber ? Math.min(this.range.endColumn - 1 - lineStartOffset - trimmedWsLength, line.length) : line.length; 40 | for (let i = 0; i < lineLength; i++) { 41 | this.elements.push(line.charCodeAt(i)); 42 | } 43 | 44 | if (lineNumber < this.range.endLineNumber) { 45 | this.elements.push('\n'.charCodeAt(0)); 46 | this.firstElementOffsetByLineIdx.push(this.elements.length); 47 | } 48 | } 49 | } 50 | 51 | toString() { 52 | return `Slice: "${this.text}"`; 53 | } 54 | 55 | get text(): string { 56 | return this.getText(new OffsetRange(0, this.length)); 57 | } 58 | 59 | getText(range: OffsetRange): string { 60 | return this.elements.slice(range.start, range.endExclusive).map(e => String.fromCharCode(e)).join(''); 61 | } 62 | 63 | getElement(offset: number): number { 64 | return this.elements[offset]; 65 | } 66 | 67 | get length(): number { 68 | return this.elements.length; 69 | } 70 | 71 | public getBoundaryScore(length: number): number { 72 | // a b c , d e f 73 | // 11 0 0 12 15 6 13 0 0 11 74 | 75 | const prevCategory = getCategory(length > 0 ? this.elements[length - 1] : -1); 76 | const nextCategory = getCategory(length < this.elements.length ? this.elements[length] : -1); 77 | 78 | if (prevCategory === CharBoundaryCategory.LineBreakCR && nextCategory === CharBoundaryCategory.LineBreakLF) { 79 | // don't break between \r and \n 80 | return 0; 81 | } 82 | if (prevCategory === CharBoundaryCategory.LineBreakLF) { 83 | // prefer the linebreak before the change 84 | return 150; 85 | } 86 | 87 | let score = 0; 88 | if (prevCategory !== nextCategory) { 89 | score += 10; 90 | if (prevCategory === CharBoundaryCategory.WordLower && nextCategory === CharBoundaryCategory.WordUpper) { 91 | score += 1; 92 | } 93 | } 94 | 95 | score += getCategoryBoundaryScore(prevCategory); 96 | score += getCategoryBoundaryScore(nextCategory); 97 | 98 | return score; 99 | } 100 | 101 | public translateOffset(offset: number, preference: 'left' | 'right' = 'right'): Position { 102 | // find smallest i, so that lineBreakOffsets[i] <= offset using binary search 103 | const i = findLastIdxMonotonous(this.firstElementOffsetByLineIdx, (value) => value <= offset); 104 | const lineOffset = offset - this.firstElementOffsetByLineIdx[i]; 105 | return new Position( 106 | this.range.startLineNumber + i, 107 | 1 + this.lineStartOffsets[i] + lineOffset + ((lineOffset === 0 && preference === 'left') ? 0 : this.trimmedWsLengthsByLineIdx[i]) 108 | ); 109 | } 110 | 111 | public translateRange(range: OffsetRange): Range { 112 | const pos1 = this.translateOffset(range.start, 'right'); 113 | const pos2 = this.translateOffset(range.endExclusive, 'left'); 114 | if (pos2.isBefore(pos1)) { 115 | return Range.fromPositions(pos2, pos2); 116 | } 117 | return Range.fromPositions(pos1, pos2); 118 | } 119 | 120 | /** 121 | * Finds the word that contains the character at the given offset 122 | */ 123 | public findWordContaining(offset: number): OffsetRange | undefined { 124 | if (offset < 0 || offset >= this.elements.length) { 125 | return undefined; 126 | } 127 | 128 | if (!isWordChar(this.elements[offset])) { 129 | return undefined; 130 | } 131 | 132 | // find start 133 | let start = offset; 134 | while (start > 0 && isWordChar(this.elements[start - 1])) { 135 | start--; 136 | } 137 | 138 | // find end 139 | let end = offset; 140 | while (end < this.elements.length && isWordChar(this.elements[end])) { 141 | end++; 142 | } 143 | 144 | return new OffsetRange(start, end); 145 | } 146 | 147 | /** fooBar has the two sub-words foo and bar */ 148 | public findSubWordContaining(offset: number): OffsetRange | undefined { 149 | if (offset < 0 || offset >= this.elements.length) { 150 | return undefined; 151 | } 152 | 153 | if (!isWordChar(this.elements[offset])) { 154 | return undefined; 155 | } 156 | 157 | // find start 158 | let start = offset; 159 | while (start > 0 && isWordChar(this.elements[start - 1]) && !isUpperCase(this.elements[start])) { 160 | start--; 161 | } 162 | 163 | // find end 164 | let end = offset; 165 | while (end < this.elements.length && isWordChar(this.elements[end]) && !isUpperCase(this.elements[end])) { 166 | end++; 167 | } 168 | 169 | return new OffsetRange(start, end); 170 | } 171 | 172 | public countLinesIn(range: OffsetRange): number { 173 | return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber; 174 | } 175 | 176 | public isStronglyEqual(offset1: number, offset2: number): boolean { 177 | return this.elements[offset1] === this.elements[offset2]; 178 | } 179 | 180 | public extendToFullLines(range: OffsetRange): OffsetRange { 181 | const start = findLastMonotonous(this.firstElementOffsetByLineIdx, x => x <= range.start) ?? 0; 182 | const end = findFirstMonotonous(this.firstElementOffsetByLineIdx, x => range.endExclusive <= x) ?? this.elements.length; 183 | return new OffsetRange(start, end); 184 | } 185 | } 186 | 187 | function isWordChar(charCode: number): boolean { 188 | return charCode >= CharCode.a && charCode <= CharCode.z 189 | || charCode >= CharCode.A && charCode <= CharCode.Z 190 | || charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9; 191 | } 192 | 193 | function isUpperCase(charCode: number): boolean { 194 | return charCode >= CharCode.A && charCode <= CharCode.Z; 195 | } 196 | 197 | const enum CharBoundaryCategory { 198 | WordLower, 199 | WordUpper, 200 | WordNumber, 201 | End, 202 | Other, 203 | Separator, 204 | Space, 205 | LineBreakCR, 206 | LineBreakLF, 207 | } 208 | 209 | const score: Record = { 210 | [CharBoundaryCategory.WordLower]: 0, 211 | [CharBoundaryCategory.WordUpper]: 0, 212 | [CharBoundaryCategory.WordNumber]: 0, 213 | [CharBoundaryCategory.End]: 10, 214 | [CharBoundaryCategory.Other]: 2, 215 | [CharBoundaryCategory.Separator]: 30, 216 | [CharBoundaryCategory.Space]: 3, 217 | [CharBoundaryCategory.LineBreakCR]: 10, 218 | [CharBoundaryCategory.LineBreakLF]: 10, 219 | }; 220 | 221 | function getCategoryBoundaryScore(category: CharBoundaryCategory): number { 222 | return score[category]; 223 | } 224 | 225 | function getCategory(charCode: number): CharBoundaryCategory { 226 | if (charCode === CharCode.LineFeed) { 227 | return CharBoundaryCategory.LineBreakLF; 228 | } else if (charCode === CharCode.CarriageReturn) { 229 | return CharBoundaryCategory.LineBreakCR; 230 | } else if (isSpace(charCode)) { 231 | return CharBoundaryCategory.Space; 232 | } else if (charCode >= CharCode.a && charCode <= CharCode.z) { 233 | return CharBoundaryCategory.WordLower; 234 | } else if (charCode >= CharCode.A && charCode <= CharCode.Z) { 235 | return CharBoundaryCategory.WordUpper; 236 | } else if (charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9) { 237 | return CharBoundaryCategory.WordNumber; 238 | } else if (charCode === -1) { 239 | return CharBoundaryCategory.End; 240 | } else if (charCode === CharCode.Comma || charCode === CharCode.Semicolon) { 241 | return CharBoundaryCategory.Separator; 242 | } else { 243 | return CharBoundaryCategory.Other; 244 | } 245 | } 246 | 247 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/defaultLinesDiffComputer/utils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CharCode } from '../../../../base/common/charCode.js'; 7 | import { LineRange } from '../../core/lineRange.js'; 8 | import { DetailedLineRangeMapping } from '../rangeMapping.js'; 9 | 10 | export class Array2D { 11 | private readonly array: T[] = []; 12 | 13 | constructor(public readonly width: number, public readonly height: number) { 14 | this.array = new Array(width * height); 15 | } 16 | 17 | get(x: number, y: number): T { 18 | return this.array[x + y * this.width]; 19 | } 20 | 21 | set(x: number, y: number, value: T): void { 22 | this.array[x + y * this.width] = value; 23 | } 24 | } 25 | 26 | export function isSpace(charCode: number): boolean { 27 | return charCode === CharCode.Space || charCode === CharCode.Tab; 28 | } 29 | 30 | export class LineRangeFragment { 31 | private static chrKeys = new Map(); 32 | 33 | private static getKey(chr: string): number { 34 | let key = this.chrKeys.get(chr); 35 | if (key === undefined) { 36 | key = this.chrKeys.size; 37 | this.chrKeys.set(chr, key); 38 | } 39 | return key; 40 | } 41 | 42 | private readonly totalCount: number; 43 | private readonly histogram: number[] = []; 44 | constructor( 45 | public readonly range: LineRange, 46 | public readonly lines: string[], 47 | public readonly source: DetailedLineRangeMapping, 48 | ) { 49 | let counter = 0; 50 | for (let i = range.startLineNumber - 1; i < range.endLineNumberExclusive - 1; i++) { 51 | const line = lines[i]; 52 | for (let j = 0; j < line.length; j++) { 53 | counter++; 54 | const chr = line[j]; 55 | const key = LineRangeFragment.getKey(chr); 56 | this.histogram[key] = (this.histogram[key] || 0) + 1; 57 | } 58 | counter++; 59 | const key = LineRangeFragment.getKey('\n'); 60 | this.histogram[key] = (this.histogram[key] || 0) + 1; 61 | } 62 | 63 | this.totalCount = counter; 64 | } 65 | 66 | public computeSimilarity(other: LineRangeFragment): number { 67 | let sumDifferences = 0; 68 | const maxLength = Math.max(this.histogram.length, other.histogram.length); 69 | for (let i = 0; i < maxLength; i++) { 70 | sumDifferences += Math.abs((this.histogram[i] ?? 0) - (other.histogram[i] ?? 0)); 71 | } 72 | return 1 - (sumDifferences / (this.totalCount + other.totalCount)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/linesDiffComputer.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { DetailedLineRangeMapping, LineRangeMapping } from './rangeMapping.js'; 7 | 8 | export interface ILinesDiffComputer { 9 | computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): LinesDiff; 10 | } 11 | 12 | export interface ILinesDiffComputerOptions { 13 | readonly ignoreTrimWhitespace: boolean; 14 | readonly maxComputationTimeMs: number; 15 | readonly computeMoves: boolean; 16 | readonly extendToSubwords?: boolean; 17 | } 18 | 19 | export class LinesDiff { 20 | constructor( 21 | readonly changes: readonly DetailedLineRangeMapping[], 22 | 23 | /** 24 | * Sorted by original line ranges. 25 | * The original line ranges and the modified line ranges must be disjoint (but can be touching). 26 | */ 27 | readonly moves: readonly MovedText[], 28 | 29 | /** 30 | * Indicates if the time out was reached. 31 | * In that case, the diffs might be an approximation and the user should be asked to rerun the diff with more time. 32 | */ 33 | readonly hitTimeout: boolean, 34 | ) { 35 | } 36 | } 37 | 38 | export class MovedText { 39 | public readonly lineRangeMapping: LineRangeMapping; 40 | 41 | /** 42 | * The diff from the original text to the moved text. 43 | * Must be contained in the original/modified line range. 44 | * Can be empty if the text didn't change (only moved). 45 | */ 46 | public readonly changes: readonly DetailedLineRangeMapping[]; 47 | 48 | constructor( 49 | lineRangeMapping: LineRangeMapping, 50 | changes: readonly DetailedLineRangeMapping[], 51 | ) { 52 | this.lineRangeMapping = lineRangeMapping; 53 | this.changes = changes; 54 | } 55 | 56 | public flip(): MovedText { 57 | return new MovedText(this.lineRangeMapping.flip(), this.changes.map(c => c.flip())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/vendor/vscode/editor/common/diff/linesDiffComputers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { LegacyLinesDiffComputer } from './legacyLinesDiffComputer.js'; 7 | import { DefaultLinesDiffComputer } from './defaultLinesDiffComputer/defaultLinesDiffComputer.js'; 8 | import { ILinesDiffComputer } from './linesDiffComputer.js'; 9 | 10 | export const linesDiffComputers = { 11 | getLegacy: () => new LegacyLinesDiffComputer(), 12 | getDefault: () => new DefaultLinesDiffComputer(), 13 | } satisfies Record ILinesDiffComputer>; 14 | -------------------------------------------------------------------------------- /src/vendor/winston-transport-vscode/logOutputChannelTransport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file was copied and modified from the vscode-winston-transport package. 3 | */ 4 | 5 | /* 6 | Apache License 7 | Version 2.0, January 2004 8 | http://www.apache.org/licenses/ 9 | 10 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 11 | 12 | 1. Definitions. 13 | 14 | "License" shall mean the terms and conditions for use, reproduction, 15 | and distribution as defined by Sections 1 through 9 of this document. 16 | 17 | "Licensor" shall mean the copyright owner or entity authorized by 18 | the copyright owner that is granting the License. 19 | 20 | "Legal Entity" shall mean the union of the acting entity and all 21 | other entities that control, are controlled by, or are under common 22 | control with that entity. For the purposes of this definition, 23 | "control" means (i) the power, direct or indirect, to cause the 24 | direction or management of such entity, whether by contract or 25 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 26 | outstanding shares, or (iii) beneficial ownership of such entity. 27 | 28 | "You" (or "Your") shall mean an individual or Legal Entity 29 | exercising permissions granted by this License. 30 | 31 | "Source" form shall mean the preferred form for making modifications, 32 | including but not limited to software source code, documentation 33 | source, and configuration files. 34 | 35 | "Object" form shall mean any form resulting from mechanical 36 | transformation or translation of a Source form, including but 37 | not limited to compiled object code, generated documentation, 38 | and conversions to other media types. 39 | 40 | "Work" shall mean the work of authorship, whether in Source or 41 | Object form, made available under the License, as indicated by a 42 | copyright notice that is included in or attached to the work 43 | (an example is provided in the Appendix below). 44 | 45 | "Derivative Works" shall mean any work, whether in Source or Object 46 | form, that is based on (or derived from) the Work and for which the 47 | editorial revisions, annotations, elaborations, or other modifications 48 | represent, as a whole, an original work of authorship. For the purposes 49 | of this License, Derivative Works shall not include works that remain 50 | separable from, or merely link (or bind by name) to the interfaces of, 51 | the Work and Derivative Works thereof. 52 | 53 | "Contribution" shall mean any work of authorship, including 54 | the original version of the Work and any modifications or additions 55 | to that Work or Derivative Works thereof, that is intentionally 56 | submitted to Licensor for inclusion in the Work by the copyright owner 57 | or by an individual or Legal Entity authorized to submit on behalf of 58 | the copyright owner. For the purposes of this definition, "submitted" 59 | means any form of electronic, verbal, or written communication sent 60 | to the Licensor or its representatives, including but not limited to 61 | communication on electronic mailing lists, source code control systems, 62 | and issue tracking systems that are managed by, or on behalf of, the 63 | Licensor for the purpose of discussing and improving the Work, but 64 | excluding communication that is conspicuously marked or otherwise 65 | designated in writing by the copyright owner as "Not a Contribution." 66 | 67 | "Contributor" shall mean Licensor and any individual or Legal Entity 68 | on behalf of whom a Contribution has been received by Licensor and 69 | subsequently incorporated within the Work. 70 | 71 | 2. Grant of Copyright License. Subject to the terms and conditions of 72 | this License, each Contributor hereby grants to You a perpetual, 73 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 74 | copyright license to reproduce, prepare Derivative Works of, 75 | publicly display, publicly perform, sublicense, and distribute the 76 | Work and such Derivative Works in Source or Object form. 77 | 78 | 3. Grant of Patent License. Subject to the terms and conditions of 79 | this License, each Contributor hereby grants to You a perpetual, 80 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 81 | (except as stated in this section) patent license to make, have made, 82 | use, offer to sell, sell, import, and otherwise transfer the Work, 83 | where such license applies only to those patent claims licensable 84 | by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) 86 | with the Work to which such Contribution(s) was submitted. If You 87 | institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work 89 | or a Contribution incorporated within the Work constitutes direct 90 | or contributory patent infringement, then any patent licenses 91 | granted to You under this License for that Work shall terminate 92 | as of the date such litigation is filed. 93 | 94 | 4. Redistribution. You may reproduce and distribute copies of the 95 | Work or Derivative Works thereof in any medium, with or without 96 | modifications, and in Source or Object form, provided that You 97 | meet the following conditions: 98 | 99 | (a) You must give any other recipients of the Work or 100 | Derivative Works a copy of this License; and 101 | 102 | (b) You must cause any modified files to carry prominent notices 103 | stating that You changed the files; and 104 | 105 | (c) You must retain, in the Source form of any Derivative Works 106 | that You distribute, all copyright, patent, trademark, and 107 | attribution notices from the Source form of the Work, 108 | excluding those notices that do not pertain to any part of 109 | the Derivative Works; and 110 | 111 | (d) If the Work includes a "NOTICE" text file as part of its 112 | distribution, then any Derivative Works that You distribute must 113 | include a readable copy of the attribution notices contained 114 | within such NOTICE file, excluding those notices that do not 115 | pertain to any part of the Derivative Works, in at least one 116 | of the following places: within a NOTICE text file distributed 117 | as part of the Derivative Works; within the Source form or 118 | documentation, if provided along with the Derivative Works; or, 119 | within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents 121 | of the NOTICE file are for informational purposes only and 122 | do not modify the License. You may add Your own attribution 123 | notices within Derivative Works that You distribute, alongside 124 | or as an addendum to the NOTICE text from the Work, provided 125 | that such additional attribution notices cannot be construed 126 | as modifying the License. 127 | 128 | You may add Your own copyright statement to Your modifications and 129 | may provide additional or different license terms and conditions 130 | for use, reproduction, or distribution of Your modifications, or 131 | for any such Derivative Works as a whole, provided Your use, 132 | reproduction, and distribution of the Work otherwise complies with 133 | the conditions stated in this License. 134 | 135 | 5. Submission of Contributions. Unless You explicitly state otherwise, 136 | any Contribution intentionally submitted for inclusion in the Work 137 | by You to the Licensor shall be under the terms and conditions of 138 | this License, without any additional terms or conditions. 139 | Notwithstanding the above, nothing herein shall supersede or modify 140 | the terms of any separate license agreement you may have executed 141 | with Licensor regarding such Contributions. 142 | 143 | 6. Trademarks. This License does not grant permission to use the trade 144 | names, trademarks, service marks, or product names of the Licensor, 145 | except as required for reasonable and customary use in describing the 146 | origin of the Work and reproducing the content of the NOTICE file. 147 | 148 | 7. Disclaimer of Warranty. Unless required by applicable law or 149 | agreed to in writing, Licensor provides the Work (and each 150 | Contributor provides its Contributions) on an "AS IS" BASIS, 151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 152 | implied, including, without limitation, any warranties or conditions 153 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 154 | PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any 156 | risks associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. In no event and under no legal theory, 159 | whether in tort (including negligence), contract, or otherwise, 160 | unless required by applicable law (such as deliberate and grossly 161 | negligent acts) or agreed to in writing, shall any Contributor be 162 | liable to You for damages, including any direct, indirect, special, 163 | incidental, or consequential damages of any character arising as a 164 | result of this License or out of the use or inability to use the 165 | Work (including but not limited to damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, or any and all 167 | other commercial damages or losses), even if such Contributor 168 | has been advised of the possibility of such damages. 169 | 170 | 9. Accepting Warranty or Additional Liability. While redistributing 171 | the Work or Derivative Works thereof, You may choose to offer, 172 | and charge a fee for, acceptance of support, warranty, indemnity, 173 | or other liability obligations and/or rights consistent with this 174 | License. However, in accepting such obligations, You may act only 175 | on Your own behalf and on Your sole responsibility, not on behalf 176 | of any other Contributor, and only if You agree to indemnify, 177 | defend, and hold each Contributor harmless for any liability 178 | incurred by, or claims asserted against, such Contributor by reason 179 | of your accepting any such warranty or additional liability. 180 | 181 | END OF TERMS AND CONDITIONS 182 | */ 183 | 184 | import { Config, LEVEL, MESSAGE } from "triple-beam"; 185 | import Transport, { TransportStreamOptions } from "winston-transport"; 186 | 187 | import type { TransformableInfo } from "logform"; 188 | import type { LogOutputChannel } from "vscode"; 189 | 190 | export class LogOutputChannelTransport extends Transport { 191 | private outputChannel: LogOutputChannel; 192 | 193 | constructor(opts: Options) { 194 | super(opts); 195 | this.outputChannel = opts.outputChannel; 196 | } 197 | 198 | public log(info: TransformableInfo, next: () => void) { 199 | setImmediate(() => { 200 | this.emit("logged", info); 201 | }); 202 | 203 | switch (info[LEVEL]) { 204 | case "error": 205 | this.outputChannel.error(info[MESSAGE] as string); 206 | break; 207 | case "warning": 208 | case "warn": 209 | this.outputChannel.warn(info[MESSAGE] as string); 210 | break; 211 | case "info": 212 | this.outputChannel.info(info[MESSAGE] as string); 213 | break; 214 | case "debug": 215 | this.outputChannel.debug(info[MESSAGE] as string); 216 | break; 217 | case "trace": 218 | this.outputChannel.trace(info[MESSAGE] as string); 219 | break; 220 | default: 221 | this.outputChannel.appendLine(info[MESSAGE] as string); 222 | break; 223 | } 224 | 225 | next(); 226 | } 227 | } 228 | 229 | export type Options = TransportStreamOptions & { 230 | outputChannel: LogOutputChannel; 231 | }; 232 | 233 | export const config: Config = { 234 | levels: { 235 | error: 0, 236 | warn: 1, 237 | info: 2, 238 | debug: 3, 239 | trace: 4, 240 | }, 241 | colors: { 242 | error: "red", 243 | warn: "yellow", 244 | info: "green", 245 | debug: "blue", 246 | trace: "grey", 247 | }, 248 | }; 249 | -------------------------------------------------------------------------------- /src/webview/graph.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | color: var(--vscode-foreground); 4 | font-family: var(--vscode-font-family); 5 | font-size: var(--vscode-font-size); 6 | background-color: var(--vscode-sideBar-background); 7 | } 8 | 9 | #graph { 10 | position: relative; 11 | padding-left: 8px; 12 | } 13 | 14 | #connections { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | pointer-events: none; 21 | z-index: 1; 22 | } 23 | 24 | /* Transition effects */ 25 | .node-circle circle, 26 | .connection-line { 27 | transition: opacity 0.2s ease-in-out; 28 | } 29 | 30 | .connection-line { 31 | stroke: var(--vscode-charts-blue); 32 | stroke-width: 2; 33 | fill: none; 34 | } 35 | 36 | /* Node styling */ 37 | .change-node { 38 | position: relative; 39 | z-index: 0; 40 | padding: 4px 8px; 41 | cursor: pointer; 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | min-height: 12px; 46 | } 47 | 48 | .change-node:hover { 49 | background-color: var(--vscode-list-hoverBackground); 50 | } 51 | 52 | .change-node.selected { 53 | background-color: var(--vscode-list-activeSelectionBackground); 54 | color: var(--vscode-list-activeSelectionForeground); 55 | } 56 | 57 | /* Dimming and highlighting effects */ 58 | .node-circle.dimmed circle, 59 | .node-circle.dimmed .heart-path { 60 | opacity: 0.1; 61 | } 62 | 63 | .node-circle.highlighted circle, 64 | .node-circle.highlighted .heart-path { 65 | opacity: 1; 66 | } 67 | 68 | .connection-line.dimmed { 69 | opacity: 0.1; 70 | } 71 | 72 | .connection-line.highlighted { 73 | opacity: 1; 74 | } 75 | 76 | /* Child connection styling */ 77 | .connection-line.highlighted.child-connection { 78 | stroke: #4CAF50; 79 | } 80 | 81 | /* Regular child node styling */ 82 | .node-circle.child-node circle { 83 | fill: #4CAF50; 84 | stroke: #4CAF50; 85 | } 86 | 87 | /* Diamond-specific child node styling */ 88 | .node-circle.child-node .diamond-path { 89 | fill: #4CAF50; 90 | } 91 | 92 | /* Text content styling */ 93 | .text-content { 94 | display: flex; 95 | flex-direction: column; 96 | min-height: 42px; 97 | /* Set a consistent minimum height */ 98 | justify-content: center; 99 | margin-left: var(--curve-offset, 0px); 100 | padding-left: 12px; 101 | } 102 | 103 | .label-text { 104 | line-height: 1.2; 105 | word-wrap: break-word; 106 | } 107 | 108 | .description { 109 | line-height: 1.2; 110 | font-size: 0.9em; 111 | opacity: 0.8; 112 | } 113 | 114 | /* Edit button styling */ 115 | .edit-button { 116 | opacity: 0; 117 | background: none; 118 | border: none; 119 | color: var(--vscode-button-foreground); 120 | cursor: pointer; 121 | padding: 2px 6px; 122 | font-size: 0.9em; 123 | border-radius: 3px; 124 | } 125 | 126 | .change-node:hover .edit-button { 127 | opacity: 1; 128 | } 129 | 130 | .edit-button:hover { 131 | background-color: var(--vscode-button-secondaryHoverBackground); 132 | } 133 | 134 | .edit-button .codicon { 135 | color: var(--vscode-icon-foreground); 136 | } 137 | 138 | .edit-button:hover .codicon { 139 | color: var(--vscode-button-secondaryForeground); 140 | } 141 | 142 | /* Node circle styling */ 143 | .node-circle { 144 | min-width: 12px; 145 | height: 12px; 146 | pointer-events: none; 147 | } 148 | 149 | .node-circle circle { 150 | fill: var(--vscode-charts-blue); 151 | stroke: var(--vscode-charts-blue); 152 | } 153 | 154 | .node-circle .heart-path { 155 | fill: whitesmoke; 156 | } 157 | 158 | .node-circle .diamond-path { 159 | fill: var(--vscode-charts-blue); 160 | } 161 | 162 | .node-content { 163 | display: flex; 164 | align-items: center; 165 | gap: 8px; 166 | flex: 1; 167 | } 168 | 169 | /* Add dimming effects for diamond paths */ 170 | .node-circle.dimmed .diamond-path { 171 | opacity: 0.1; 172 | } 173 | 174 | .node-circle.highlighted .diamond-path { 175 | opacity: 1; 176 | } -------------------------------------------------------------------------------- /syntaxes/jj-commit.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JJ Commit Message", 3 | "scopeName": "text.jj-commit", 4 | "patterns": [ 5 | { 6 | "comment": "User supplied message", 7 | "name": "meta.scope.message.jj-commit", 8 | "begin": "^(?!JJ:)", 9 | "end": "^(?=JJ:)", 10 | "patterns": [ 11 | { 12 | "comment": "Mark > 50 lines as deprecated, > 72 as illegal", 13 | "name": "meta.scope.subject.jj-commit", 14 | "match": "\\G.{0,50}(.{0,22}(.*))$", 15 | "captures": { 16 | "1": { 17 | "name": "invalid.deprecated.line-too-long.jj-commit" 18 | }, 19 | "2": { 20 | "name": "invalid.illegal.line-too-long.jj-commit" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | { 27 | "comment": "JJ supplied metadata in a number of lines starting with JJ:", 28 | "name": "meta.scope.metadata.jj-commit", 29 | "begin": "^(?=JJ:)", 30 | "contentName": "comment.line.indicator.jj-commit", 31 | "end": "^(?!JJ:)", 32 | "patterns": [ 33 | { 34 | "match": "^JJ:\\s+((M|R) .*)$", 35 | "captures": { 36 | "1": { 37 | "name": "markup.changed.jj-commit" 38 | } 39 | } 40 | }, 41 | { 42 | "match": "^JJ:\\s+(A .*)$", 43 | "captures": { 44 | "1": { 45 | "name": "markup.inserted.jj-commit" 46 | } 47 | } 48 | }, 49 | { 50 | "match": "^JJ:\\s+(D .*)$", 51 | "captures": { 52 | "1": { 53 | "name": "markup.deleted.jj-commit" 54 | } 55 | } 56 | } 57 | ] 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Preserve", 4 | "target": "ES2022", 5 | "lib": ["ES2022"], 6 | "sourceMap": true, 7 | "rootDir": "src", 8 | "strict": true /* enable all strict type-checking options */, 9 | "outDir": "./dist", 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | "moduleResolution": "bundler", 15 | "skipLibCheck": true /* Needed to avoid error in lru-cache v10 https://github.com/isaacs/node-lru-cache/issues/354 */ 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist", "out", ".vscode-test"] 19 | } 20 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | - This folder contains all of the files necessary for your extension. 6 | - `package.json` - this is the manifest file in which you declare your extension and command. 7 | - The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | - `src/main.ts` - this is the main file where you will provide the implementation of your command. 9 | - The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | - We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | - install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) 15 | 16 | ## Get up and running straight away 17 | 18 | - Press `F5` to open a new window with your extension loaded. 19 | - Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 20 | - Set breakpoints in your code inside `src/main.ts` to debug your extension. 21 | - Find output from your extension in the debug console. 22 | 23 | ## Make changes 24 | 25 | - You can relaunch the extension from the debug toolbar after changing code in `src/main.ts`. 26 | - You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 27 | 28 | ## Explore the API 29 | 30 | - You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 31 | 32 | ## Run tests 33 | 34 | - Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 35 | - Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 36 | - Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 37 | - See the output of the test result in the Test Results view. 38 | - Make changes to `src/test/main.test.ts` or create new test files inside the `test` folder. 39 | - The provided test runner will only consider files matching the name pattern `**.test.ts`. 40 | - You can create folders inside the `test` folder to structure your tests any way you want. 41 | 42 | ## Go further 43 | 44 | - Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 45 | - [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 46 | - Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 47 | --------------------------------------------------------------------------------