├── .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 | 
4 |
5 | > A Visual Studio Code extension for the [Jujutsu (jj) version control system](https://github.com/jj-vcs/jj).
6 |
7 | [](https://marketplace.visualstudio.com/items?itemName=jjk.jjk)
8 | [](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 | 
22 | - View line-by-line blame
23 |
24 |
25 | ### 💫 Change Management
26 |
27 | - Create new changes with optional descriptions
28 | - Edit descriptions for Working Copy and parent changes
29 | 
30 | - Move changes between Working Copy and parents
31 | 
32 | - Move specific lines from the Working Copy to its parent changes
33 | 
34 | - Discard changes
35 | 
36 | - Browse and navigate revision history
37 |
38 | - Create merge changes
39 |
40 |
41 | ### 🔄 Operation Management
42 |
43 | - Undo jj operations or restore to a previous state
44 |
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 |
--------------------------------------------------------------------------------