diff --git a/.config/guardian/.gdnbaselines b/.config/guardian/.gdnbaselines index 063d926b6ba..e8f9f8db2f7 100644 --- a/.config/guardian/.gdnbaselines +++ b/.config/guardian/.gdnbaselines @@ -296,21 +296,6 @@ "expirationDate": "2025-11-19 21:48:17Z", "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" }, - "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8": { - "signature": "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8", - "alternativeSignatures": [ - "07746898f43afab7cc50931b33154c2d9e1a35f82a649dbe8aecf785b3d5a813" - ], - "target": "file:///D:/a/_work/1/vscode-server-win32-x64/node_modules/@vscode/vsce-sign/bin/vsce-sign.exe", - "memberOf": [ - "default" - ], - "tool": "binskim", - "ruleId": "BA2008", - "createdDate": "2025-06-02 21:46:49Z", - "expirationDate": "2025-11-19 21:48:17Z", - "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" - }, "1d4a48ebc63e3b652146bc16309b2d960a7168d299c7ac94cf794347c06265ef": { "signature": "1d4a48ebc63e3b652146bc16309b2d960a7168d299c7ac94cf794347c06265ef", "alternativeSignatures": [ @@ -326,21 +311,6 @@ "expirationDate": "2025-11-19 21:48:17Z", "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" }, - "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624": { - "signature": "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624", - "alternativeSignatures": [ - "4a6cb67bd4b401e9669c13a2162660aaefc0a94a4122e5b50c198414db545672" - ], - "target": "file:///D:/a/_work/1/vscode-server-win32-x64-web/node_modules/@vscode/vsce-sign/bin/vsce-sign.exe", - "memberOf": [ - "default" - ], - "tool": "binskim", - "ruleId": "BA2008", - "createdDate": "2025-06-02 21:46:49Z", - "expirationDate": "2025-11-19 21:48:17Z", - "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" - }, "21b8091cf937b1be55c7a300483182fec206bc0cd8e2666727b29c8c200aa101": { "signature": "21b8091cf937b1be55c7a300483182fec206bc0cd8e2666727b29c8c200aa101", "alternativeSignatures": [ @@ -416,21 +386,6 @@ "expirationDate": "2025-11-19 21:48:17Z", "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" }, - "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad": { - "signature": "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad", - "alternativeSignatures": [ - "b7b9eb974d7d3a4ae14df8695ca5a62592c8c9d20b7eda70a6535d50cbda3e7f" - ], - "target": "file:///D:/a/_work/1/VSCode-win32-x64/resources/app/node_modules/@vscode/vsce-sign/bin/vsce-sign.exe", - "memberOf": [ - "default" - ], - "tool": "binskim", - "ruleId": "BA2008", - "createdDate": "2025-06-02 21:46:49Z", - "expirationDate": "2025-11-19 21:48:17Z", - "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" - }, "d23a7cc83e649f9a9c5831255cb7569d363799adb5490ff7e299685ea7cf5000": { "signature": "d23a7cc83e649f9a9c5831255cb7569d363799adb5490ff7e299685ea7cf5000", "alternativeSignatures": [ @@ -462,4 +417,4 @@ "justification": "This error is baselined with an expiration date of 180 days from 2025-06-02 21:48:17Z" } } -} \ No newline at end of file +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ce9e17c1c96..ad1f2b23812 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,12 @@ --- name: Bug report about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + --- + @@ -19,4 +24,4 @@ Does this issue occur when all extensions are disabled?: Yes/No Steps to Reproduce: 1. -2. +2. diff --git a/.github/ISSUE_TEMPLATE/copilot_bug_report.md b/.github/ISSUE_TEMPLATE/copilot_bug_report.md new file mode 100644 index 00000000000..9a77481a8c6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/copilot_bug_report.md @@ -0,0 +1,23 @@ +--- +name: Copilot Bug report +about: Create a report to help us improve Copilot's chat interface in VS Code +title: '' +labels: chat-ext-issue +assignees: '' + +--- + + + + +- Copilot Chat Extension Version: +- VS Code Version: +- OS Version: +- Feature (e.g. agent/edit/ask mode): +- Selected model (e.g. GPT 4.1, Claude 3.7 Sonnet): +- Logs: + +Steps to Reproduce: + +1. +2. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b9c6c83caa3..4e2639b9c6f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,9 @@ --- name: Feature request about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' --- diff --git a/.github/chatmodes/learn.chatmode.md b/.github/chatmodes/learn.chatmode.md deleted file mode 100644 index debbed0e56e..00000000000 --- a/.github/chatmodes/learn.chatmode.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: 'Save learnings from conversation' -tools: ['codebase', 'editFiles', 'githubRepo', 'runCommands', 'search', 'searchResults', 'usages'] ---- -Please take a moment and deeply reflect on all the steps you took and think if there would have been a piece of information which would have allowed you to work faster (take less steps). - -The file .vscode/project.instructions.md has been already provided to you. Edit the file such that it would contain information which would have made you work faster. Please don't make this too specific to this task, but rather something that is generic but useful enought to ensure speed-ups in the future for other tasks. diff --git a/.github/chatmodes/plan.chatmode.md b/.github/chatmodes/plan.chatmode.md deleted file mode 100644 index 44b5a4e902d..00000000000 --- a/.github/chatmodes/plan.chatmode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -description: 'Plan the solution for a problem.' -tools: ['codebase', 'findTestFiles', 'githubRepo', 'search', 'searchResults', 'usages'] ---- -I need your help with the following problem. Please take a look, understand the request in depth, and if the request makes sense, research it, understand the existing code, then suggest a clear plan with steps to take to address the request. diff --git a/.github/classifier.json b/.github/classifier.json index c941f85491e..be82395f598 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -54,8 +54,8 @@ "editor-hover": {"assign": ["aiday-mar"]}, "editor-indent-detection": {"assign": ["alexdima"]}, "editor-indent-guides": {"assign": ["hediet"]}, - "editor-input": {"assign": ["alexdima"]}, - "editor-input-IME": {"assign": ["alexdima"]}, + "editor-input": {"assign": ["aiday-mar"]}, + "editor-input-IME": {"assign": ["aiday-mar"]}, "editor-insets": {"assign": ["jrieken"]}, "editor-minimap": {"assign": ["alexdima"]}, "editor-multicursor": {"assign": ["alexdima"]}, @@ -75,7 +75,7 @@ "emmet": {"assign": ["rzhao271"]}, "emmet-parse": {"assign": ["rzhao271"]}, "error-list": {"assign": ["sandy081"]}, - "extension-activation": {"assign": ["joyceerhl", "alexdima"]}, + "extension-activation": {"assign": ["alexdima"]}, "extension-host": {"assign": ["alexdima"]}, "extension-prerelease": {"assign": ["sandy081"]}, "extension-recommendations": {"assign": ["sandy081"]}, @@ -263,7 +263,7 @@ "user-profiles": {"assign": ["sandy081"]}, "ux": {"assign": ["daviddossett"]}, "variable-resolving": {"assign": ["alexr00"]}, - "VIM": {"assign": ["alexdima", "rebornix"]}, + "VIM": {"assign": ["rebornix"]}, "virtual-workspaces": {"assign": []}, "vscode.dev": {"assign": []}, "vscode-build": {"assign": []}, diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6f20c66c400..bf7d42ba5a3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,57 +1,116 @@ -# Coding Guidelines +# VS Code Copilot Instructions -## Introduction +## Project Overview -These are VS Code coding guidelines. Please also review our [Source Code Organisation](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) page. +Visual Studio Code is built with a layered architecture using TypeScript, web APIs and Electron, combining web technologies with native app capabilities. The codebase is organized into key architectural layers: -## Indentation +### Root Folders +- `src/`: Main TypeScript source code with unit tests in `src/vs/*/test/` folders +- `build/`: Build scripts and CI/CD tools +- `extensions/`: Built-in extensions that ship with VS Code +- `test/`: Integration tests and test infrastructure +- `scripts/`: Development and build scripts +- `resources/`: Static resources (icons, themes, etc.) +- `out/`: Compiled JavaScript output (generated during build) + +### Core Architecture (`src/` folder) +- `src/vs/base/` - Foundation utilities and cross-platform abstractions +- `src/vs/platform/` - Platform services and dependency injection infrastructure +- `src/vs/editor/` - Text editor implementation with language services, syntax highlighting, and editing features +- `src/vs/workbench/` - Main application workbench for web and desktop + - `workbench/browser/` - Core workbench UI components (parts, layout, actions) + - `workbench/services/` - Service implementations + - `workbench/contrib/` - Feature contributions (git, debug, search, terminal, etc.) + - `workbench/api/` - Extension host and VS Code API implementation +- `src/vs/code/` - Electron main process specific implementation +- `src/vs/server/` - Server specific implementation + +The core architecture follows these principles: +- **Layered architecture** - from `base`, `platform`, `editor`, to `workbench` +- **Dependency injection** - Services are injected through constructor parameters +- **Contribution model** - Features contribute to registries and extension points +- **Cross-platform compatibility** - Abstractions separate platform-specific code + +### Built-in Extensions (`extensions/` folder) +The `extensions/` directory contains first-party extensions that ship with VS Code: +- **Language support** - `typescript-language-features/`, `html-language-features/`, `css-language-features/`, etc. +- **Core features** - `git/`, `debug-auto-launch/`, `emmet/`, `markdown-language-features/` +- **Themes** - `theme-*` folders for default color themes +- **Development tools** - `extension-editing/`, `vscode-api-tests/` + +Each extension follows the standard VS Code extension structure with `package.json`, TypeScript sources, and contribution points to extend the workbench through the Extension API. + +### Finding Related Code +1. **Semantic search first**: Use file search for general concepts +2. **Grep for exact strings**: Use grep for error messages or specific function names +3. **Follow imports**: Check what files import the problematic module +4. **Check test files**: Often reveal usage patterns and expected behavior + +## Coding Guidelines + +### Indentation We use tabs, not spaces. -## Naming Conventions +### Naming Conventions -* Use PascalCase for `type` names -* Use PascalCase for `enum` values -* Use camelCase for `function` and `method` names -* Use camelCase for `property` names and `local variables` -* Use whole words in names when possible +- Use PascalCase for `type` names +- Use PascalCase for `enum` values +- Use camelCase for `function` and `method` names +- Use camelCase for `property` names and `local variables` +- Use whole words in names when possible -## Types +### Types -* Do not export `types` or `functions` unless you need to share it across multiple components -* Do not introduce new `types` or `values` to the global namespace +- Do not export `types` or `functions` unless you need to share it across multiple components +- Do not introduce new `types` or `values` to the global namespace -## Comments +### Comments -* When there are comments for `functions`, `interfaces`, `enums`, and `classes` use JSDoc style comments +- Use JSDoc style comments for `functions`, `interfaces`, `enums`, and `classes` -## Strings +### Strings -* Use "double quotes" for strings shown to the user that need to be externalized (localized) -* Use 'single quotes' otherwise -* All strings visible to the user need to be externalized +- Use "double quotes" for strings shown to the user that need to be externalized (localized) +- Use 'single quotes' otherwise +- All strings visible to the user need to be externalized -## Style +### UI labels +- Use title-style capitalization for command labels, buttons and menu items (each word is capitalized). +- Don't capitalize prepositions of four or fewer letters unless it's the first or last word (e.g. "in", "with", "for"). -* Use arrow functions `=>` over anonymous function expressions -* Only surround arrow function parameters when necessary. For example, `(x) => x + x` is wrong but the following are correct: +### Style -```javascript +- Use arrow functions `=>` over anonymous function expressions +- Only surround arrow function parameters when necessary. For example, `(x) => x + x` is wrong but the following are correct: + +```typescript x => x + x (x, y) => x + y (x: T, y: T) => x === y ``` -* Always surround loop and conditional bodies with curly braces -* Open curly braces always go on the same line as whatever necessitates them -* Parenthesized constructs should have no surrounding whitespace. A single space follows commas, colons, and semicolons in those constructs. For example: +- Always surround loop and conditional bodies with curly braces +- Open curly braces always go on the same line as whatever necessitates them +- Parenthesized constructs should have no surrounding whitespace. A single space follows commas, colons, and semicolons in those constructs. For example: -```javascript +```typescript for (let i = 0, n = str.length; i < 10; i++) { if (x < 10) { foo(); } } - function f(x: number, y: string): void { } ``` + +- Whenever possible, use in top-level scopes `export function x(…) {…}` instead of `export const x = (…) => {…}`. One advantage of using the `function` keyword is that the stack-trace shows a good name when debugging. + +### Code Quality + +- All files must include Microsoft copyright header +- Prefer `async` and `await` over `Promise` and `then` calls +- All user facing messages must be localized using the applicable localization framework (for example `nls.localize()` method) +- Don't add tests to the wrong test suite (e.g., adding to end of file instead of inside relevant suite) +- Look for existing test patterns before creating new structures +- Use `describe` and `test` consistently with existing patterns +- If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md new file mode 100644 index 00000000000..bf7f7f394c5 --- /dev/null +++ b/.github/instructions/typescript.instructions.md @@ -0,0 +1,28 @@ +--- +applyTo: '**/*.ts' +--- + +# VS Code Copilot Development Instructions for TypeScript + +You MUST check compilation output before running ANY script or declaring work complete! + +1. **ALWAYS** check the "Core - Build" task output for compilation errors +2. **ALWAYS** check the "Ext - Build" task output for compilation errors +3. **NEVER** run tests if there are compilation errors +3. **NEVER** use `npm run compile` to compile TypeScript files, always check task output +4. **FIX** all compilation errors before moving forward + +## TypeScript compilation steps + +Typescript compilation errors can be found by running the "Core - Build" and "Ext - Build" tasks: +- **Core - Build**: Compiles the main VS Code TypeScript sources +- **Ext - Build**: Compiles the built-in extensions +- These background tasks may already be running from previous development sessions +- If not already running, start them to get real-time compilation feedback +- The tasks provide incremental compilation, so they will automatically recompile when files change + +## TypeScript validation steps +- Use `scripts/test.sh` (or `scripts\test.bat` on Windows) for unit tests (add `--grep ` to filter tests) +- Use `scripts/test-integration.sh` (or `scripts\test-integration.bat` on Windows) for integration tests +- Use `npm run valid-layers-check` to check for layering issues + diff --git a/.github/prompts/implement.prompt.md b/.github/prompts/implement.prompt.md new file mode 100644 index 00000000000..7f62a7f1cc4 --- /dev/null +++ b/.github/prompts/implement.prompt.md @@ -0,0 +1,10 @@ +--- +mode: agent +description: 'Implement the solution for a problem.' +tools: ['changes', 'codebase', 'editFiles', 'fetch', 'findTestFiles', 'openSimpleBrowser', 'problems', 'readNotebookCellOutput', 'runCommands', 'runNotebooks', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI'] +--- +Please write a high quality, general purpose solution. Implement a solution that works correctly for all valid inputs, not just the test cases. Do not hard-code values or create solutions that only work for specific test inputs. Instead, implement the actual logic that solves the problem generally. + +Focus on understanding the problem requirements and implementing the correct algorithm. Tests are there to verify correctness, not to define the solution. Provide a principled implementation that follows best practices and software design principles. + +If the task is unreasonable or infeasible, or if any of the tests are incorrect, please tell me. The solution should be robust, maintainable, and extendable. diff --git a/.github/prompts/plan.prompt.md b/.github/prompts/plan.prompt.md new file mode 100644 index 00000000000..85b45cdcf2f --- /dev/null +++ b/.github/prompts/plan.prompt.md @@ -0,0 +1,19 @@ +--- +mode: agent +description: 'Plan the solution for a problem.' +tools: ['codebase', 'fetch', 'findTestFiles', 'githubRepo', 'get_issue', 'get_issue_comments', 'get_me', 'search', 'searchResults', 'usages', 'vscodeAPI'] +--- +Your goal is to prepare a detailed plan to fix the bug or add the new feature, for this you first need to: +* Understand the context of the bug or feature by reading the issue description and comments. +* Understand the codebase by reading the relevant instruction files. +* If its a bug, then identify the root cause of the bug, and explain this to the user. + +Based on your above understanding generate a plan to fix the bug or add the new feature. +Ensure the plan consists of a Markdown document that has the following sections: + +* Overview: A brief description of the bug/feature. +* Root Cause: A detailed explanation of the root cause of the bug, including any relevant code snippets or references to the codebase. (only if it's a bug) +* Requirements: A list of requirements to resolve the bug or add the new feature. +* Implementation Steps: A detailed list of steps to implement the bug fix or new feature. + +Remember, do not make any code edits, just generate a plan. Use thinking and reasoning skills to outline the steps needed to achieve the desired outcome. diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index e97f3bbb03a..a6f6912cd39 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -68,7 +68,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 # flipped the default to support legacy linux distros which shouldn't happen @@ -93,6 +93,7 @@ jobs: id: cache-builtin-extensions uses: actions/cache/restore@v4 with: + enableCrossOsArchive: true path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" @@ -100,7 +101,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Transpile client and extensions run: npm run gulp transpile-client-esbuild transpile-extensions @@ -124,7 +125,7 @@ jobs: sleep 5 # optional: add a small delay between retries done env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: 🧪 Run unit tests (Electron) if: ${{ inputs.electron_tests }} diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index b9c24a317d4..323f72348a9 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -79,7 +79,7 @@ jobs: echo "Npm install failed $i, trying again..." done env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -101,7 +101,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -121,6 +121,7 @@ jobs: id: cache-builtin-extensions uses: actions/cache/restore@v4 with: + enableCrossOsArchive: true path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" @@ -128,7 +129,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Transpile client and extensions run: npm run gulp transpile-client-esbuild transpile-extensions @@ -152,7 +153,7 @@ jobs: sleep 5 # optional: add a small delay between retries done env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: 🧪 Run unit tests (Electron) if: ${{ inputs.electron_tests }} diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index fdc8a0901ec..625c63f95ab 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -32,6 +32,10 @@ jobs: path: .build/node_modules_cache key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + - name: Install build tools if: steps.cache-node-modules.outputs.cache-hit != 'true' run: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev @@ -72,6 +76,7 @@ jobs: id: cache-builtin-extensions uses: actions/cache@v4 with: + enableCrossOsArchive: true path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" @@ -108,23 +113,6 @@ jobs: path: .build/node_modules_cache key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" - - name: Install build dependencies - if: steps.cache-node-modules.outputs.cache-hit != 'true' - working-directory: build - run: | - set -e - - for i in {1..5}; do # try 5 times - npm ci && break - if [ $i -eq 5 ]; then - echo "Npm install failed too many times" >&2 - exit 1 - fi - echo "Npm install failed $i, trying again..." - done - env: - GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} - - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index b0fed3bd32c..a4bc311b787 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -78,7 +78,7 @@ jobs: VSCODE_ARCH: ${{ env.VSCODE_ARCH }} ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create node_modules archive if: steps.node-modules-cache.outputs.cache-hit != 'true' @@ -102,6 +102,7 @@ jobs: id: cache-builtin-extensions uses: actions/cache/restore@v4 with: + enableCrossOsArchive: true path: .build/builtInExtensions key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" @@ -109,7 +110,7 @@ jobs: if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Transpile client and extensions shell: pwsh @@ -133,7 +134,7 @@ jobs: } } env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: 🧪 Run unit tests (Electron) if: ${{ inputs.electron_tests }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5e9449c5a5f..b43eff9f41d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -64,7 +64,7 @@ jobs: env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' @@ -84,7 +84,7 @@ jobs: - name: Compile & Hygiene run: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check env: - GITHUB_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.VSCODE_OSS || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} linux-cli-tests: name: Linux diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index ed5ae1cf8a2..7533157d8b6 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"June 2025\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"July 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 814791b1ac5..8bb13f5998a 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"June 2025\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"July 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index d4660d04674..334fd46541b 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"June 2025\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"July 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 68c38b3ca49..710a4398b0e 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"May 2025\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"July 2025\"\n" }, { "kind": 1, @@ -82,7 +82,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS is:open assignee:@me label:triage-needed\n" + "value": "$REPOS is:open assignee:@me label:triage-needed,copilot-triage-needed\n" }, { "kind": 1, diff --git a/.vscode/project.instructions.md b/.vscode/project.instructions.md deleted file mode 100644 index c5ad68bce74..00000000000 --- a/.vscode/project.instructions.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -applyTo: '**' ---- - -# VS Code Copilot Development Guide - -This file contains key information to help AI assistants work more efficiently with the VS Code codebase. - -## Quick Reference for Common Issues - -### Build & Test Workflow -1. **Compile**: `npm run compile` (required before testing code changes) -2. **Run specific tests**: `./scripts/test.sh --grep "pattern"` -3. **Test file location**: `out/` directory contains compiled JavaScript -4. **Extension compilation**: Extensions compile separately and take significant time - -### Code Architecture Patterns - -#### Testing Strategy -- Unit tests in `src/vs/*/test/` directories -- Integration tests in `test/` directory -- Use `npm run compile` before running node-based tests - -## Common Gotchas - -### Module Loading -- Use compiled files from `out/` directory when testing with node -- Import paths: `const { Class } = require('../out/vs/path/to/module.js')` -- ES modules require `.mjs` extension or package.json type modification - -### Test Location -- Don't add tests to the wrong test suite (e.g., adding to end of file instead of inside relevant suite) -- Look for existing test patterns before creating new structures -- Use `describe` and `test` consistently with existing patterns - -## Investigation Shortcuts - -### Finding Related Code -1. **Semantic search first**: Use file search for general concepts -2. **Grep for exact strings**: Use grep for error messages or specific function names -3. **Follow imports**: Check what files import the problematic module -4. **Check test files**: Often reveal usage patterns and expected behavior - -### Build Optimization -- Compilation takes ~2 minutes - do this once at start -- Extensions compile separately - skip if not needed -- Use incremental compilation for faster iteration - -## File Structure Quick Reference - -``` -src/vs/ -├── base/common/ # Core utilities (color.ts, etc.) -├── editor/contrib/ # Editor features -├── platform/ # Platform services -└── workbench/ # Main UI components - -test/ # Integration tests -out/ # Compiled output -scripts/ # Build and test scripts -``` diff --git a/.vscode/sdfsd/test.ipynb b/.vscode/sdfsd/test.ipynb deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/.vscode/settings.json b/.vscode/settings.json index 1651611f6ef..31b1c4af47f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,39 @@ { + // --- Chat --- + // "inlineChat.enableV2": true, + + // --- Editor --- "editor.insertSpaces": false, + "editor.experimental.asyncTokenization": true, + "editor.experimental.asyncTokenizationVerification": true, + "editor.occurrencesHighlightDelay": 0, + // "editor.experimental.preferTreeSitter.typescript": true, + // "editor.experimental.preferTreeSitter.regex": true, + // "editor.experimental.preferTreeSitter.css": true, + + // --- Language Specific --- + "[plaintext]": { + "files.insertFinalNewline": false + }, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.formatOnSave": true + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true, + }, + "[github-issues]": { + "editor.wordWrap": "on" + }, + + // --- Files --- "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, "files.exclude": { ".git": true, ".build": true, @@ -17,26 +50,6 @@ "cglicenses.json": "jsonc", "*.tst": "typescript" }, - "search.exclude": { - "**/node_modules": true, - "cli/target/**": true, - ".build/**": true, - "out/**": true, - "out-build/**": true, - "out-vscode/**": true, - "i18n/**": true, - "extensions/**/dist/**": true, - "extensions/**/out/**": true, - "test/smoke/out/**": true, - "test/automation/out/**": true, - "test/integration/browser/out/**": true, - "src/vs/base/test/common/filters.perf.data.js": true, - "src/vs/base/test/node/uri.perf.data.txt": true, - "src/vs/workbench/api/test/browser/extHostDocumentData.test.perf-data.ts": true, - "src/vs/base/test/node/uri.test.data.txt": true, - "src/vs/editor/test/node/diffing/fixtures/**": true, - "build/loader.min": true - }, "files.readonlyInclude": { "**/node_modules/**/*.*": true, "**/yarn.lock": true, @@ -60,6 +73,102 @@ "build/npm/*.js": true, "build/*.js": true }, + + // --- Search --- + "search.exclude": { + "**/node_modules": true, + "cli/target/**": true, + ".build/**": true, + "out/**": true, + "out-build/**": true, + "out-vscode/**": true, + "i18n/**": true, + "extensions/**/dist/**": true, + "extensions/**/out/**": true, + "test/smoke/out/**": true, + "test/automation/out/**": true, + "test/integration/browser/out/**": true, + "src/vs/base/test/common/filters.perf.data.js": true, + "src/vs/base/test/node/uri.perf.data.txt": true, + "src/vs/workbench/api/test/browser/extHostDocumentData.test.perf-data.ts": true, + "src/vs/base/test/node/uri.test.data.txt": true, + "src/vs/editor/test/node/diffing/fixtures/**": true, + "build/loader.min": true + }, + + // --- TypeScript --- + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.quoteStyle": "single", + "typescript.tsc.autoDetect": "off", + "typescript.preferences.autoImportFileExcludePatterns": [ + "@xterm/xterm", + "@xterm/headless", + "node-pty", + "vscode-notebook-renderer", + "src/vs/workbench/workbench.web.main.internal.ts" + ], + + // --- Languages --- + "json.schemas": [ + { + "fileMatch": [ + "cgmanifest.json" + ], + "url": "https://www.schemastore.org/component-detection-manifest.json", + }, + { + "fileMatch": [ + "cglicenses.json" + ], + "url": "./.vscode/cglicenses.schema.json" + } + ], + "css.format.spaceAroundSelectorSeparator": true, + + // --- Git --- + "git.ignoreLimitWarning": true, + "git.branchProtection": [ + "main", + "distro", + "release/*" + ], + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + "git.branchRandomName.enable": true, + "git.pullBeforeCheckout": true, + "git.mergeEditor": true, + "git.diagnosticsCommitHook.enabled": true, + "git.diagnosticsCommitHook.sources": { + "*": "error", + "ts": "warning", + "eslint": "warning" + }, + + // --- GitHub --- + "githubPullRequests.experimental.createView": true, + "githubPullRequests.assignCreated": "${user}", + "githubPullRequests.defaultMergeMethod": "squash", + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ], + "githubPullRequests.codingAgent.enabled": true, + "githubPullRequests.codingAgent.uiIntegration": true, + + // --- Testing & Debugging --- + "testing.autoRun.mode": "rerun", + "debug.javascript.terminalOptions": { + "outFiles": [ + "${workspaceFolder}/out/**/*.js", + "${workspaceFolder}/build/**/*.js" + ] + }, + "extension-test-runner.debugOptions": { + "outFiles": [ + "${workspaceFolder}/extensions/*/out/**/*.js", + ] + }, + + // --- Coverage --- "lcov.path": [ "./.build/coverage/lcov.info", "./.build/coverage-single/lcov.info" @@ -73,60 +182,15 @@ } } ], - "typescript.tsdk": "node_modules/typescript/lib", + + // --- Tools --- "npm.exclude": "**/extensions/**", + "eslint.useFlatConfig": true, "emmet.excludeLanguages": [], - "typescript.preferences.importModuleSpecifier": "relative", - "typescript.preferences.quoteStyle": "single", - "json.schemas": [ - { - "fileMatch": [ - "cgmanifest.json" - ], - "url": "https://json.schemastore.org/component-detection-manifest.json", - }, - { - "fileMatch": [ - "cglicenses.json" - ], - "url": "./.vscode/cglicenses.schema.json" - } - ], - "git.ignoreLimitWarning": true, - "git.branchProtection": [ - "main", - "distro", - "release/*" - ], - "git.branchProtectionPrompt": "alwaysCommitToNewBranch", - "git.branchRandomName.enable": true, - "git.pullBeforeCheckout": true, - "git.mergeEditor": true, - "remote.extensionKind": { - "msjsdiag.debugger-for-chrome": "workspace" - }, "gulp.autoDetect": "off", - "files.insertFinalNewline": true, - "[plaintext]": { - "files.insertFinalNewline": false - }, - "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features", - "editor.formatOnSave": true - }, - "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features", - "editor.formatOnSave": true - }, - "[rust]": { - "editor.defaultFormatter": "rust-lang.rust-analyzer", - "editor.formatOnSave": true, - }, "rust-analyzer.linkedProjects": [ "cli/Cargo.toml" ], - "typescript.tsc.autoDetect": "off", - "testing.autoRun.mode": "rerun", "conventionalCommits.scopes": [ "tree", "scm", @@ -137,55 +201,11 @@ "git", "sash" ], - "githubPullRequests.experimental.createView": true, - "debug.javascript.terminalOptions": { - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "${workspaceFolder}/build/**/*.js" - ] + + // --- Workbench --- + "remote.extensionKind": { + "msjsdiag.debugger-for-chrome": "workspace" }, - "extension-test-runner.debugOptions": { - "outFiles": [ - "${workspaceFolder}/extensions/*/out/**/*.js", - ] - }, - "githubPullRequests.assignCreated": "${user}", - "githubPullRequests.defaultMergeMethod": "squash", - "githubPullRequests.ignoredPullRequestBranches": [ - "main" - ], - "application.experimental.rendererProfiling": true, - "editor.experimental.asyncTokenization": true, - "editor.experimental.asyncTokenizationVerification": true, "terminal.integrated.suggest.enabled": true, - "typescript.preferences.autoImportFileExcludePatterns": [ - "@xterm/xterm", - "@xterm/headless", - "node-pty", - "vscode-notebook-renderer", - "src/vs/workbench/workbench.web.main.internal.ts" - ], - "[github-issues]": { - "editor.wordWrap": "on" - }, - "inlineChat.enableV2": true, - "css.format.spaceAroundSelectorSeparator": true, - "eslint.useFlatConfig": true, - "editor.occurrencesHighlightDelay": 0, - // "editor.experimental.preferTreeSitter.typescript": true, - // "editor.experimental.preferTreeSitter.regex": true, - // "editor.experimental.preferTreeSitter.css": true, - "typescript.experimental.expandableHover": true, - "git.diagnosticsCommitHook.enabled": true, - "git.diagnosticsCommitHook.sources": { - "*": "error", - "ts": "warning", - "eslint": "warning" - }, - "chat.instructionsFilesLocations": { - ".github/instructions": true, - ".vscode": true - }, - "githubPullRequests.codingAgent.enabled": true, - "githubPullRequests.codingAgent.uiIntegration": true + "application.experimental.rendererProfiling": true } diff --git a/build/azure-pipelines/product-build-pr.yml b/build/azure-pipelines/product-build-pr.yml index e851856eb12..f7aff453fec 100644 --- a/build/azure-pipelines/product-build-pr.yml +++ b/build/azure-pipelines/product-build-pr.yml @@ -1,10 +1,9 @@ trigger: - - main - release/* pr: branches: - include: ["main", "release/*"] + include: ["release/*"] variables: - name: Codeql.SkipTaskAutoInjection diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 571e877947c..73f2b8f977e 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -99,6 +99,17 @@ steps: timeoutInMinutes: 20 - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + # Copy client, server and web builds to a separate test directory, to avoid Access Denied errors in codesign + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $TestDir = "$(agent.builddirectory)\test" + New-Item -ItemType Directory -Path $TestDir -Force + Copy-Item -Path "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" -Destination "$TestDir\VSCode-win32-$(VSCODE_ARCH)" -Recurse -Force + Copy-Item -Path "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)" -Destination "$TestDir\vscode-server-win32-$(VSCODE_ARCH)" -Recurse -Force + Copy-Item -Path "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)-web" -Destination "$TestDir\vscode-server-win32-$(VSCODE_ARCH)-web" -Recurse -Force + displayName: Copy builds to test directory + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - powershell: | # Figure out the full absolute path of the product we just built @@ -106,11 +117,11 @@ steps: # to run with these builds instead of running out of sources. . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppRoot = "$(agent.builddirectory)\test\VSCode-win32-$(VSCODE_ARCH)" $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json $AppNameShort = $AppProductJson.nameShort $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH)" exec { .\scripts\test-integration.bat --build --tfs "Integration Tests" } displayName: 🧪 Run integration tests (Electron) timeoutInMinutes: 20 @@ -119,7 +130,7 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)-web" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH)-web" exec { .\scripts\test-web-integration.bat --browser firefox } displayName: 🧪 Run integration tests (Browser, Firefox) timeoutInMinutes: 20 @@ -128,11 +139,11 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppRoot = "$(agent.builddirectory)\test\VSCode-win32-$(VSCODE_ARCH)" $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json $AppNameShort = $AppProductJson.nameShort $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH)" exec { .\scripts\test-remote-integration.bat } displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 @@ -164,7 +175,7 @@ steps: - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 - - powershell: npm run smoketest-no-compile -- -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + - powershell: npm run smoketest-no-compile -- -- --verbose --tracing --build "$(agent.builddirectory)\test\VSCode-win32-$(VSCODE_ARCH)" displayName: 🧪 Run smoke tests (Electron) timeoutInMinutes: 20 @@ -172,15 +183,15 @@ steps: # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 - powershell: npm run smoketest-no-compile -- -- --web --tracing --headless env: - VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)-web + VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH)-web displayName: 🧪 Run smoke tests (Browser, Chromium) timeoutInMinutes: 20 - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: # Additional "--" needed to workaround https://github.com/npm/cli/issues/7375 - - powershell: npm run smoketest-no-compile -- -- --tracing --remote --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + - powershell: npm run smoketest-no-compile -- -- --tracing --remote --build "$(agent.builddirectory)\test\VSCode-win32-$(VSCODE_ARCH)" env: - VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH) + VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH) displayName: 🧪 Run smoke tests (Remote) timeoutInMinutes: 20 diff --git a/build/darwin/sign.js b/build/darwin/sign.js index dff30fd0e18..67656380dd5 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -108,8 +108,16 @@ async function main(buildDir) { await electron_osx_sign_1.default.signAsync(appOpts); } if (require.main === module) { - main(process.argv[2]).catch(err => { + main(process.argv[2]).catch(async (err) => { console.error(err); + const identities = await (0, cross_spawn_promise_1.spawn)('security', ['find-identity', '-p', 'codesigning', '-v']); + console.error(`Available identities:\n${identities}`); + const tempDir = process.env['AGENT_TEMPDIRECTORY']; + if (tempDir) { + const keychain = path_1.default.join(tempDir, 'buildagent.keychain'); + const dump = await (0, cross_spawn_promise_1.spawn)('security', ['dump-keychain', keychain]); + console.error(`Keychain dump:\n${dump}`); + } process.exit(1); }); } diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index ecf162743ef..74772d991bd 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -118,8 +118,16 @@ async function main(buildDir?: string): Promise { } if (require.main === module) { - main(process.argv[2]).catch(err => { + main(process.argv[2]).catch(async err => { console.error(err); + const identities = await spawn('security', ['find-identity', '-p', 'codesigning', '-v']); + console.error(`Available identities:\n${identities}`); + const tempDir = process.env['AGENT_TEMPDIRECTORY']; + if (tempDir) { + const keychain = path.join(tempDir, 'buildagent.keychain'); + const dump = await spawn('security', ['dump-keychain', keychain]); + console.error(`Keychain dump:\n${dump}`); + } process.exit(1); }); } diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 98175f530dd..4a9a644d00d 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -115,6 +115,7 @@ function buildWin32Setup(arch, target) { if (quality === 'insider') { definitions['AppxPackage'] = `code_insiders_explorer_${arch}.appx`; definitions['AppxPackageFullname'] = `Microsoft.${product.win32RegValueName}_1.0.0.0_neutral__8wekyb3d8bbwe`; + definitions['AppxPackageName'] = `Microsoft.${product.win32RegValueName}`; } packageInnoSetup(issPath, { definitions }, cb); diff --git a/build/package-lock.json b/build/package-lock.json index c95428de38e..0c15a8215c8 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -60,8 +60,7 @@ "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", - "yauzl": "^2.10.0", - "zx": "8.5.0" + "yauzl": "^2.10.0" }, "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", @@ -4664,19 +4663,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zx": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/zx/-/zx-8.5.0.tgz", - "integrity": "sha512-XS5/oKOQxKNfG2sVO6TQQjZF5RqWGE5QGSUOCZZVTnvYr3RDBTdbX3IFmV9CrnycCAQWcY0hAD3DDUa4RJE4+w==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "zx": "build/cli.js" - }, - "engines": { - "node": ">= 12.17.0" - } } } } diff --git a/build/package.json b/build/package.json index 6fa10f8a1c6..6e2fca698ff 100644 --- a/build/package.json +++ b/build/package.json @@ -54,8 +54,7 @@ "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", - "yauzl": "^2.10.0", - "zx": "8.5.0" + "yauzl": "^2.10.0" }, "type": "commonjs", "scripts": { diff --git a/build/win32/code.iss b/build/win32/code.iss index cad6e399f66..1a48e7421bf 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -107,11 +107,6 @@ Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; File Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent -#ifdef AppxPackageFullname -[UninstallRun] -Filename: "powershell.exe"; Parameters: "Invoke-Command -ScriptBlock {{Remove-AppxPackage -Package ""{#AppxPackageFullname}""}"; Check: IsWindows11OrLater and QualityIsInsiders; Flags: shellexec waituntilterminated runhidden -#endif - [Registry] #if "user" == InstallTarget #define SoftwareClassesRootKey "HKCU" @@ -1472,16 +1467,36 @@ begin end; #ifdef AppxPackageFullname +var + Line: String; + +procedure ExecAndGetFirstLineLog(const S: String; const Error, FirstLine: Boolean); +begin + if not Error and (Line = '') and (Trim(S) <> '') then + Line := S; + Log(S); +end; + +function AppxPackageInstalled(var ResultCode: Integer): Boolean; +begin + Line := ''; + try + ExecAndLogOutput('powershell.exe', '-Command ' + AddQuotes('Get-AppxPackage -Name ''{#AppxPackageName}'''), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, @ExecAndGetFirstLineLog); + except + Log(GetExceptionMessage); + end; + if (Line <> '') then + Result := True + else + Result := False +end; + procedure AddAppxPackage(); var AddAppxPackageResultCode: Integer; begin - if WizardIsTaskSelected('addcontextmenufiles') then begin + if not AppxPackageInstalled(AddAppxPackageResultCode) and WizardIsTaskSelected('addcontextmenufiles') then begin ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\Drive\shell\{#RegValueName}'); end; end; @@ -1489,9 +1504,8 @@ procedure RemoveAppxPackage(); var RemoveAppxPackageResultCode: Integer; begin - ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Remove-AppxPackage -Package ''{#AppxPackageFullname}'''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); - if not WizardIsTaskSelected('addcontextmenufiles') then begin - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\{#RegValueName}ContextMenu'); + if AppxPackageInstalled(RemoveAppxPackageResultCode) then begin + ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Remove-AppxPackage -Package ''{#AppxPackageFullname}'''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); end; end; #endif @@ -1503,6 +1517,17 @@ var begin if CurStep = ssPostInstall then begin +#ifdef AppxPackageFullname + if not WizardIsTaskSelected('addcontextmenufiles') then begin + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\{#RegValueName}ContextMenu'); + end else begin + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\Drive\shell\{#RegValueName}'); + end; +#endif + if IsBackgroundUpdate() then begin CreateMutex('{#AppMutex}-ready'); @@ -1581,10 +1606,16 @@ var Parts: TArrayOfString; NewPath: string; i: Integer; + ResultCode: Integer; begin if not CurUninstallStep = usUninstall then begin exit; end; +#ifdef AppxPackageFullname + if AppxPackageInstalled(ResultCode) then begin + RemoveAppxPackage(); + end; +#endif if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', Path) then begin exit; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 52020627ca5..52c5af6d7d4 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -216,6 +216,9 @@ pub struct ServeWebArgs { /// Specifies the directory that server data is kept in. #[clap(long)] pub server_data_dir: Option, + /// Use a specific commit SHA for the client. + #[clap(long)] + pub commit_id: Option, } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 2ddefe13083..a8b09b82838 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -88,8 +88,10 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result = ConnectionManager::new(&ctx, platform, args.clone()); let update_check_interval = 3600; - cm.clone() - .start_update_checker(Duration::from_secs(update_check_interval)); + if args.commit_id.is_none() { + cm.clone() + .start_update_checker(Duration::from_secs(update_check_interval)); + } let key = get_server_key_half(&ctx.paths); let make_svc = move || { @@ -629,6 +631,22 @@ impl ConnectionManager { Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) })?; + if let Some(commit) = &self.args.commit_id { + let release = Release { + name: commit.to_string(), + commit: commit.to_string(), + platform: self.platform, + target: target_kind, + quality, + }; + debug!( + self.log, + "using provided commit instead of latest release: {}", release + ); + *latest = Some((now, release.clone())); + return Ok(release); + } + let release = self .update_service .get_latest_commit(self.platform, target_kind, quality) diff --git a/extensions/git/package.json b/extensions/git/package.json index c85b212746c..6b4b3c42570 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -28,6 +28,7 @@ "scmActionButton", "scmHistoryProvider", "scmMultiDiffEditor", + "scmProviderOptions", "scmSelectedProvider", "scmTextDocument", "scmValidation", @@ -538,6 +539,18 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.createWorktree", + "title": "%command.createWorktree%", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.deleteWorktree", + "title": "%command.deleteWorktree%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.graph.deleteTag", "title": "%command.graphDeleteTag%", @@ -1288,6 +1301,14 @@ "command": "git.deleteTag", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.createWorktree", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, + { + "command": "git.deleteWorktree", + "when": "false" + }, { "command": "git.deleteRemoteTag", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1608,6 +1629,11 @@ "group": "2_main@7", "when": "scmProvider == git" }, + { + "submenu": "git.worktrees", + "group": "2_main@8", + "when": "scmProvider == git" + }, { "command": "git.showOutput", "group": "3_footer", @@ -2537,6 +2563,18 @@ "command": "git.deleteRemoteTag", "group": "tags@3" } + ], + "git.worktrees": [ + { + "when": "scmProviderContext == repository", + "command": "git.createWorktree", + "group": "worktrees@1" + }, + { + "when": "scmProviderContext == worktree", + "command": "git.deleteWorktree", + "group": "worktrees@2" + } ] }, "submenus": [ @@ -2567,6 +2605,10 @@ { "id": "git.tags", "label": "%submenu.tags%" + }, + { + "id": "git.worktrees", + "label": "%submenu.worktrees%" } ], "configuration": { @@ -2976,6 +3018,18 @@ "default": 10, "description": "%config.detectSubmodulesLimit%" }, + "git.detectWorktrees": { + "type": "boolean", + "scope": "resource", + "default": false, + "description": "%config.detectWorktrees%" + }, + "git.detectWorktreesLimit": { + "type": "number", + "scope": "resource", + "default": 10, + "description": "%config.detectWorktreesLimit%" + }, "git.alwaysShowStagedChangesResourceGroup": { "type": "boolean", "scope": "resource", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 403f704e2f6..167f90969b6 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -75,6 +75,8 @@ "command.rebase": "Rebase Branch...", "command.createTag": "Create Tag...", "command.deleteTag": "Delete Tag...", + "command.createWorktree": "Create Worktree...", + "command.deleteWorktree": "Delete Worktree", "command.deleteRemoteTag": "Delete Remote Tag...", "command.fetch": "Fetch", "command.fetchPrune": "Fetch (Prune)", @@ -217,6 +219,8 @@ "config.inputValidationSubjectLength": "Controls the commit message subject length threshold for showing a warning. Unset it to inherit the value of `#git.inputValidationLength#`.", "config.detectSubmodules": "Controls whether to automatically detect Git submodules.", "config.detectSubmodulesLimit": "Controls the limit of Git submodules detected.", + "config.detectWorktrees": "Controls whether to automatically detect Git worktrees.", + "config.detectWorktreesLimit": "Controls the limit of Git worktrees detected.", "config.alwaysShowStagedChangesResourceGroup": "Always show the Staged Changes resource group.", "config.alwaysSignOff": "Controls the signoff flag for all commits.", "config.ignoreSubmodules": "Ignore modifications to submodules in the file tree.", @@ -301,6 +305,7 @@ "submenu.remotes": "Remote", "submenu.stash": "Stash", "submenu.tags": "Tags", + "submenu.worktrees": "Worktrees", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.stageModified": "Color for modified resources which have been staged.", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index c25dd0b7aa1..0891d443306 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -440,5 +440,6 @@ export const enum GitErrorCodes { BranchNotYetBorn = 'BranchNotYetBorn', TagConflict = 'TagConflict', CherryPickEmpty = 'CherryPickEmpty', - CherryPickConflict = 'CherryPickConflict' + CherryPickConflict = 'CherryPickConflict', + WorktreeContainsChanges = 'WorktreeContainsChanges' } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index fc4193392b8..8fa1f2c5146 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3315,6 +3315,111 @@ export class CommandCenter { } } + @command('git.createWorktree', { repository: true }) + async createWorktree(repository: Repository): Promise { + await this._createWorktree(repository, undefined, undefined); + } + + private async _createWorktree(repository: Repository, worktreePath?: string, name?: string): Promise { + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + + if (!name) { + const getBranchPicks = async () => { + const refs = await repository.getRefs({ + pattern: 'refs/heads', + includeCommitDetails: showRefDetails + }); + const processors = [new RefProcessor(RefType.Head, BranchItem)]; + const itemsProcessor = new RefItemsProcessor(repository, processors); + return itemsProcessor.processRefs(refs); + }; + + const placeHolder = l10n.t('Select a branch to create the new worktree from'); + const choice = await this.pickRef(getBranchPicks(), placeHolder); + + if (!(choice instanceof BranchItem) || !choice.refName) { + return; + } + name = choice.refName; + } + + const disposables: Disposable[] = []; + const inputBox = window.createInputBox(); + inputBox.placeholder = l10n.t('Worktree name'); + inputBox.prompt = l10n.t('Please provide a worktree name'); + inputBox.value = name || ''; + inputBox.show(); + + const worktreeName = await new Promise((resolve) => { + disposables.push(inputBox.onDidHide(() => resolve(undefined))); + disposables.push(inputBox.onDidAccept(() => resolve(inputBox.value))); + }); + + dispose(disposables); + inputBox.dispose(); + + // Default to view parent directory of repository root + const defaultUri = Uri.file(path.dirname(repository.root)); + + const uris = await window.showOpenDialog({ + defaultUri, + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: l10n.t('Select as Worktree Destination'), + }); + + if (!uris || uris.length === 0) { + return; + } + + if (!worktreeName || worktreeName.trim() === '') { + return; + } + + worktreePath = path.join(uris[0].fsPath, worktreeName); + + await repository.worktree({ + name: name, + path: worktreePath, + }); + } + + @command('git.deleteWorktree', { repository: true }) + async deleteWorktree(repository: Repository): Promise { + if (!repository.dotGit.commonPath) { + return; + } + + const mainRepository = this.model.getRepository(path.dirname(repository.dotGit.commonPath)); + if (!mainRepository) { + return; + } + + // Dispose worktree repository + this.model.disposeRepository(repository); + + try { + await mainRepository.deleteWorktree(repository.root); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.WorktreeContainsChanges) { + const forceDelete = l10n.t('Force Delete'); + const message = l10n.t('The worktree contains modified or untracked files. Do you want to force delete?'); + const choice = await window.showWarningMessage(message, { modal: true }, forceDelete); + if (choice === forceDelete) { + await mainRepository.deleteWorktree(repository.root, { force: true }); + } else { + await this.model.openRepository(repository.root); + } + + return; + } + + throw err; + } + } + @command('git.graph.deleteTag', { repository: true }) async deleteTag2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e5ab4067248..7699b9369cb 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -25,6 +25,12 @@ export interface IGit { version: string; } +export interface IDotGit { + readonly path: string; + readonly commonPath?: string; + readonly superProjectPath?: string; +} + export interface IFileStatus { x: string; y: string; @@ -343,6 +349,8 @@ function getGitErrorCode(stderr: string): string | undefined { return GitErrorCodes.DirtyWorkTree; } else if (/detected dubious ownership in repository at/.test(stderr)) { return GitErrorCodes.NotASafeGitRepository; + } else if (/contains modified or untracked files|use --force to delete it/.test(stderr)) { + return GitErrorCodes.WorktreeContainsChanges; } return undefined; @@ -403,7 +411,7 @@ export class Git { return Versions.compare(Versions.fromString(this.version), Versions.fromString(version)); } - open(repositoryRoot: string, repositoryRootRealPath: string | undefined, dotGit: { path: string; commonPath?: string }, logger: LogOutputChannel): Repository { + open(repositoryRoot: string, repositoryRootRealPath: string | undefined, dotGit: IDotGit, logger: LogOutputChannel): Repository { return new Repository(this, repositoryRoot, repositoryRootRealPath, dotGit, logger); } @@ -539,9 +547,16 @@ export class Git { return repositoryRootPath; } - async getRepositoryDotGit(repositoryPath: string): Promise<{ path: string; commonPath?: string }> { - const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir', '--git-common-dir']); - let [dotGitPath, commonDotGitPath] = result.stdout.split('\n').map(r => r.trim()); + async getRepositoryDotGit(repositoryPath: string): Promise { + let dotGitPath: string | undefined, commonDotGitPath: string | undefined, superProjectPath: string | undefined; + + const args = ['rev-parse', '--git-dir', '--git-common-dir']; + if (this.compareGitVersionTo('2.13.0') >= 0) { + args.push('--show-superproject-working-tree'); + } + + const result = await this.exec(repositoryPath, args); + [dotGitPath, commonDotGitPath, superProjectPath] = result.stdout.split('\n').map(r => r.trim()); if (!path.isAbsolute(dotGitPath)) { dotGitPath = path.join(repositoryPath, dotGitPath); @@ -553,11 +568,13 @@ export class Git { commonDotGitPath = path.join(repositoryPath, commonDotGitPath); } commonDotGitPath = path.normalize(commonDotGitPath); - - return { path: dotGitPath, commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined }; } - return { path: dotGitPath }; + return { + path: dotGitPath, + commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined, + superProjectPath: superProjectPath ? path.normalize(superProjectPath) : undefined + }; } async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise> { @@ -845,6 +862,11 @@ export class GitStatusParser { } } +export interface Worktree { + readonly name: string; + readonly path: string; +} + export interface Submodule { name: string; path: string; @@ -1213,9 +1235,20 @@ export class Repository { private _git: Git, private repositoryRoot: string, private repositoryRootRealPath: string | undefined, - readonly dotGit: { path: string; commonPath?: string }, + readonly dotGit: IDotGit, private logger: LogOutputChannel - ) { } + ) { + this._kind = this.dotGit.commonPath + ? 'worktree' + : this.dotGit.superProjectPath + ? 'submodule' + : 'repository'; + } + + private readonly _kind: 'repository' | 'submodule' | 'worktree'; + get kind(): 'repository' | 'submodule' | 'worktree' { + return this._kind; + } get git(): Git { return this._git; @@ -2002,6 +2035,22 @@ export class Repository { await this.exec(args); } + async worktree(options: { path: string; name: string }): Promise { + const args = ['worktree', 'add', options.path, options.name]; + await this.exec(args); + } + + async deleteWorktree(path: string, options?: { force?: boolean }): Promise { + const args = ['worktree', 'remove']; + + if (options?.force) { + args.push('--force'); + } + + args.push(path); + await this.exec(args); + } + async deleteRemoteRef(remoteName: string, refName: string, options?: { force?: boolean }): Promise { const args = ['push', remoteName, '--delete']; @@ -2714,6 +2763,62 @@ export class Repository { return parseGitStashes(result.stdout.trim()); } + async getWorktrees(): Promise { + return await this.getWorktreesFS(); + } + + private async getWorktreesFS(): Promise { + const config = workspace.getConfiguration('git', Uri.file(this.repositoryRoot)); + const shouldDetectWorktrees = config.get('detectWorktrees') === true; + + if (!shouldDetectWorktrees) { + this.logger.info('[Git][getWorktreesFS] Worktree detection is disabled, skipping worktree detection'); + return []; + } + + if (this.kind !== 'repository') { + this.logger.info('[Git][getWorktreesFS] Either a submodule or a worktree, skipping worktree detection'); + return []; + } + + try { + // List all worktree folder names + const worktreesPath = path.join(this.repositoryRoot, '.git', 'worktrees'); + const dirents = await fs.readdir(worktreesPath, { withFileTypes: true }); + const result: Worktree[] = []; + + for (const dirent of dirents) { + if (!dirent.isDirectory()) { + continue; + } + + const gitdirPath = path.join(worktreesPath, dirent.name, 'gitdir'); + + try { + const gitdirContent = (await fs.readFile(gitdirPath, 'utf8')).trim(); + // Remove trailing '/.git' + const gitdirTrimmed = gitdirContent.replace(/\.git.*$/, ''); + result.push({ name: dirent.name, path: gitdirTrimmed }); + } catch (err) { + if (/ENOENT/.test(err.message)) { + continue; + } + + throw err; + } + } + + return result; + } + catch (err) { + if (/ENOENT/.test(err.message) || /ENOTDIR/.test(err.message)) { + return []; + } + + throw err; + } + } + async getRemotes(): Promise { const remotes: MutableRemote[] = []; diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 74486e6a090..54a1dbb15d3 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -439,7 +439,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi @debounce(500) private eventuallyScanPossibleGitRepositories(): void { for (const path of this.possibleGitRepositoryPaths) { - this.openRepository(path); + this.openRepository(path, false, true); } this.possibleGitRepositoryPaths.clear(); @@ -548,7 +548,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } @sequentialize - async openRepository(repoPath: string, openIfClosed = false): Promise { + async openRepository(repoPath: string, openIfClosed = false, openIfParent = false): Promise { this.logger.trace(`[Model][openRepository] Repository: ${repoPath}`); const existingRepository = await this.getRepositoryExact(repoPath); if (existingRepository) { @@ -597,7 +597,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt'); if (parentRepositoryConfig !== 'always' && this.globalState.get(`parentRepository:${repositoryRoot}`) !== true) { const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot); - if (isRepositoryOutsideWorkspace) { + if (!openIfParent && isRepositoryOutsideWorkspace) { this.logger.trace(`[Model][openRepository] Repository in parent folder: ${repositoryRoot}`); if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) { @@ -635,13 +635,15 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Open repository const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]); - const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); + const gitRepository = this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger); + const repository = new Repository(gitRepository, this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); this.logger.info(`[Model][openRepository] Opened repository (path): ${repository.root}`); this.logger.info(`[Model][openRepository] Opened repository (real path): ${repository.rootRealPath ?? repository.root}`); + this.logger.info(`[Model][openRepository] Opened repository (kind): ${gitRepository.kind}`); // Do not await this, we want SCM // to know about the repo asap @@ -711,6 +713,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed); const disappearListener = onDidDisappearRepository(() => dispose()); + const disposeParentListener = repository.sourceControl.onDidDisposeParent(() => dispose()); const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri })); const originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri })); @@ -722,6 +725,14 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi .getConfiguration('git', Uri.file(repository.root)) .get('detectSubmodulesLimit') as number; + const shouldDetectWorktrees = workspace + .getConfiguration('git', Uri.file(repository.root)) + .get('detectWorktrees') as boolean; + + const worktreesLimit = workspace + .getConfiguration('git', Uri.file(repository.root)) + .get('detectWorktreesLimit') as number; + const checkForSubmodules = () => { if (!shouldDetectSubmodules) { this.logger.trace('[Model][open] Automatic detection of git submodules is not enabled.'); @@ -742,6 +753,25 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi }); }; + const checkForWorktrees = () => { + if (!shouldDetectWorktrees) { + this.logger.trace('[Model][open] Automatic detection of git worktrees is not enabled.'); + return; + } + + if (repository.worktrees.length > worktreesLimit) { + window.showWarningMessage(l10n.t('The "{0}" repository has {1} worktrees which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.worktrees.length)); + statusListener.dispose(); + } + + repository.worktrees + .slice(0, worktreesLimit) + .forEach(w => { + this.logger.trace(`[Model][open] Opening worktree: '${w.path}'`); + this.eventuallyScanPossibleGitRepository(w.path); + }); + }; + const updateMergeChanges = () => { // set mergeChanges context const mergeChanges: Uri[] = []; @@ -755,10 +785,12 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const statusListener = repository.onDidRunGitStatus(() => { checkForSubmodules(); + checkForWorktrees(); updateMergeChanges(); this.onDidChangeActiveTextEditor(); }); checkForSubmodules(); + checkForWorktrees(); this.onDidChangeActiveTextEditor(); const updateOperationInProgressContext = () => { @@ -778,6 +810,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const dispose = () => { disappearListener.dispose(); + disposeParentListener.dispose(); changeListener.dispose(); originalResourceChangeListener.dispose(); statusListener.dispose(); @@ -1107,6 +1140,16 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } + disposeRepository(repository: Repository): void { + const openRepository = this.getOpenRepository(repository); + if (!openRepository) { + return; + } + + this.logger.info(`[Model][disposeRepository] Repository: ${repository.root}`); + openRepository.dispose(); + } + dispose(): void { const openRepositories = [...this.openRepositories]; openRepositories.forEach(r => r.dispose()); diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index d8b2773f1c2..eaa91d4a047 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -22,6 +22,7 @@ export const enum OperationKind { DeleteRef = 'DeleteRef', DeleteRemoteRef = 'DeleteRemoteRef', DeleteTag = 'DeleteTag', + DeleteWorktree = 'DeleteWorktree', Diff = 'Diff', Fetch = 'Fetch', FindTrackingBranches = 'GetTracking', @@ -31,6 +32,7 @@ export const enum OperationKind { GetObjectDetails = 'GetObjectDetails', GetObjectFiles = 'GetObjectFiles', GetRefs = 'GetRefs', + GetWorktrees = 'GetWorktrees', GetRemoteRefs = 'GetRemoteRefs', HashObject = 'HashObject', Ignore = 'Ignore', @@ -62,17 +64,18 @@ export const enum OperationKind { SubmoduleUpdate = 'SubmoduleUpdate', Sync = 'Sync', Tag = 'Tag', + Worktree = 'Worktree' } export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation | CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | - DeleteRefOperation | DeleteRemoteRefOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | - GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | + DeleteRefOperation | DeleteRemoteRefOperation | DeleteTagOperation | DeleteWorktreeOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | + GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | GetWorktreesOperation | GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation | ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation | RevListOperation | RevParseOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | - SubmoduleUpdateOperation | SyncOperation | TagOperation; + SubmoduleUpdateOperation | SyncOperation | TagOperation | WorktreeOperation; type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean }; export type AddOperation = BaseOperation & { kind: OperationKind.Add }; @@ -90,6 +93,7 @@ export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.Delete export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef }; export type DeleteRemoteRefOperation = BaseOperation & { kind: OperationKind.DeleteRemoteRef }; export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag }; +export type DeleteWorktreeOperation = BaseOperation & { kind: OperationKind.DeleteWorktree }; export type DiffOperation = BaseOperation & { kind: OperationKind.Diff }; export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch }; export type FindTrackingBranchesOperation = BaseOperation & { kind: OperationKind.FindTrackingBranches }; @@ -99,6 +103,7 @@ export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.G export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails }; export type GetObjectFilesOperation = BaseOperation & { kind: OperationKind.GetObjectFiles }; export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs }; +export type GetWorktreesOperation = BaseOperation & { kind: OperationKind.GetWorktrees }; export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs }; export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject }; export type IgnoreOperation = BaseOperation & { kind: OperationKind.Ignore }; @@ -130,6 +135,7 @@ export type StashOperation = BaseOperation & { kind: OperationKind.Stash }; export type SubmoduleUpdateOperation = BaseOperation & { kind: OperationKind.SubmoduleUpdate }; export type SyncOperation = BaseOperation & { kind: OperationKind.Sync }; export type TagOperation = BaseOperation & { kind: OperationKind.Tag }; +export type WorktreeOperation = BaseOperation & { kind: OperationKind.Worktree }; export const Operation = { Add: (showProgress: boolean): AddOperation => ({ kind: OperationKind.Add, blocking: false, readOnly: false, remote: false, retry: false, showProgress }), @@ -147,6 +153,7 @@ export const Operation = { DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation, DeleteRemoteRef: { kind: OperationKind.DeleteRemoteRef, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteRefOperation, DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation, + DeleteWorktree: { kind: OperationKind.DeleteWorktree, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteWorktreeOperation, Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as DiffOperation, Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation), FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation, @@ -156,6 +163,7 @@ export const Operation = { GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation, GetObjectFiles: { kind: OperationKind.GetObjectFiles, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectFilesOperation, GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation, + GetWorktrees: { kind: OperationKind.GetWorktrees, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetWorktreesOperation, GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation, HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation, Ignore: { kind: OperationKind.Ignore, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as IgnoreOperation, @@ -186,7 +194,8 @@ export const Operation = { Stash: { kind: OperationKind.Stash, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StashOperation, SubmoduleUpdate: { kind: OperationKind.SubmoduleUpdate, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SubmoduleUpdateOperation, Sync: { kind: OperationKind.Sync, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as SyncOperation, - Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation + Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation, + Worktree: { kind: OperationKind.Worktree, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as WorktreeOperation }; export interface OperationResult { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 111561b665b..ff68d91cb36 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,14 +7,14 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import picomatch from 'picomatch'; -import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git'; import { AutoFetcher } from './autofetch'; import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, sequentialize, throttle } from './decorators'; -import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule } from './git'; +import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git'; import { GitHistoryProvider } from './historyProvider'; import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; @@ -751,6 +751,11 @@ export class Repository implements Disposable { return this._submodules; } + private _worktrees: Worktree[] = []; + get worktrees(): Worktree[] { + return this._worktrees; + } + private _rebaseCommit: Commit | undefined = undefined; set rebaseCommit(rebaseCommit: Commit | undefined) { @@ -886,8 +891,24 @@ export class Repository implements Disposable { this.disposables.push(new FileEventLogger(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange, logger)); + // Parent source control + const parentRoot = repository.kind === 'submodule' + ? repository.dotGit.superProjectPath + : repository.kind === 'worktree' && repository.dotGit.commonPath + ? path.dirname(repository.dotGit.commonPath) + : undefined; + const parent = this.repositoryResolver.getRepository(parentRoot)?.sourceControl; + + // Icon + const icon = repository.kind === 'submodule' + ? new ThemeIcon('archive') + : repository.kind === 'worktree' + ? new ThemeIcon('list-tree') + : new ThemeIcon('repo'); + const root = Uri.file(repository.root); - this._sourceControl = scm.createSourceControl('git', 'Git', root); + this._sourceControl = scm.createSourceControl('git', 'Git', root, icon, parent); + this._sourceControl.contextValue = repository.kind; this._sourceControl.quickDiffProvider = this; this._sourceControl.secondaryQuickDiffProvider = new StagedResourceQuickDiffProvider(this, logger); @@ -1671,6 +1692,10 @@ export class Repository implements Disposable { return await this.run(Operation.GetRefs, () => this.repository.getRefs(query, cancellationToken)); } + async getWorktrees(): Promise { + return await this.run(Operation.GetWorktrees, () => this.repository.getWorktrees()); + } + async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean }): Promise { return await this.run(Operation.GetRemoteRefs, () => this.repository.getRemoteRefs(remote, opts)); } @@ -1699,6 +1724,14 @@ export class Repository implements Disposable { await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name)); } + async worktree(options: { path: string; name: string }): Promise { + await this.run(Operation.Worktree, () => this.repository.worktree(options)); + } + + async deleteWorktree(path: string, options?: { force?: boolean }): Promise { + await this.run(Operation.DeleteWorktree, () => this.repository.deleteWorktree(path, options)); + } + async deleteRemoteRef(remoteName: string, refName: string, options?: { force?: boolean }): Promise { await this.run(Operation.DeleteRemoteRef, () => this.repository.deleteRemoteRef(remoteName, refName, options)); } @@ -2297,11 +2330,12 @@ export class Repository implements Disposable { this._updateResourceGroupsState(optimisticResourcesGroups); } - const [HEAD, remotes, submodules, rebaseCommit, mergeInProgress, cherryPickInProgress, commitTemplate] = + const [HEAD, remotes, submodules, worktrees, rebaseCommit, mergeInProgress, cherryPickInProgress, commitTemplate] = await Promise.all([ this.repository.getHEADRef(), this.repository.getRemotes(), this.repository.getSubmodules(), + this.repository.getWorktrees(), this.getRebaseCommit(), this.isMergeInProgress(), this.isCherryPickInProgress(), @@ -2321,6 +2355,7 @@ export class Repository implements Disposable { this._HEAD = HEAD; this._remotes = remotes!; this._submodules = submodules!; + this._worktrees = worktrees!; this.rebaseCommit = rebaseCommit; this.mergeInProgress = mergeInProgress; this.cherryPickInProgress = cherryPickInProgress; diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index c79be520be9..c1179f34d29 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -17,6 +17,7 @@ "../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.scmProviderOptions.d.ts", "../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts", "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", "../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts", diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 0c470b1d65d..e9954dc2d02 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -571,14 +571,14 @@ export function getFlows(query: IFlowQuery) { */ export const enum GitHubSocialSignInProvider { Google = 'google', - // Apple = 'apple', + Apple = 'apple', } const GitHubSocialSignInProviderLabels = { [GitHubSocialSignInProvider.Google]: l10n.t('Google'), - // [GitHubSocialSignInProvider.Apple]: l10n.t('Apple'), + [GitHubSocialSignInProvider.Apple]: l10n.t('Apple'), }; export function isSocialSignInProvider(provider: unknown): provider is GitHubSocialSignInProvider { - return provider === GitHubSocialSignInProvider.Google; // || provider === GitHubSocialSignInProvider.Apple; + return provider === GitHubSocialSignInProvider.Google || provider === GitHubSocialSignInProvider.Apple; } diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index d57ebf80834..1172d6cbde1 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -158,7 +158,7 @@ async function getSchemaRequestService(context: ExtensionContext, log: LogOutput return { getContent: async (uri: string) => { - if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { + if (cache && /^https?:\/\/(json|www)\.schemastore\.org\//.test(uri)) { const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); if (content) { if (log.logLevel === LogLevel.Trace) { diff --git a/extensions/json-language-features/package-lock.json b/extensions/json-language-features/package-lock.json index 559186572ec..11c7b3a7a91 100644 --- a/extensions/json-language-features/package-lock.json +++ b/extensions/json-language-features/package-lock.json @@ -175,9 +175,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" diff --git a/extensions/json-language-features/server/README.md b/extensions/json-language-features/server/README.md index 10956439e32..1c382916072 100644 --- a/extensions/json-language-features/server/README.md +++ b/extensions/json-language-features/server/README.md @@ -90,7 +90,7 @@ The server supports the following settings: "foo.json", "*.superfoo.json" ], - "url": "http://json.schemastore.org/foo", + "url": "http://www.schemastore.org/foo", "schema": { "type": "array" } diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index 47f3164d798..352ee31ae9f 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -63,9 +63,10 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } diff --git a/extensions/npm/package.json b/extensions/npm/package.json index de458a52069..9a67c3a83ec 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -336,11 +336,11 @@ "jsonValidation": [ { "fileMatch": "package.json", - "url": "https://json.schemastore.org/package" + "url": "https://www.schemastore.org/package" }, { "fileMatch": "bower.json", - "url": "https://json.schemastore.org/bower" + "url": "https://www.schemastore.org/bower" } ], "taskDefinitions": [ diff --git a/extensions/package-lock.json b/extensions/package-lock.json index 6694419fa1b..d94892b0591 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "typescript": "^5.8.3" + "typescript": "^5.9.0-beta" }, "devDependencies": { "@parcel/watcher": "2.5.1", @@ -940,9 +940,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.0-beta", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-beta.tgz", + "integrity": "sha512-p91qoTdwWKj9YEBYavmGiBn0DF4OBElzw4pW4oPbK4HeCfr/SDz9+yviVWshZXGvGvFCJ3AVQ+J7F1UZXc23QQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/extensions/package.json b/extensions/package.json index eb206022d50..17980f357fe 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.8.3" + "typescript": "^5.9.0-beta" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index c57beef6567..e99f1d7a4aa 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -46,7 +46,7 @@ type ShellGlobalsCacheEntryWithMeta = ShellGlobalsCacheEntry & { timestamp: numb const cachedGlobals: Map = new Map(); let pathExecutableCache: PathExecutableCache; const CACHE_KEY = 'terminalSuggestGlobalsCacheV2'; -let globalStorage: vscode.Memento; +let globalStorageUri: vscode.Uri; const CACHE_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 7; // 7 days function getCacheKey(machineId: string, remoteAuthority: string | undefined, shellType: TerminalShellType): string { @@ -150,7 +150,7 @@ async function fetchAndCacheShellGlobals( async function writeGlobalsCache(): Promise { - if (!globalStorage) { + if (!globalStorageUri) { return; } // Remove old entries @@ -165,7 +165,12 @@ async function writeGlobalsCache(): Promise { obj[key] = value; } try { - await globalStorage.update(CACHE_KEY, obj); + // Ensure the directory exists + const terminalSuggestDir = vscode.Uri.joinPath(globalStorageUri, 'terminal-suggest'); + await vscode.workspace.fs.createDirectory(terminalSuggestDir); + const cacheFile = vscode.Uri.joinPath(terminalSuggestDir, `${CACHE_KEY}.json`); + const data = Buffer.from(JSON.stringify(obj), 'utf8'); + await vscode.workspace.fs.writeFile(cacheFile, data); } catch (err) { console.error('Failed to write terminal suggest globals cache:', err); } @@ -173,17 +178,27 @@ async function writeGlobalsCache(): Promise { async function readGlobalsCache(): Promise { - if (!globalStorage) { + if (!globalStorageUri) { return; } try { - const obj = globalStorage.get>(CACHE_KEY); + const terminalSuggestDir = vscode.Uri.joinPath(globalStorageUri, 'terminal-suggest'); + const cacheFile = vscode.Uri.joinPath(terminalSuggestDir, `${CACHE_KEY}.json`); + const data = await vscode.workspace.fs.readFile(cacheFile); + const obj = JSON.parse(data.toString()) as Record; if (obj) { for (const key of Object.keys(obj)) { cachedGlobals.set(key, obj[key]); } } - } catch { } + } catch (err) { + // File might not exist yet, which is expected on first run + if (err instanceof vscode.FileSystemError && err.code === 'FileNotFound') { + // This is expected on first run + return; + } + console.error('Failed to read terminal suggest globals cache:', err); + } } @@ -193,7 +208,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(pathExecutableCache); let currentTerminalEnv: ITerminalEnvironment = process.env; - globalStorage = context.globalState; + globalStorageUri = context.globalStorageUri; await readGlobalsCache(); // Get a machineId for this install (persisted per machine, not synced) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index a087e162080..1de7b92c398 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -81,7 +81,7 @@ }, { "fileMatch": "tsconfig.json", - "url": "https://json.schemastore.org/tsconfig" + "url": "https://www.schemastore.org/tsconfig" }, { "fileMatch": "tsconfig.json", @@ -89,7 +89,7 @@ }, { "fileMatch": "tsconfig.*.json", - "url": "https://json.schemastore.org/tsconfig" + "url": "https://www.schemastore.org/tsconfig" }, { "fileMatch": "tsconfig-*.json", @@ -97,7 +97,7 @@ }, { "fileMatch": "tsconfig-*.json", - "url": "https://json.schemastore.org/tsconfig" + "url": "https://www.schemastore.org/tsconfig" }, { "fileMatch": "tsconfig.*.json", @@ -105,27 +105,27 @@ }, { "fileMatch": "typings.json", - "url": "https://json.schemastore.org/typings" + "url": "https://www.schemastore.org/typings" }, { "fileMatch": ".bowerrc", - "url": "https://json.schemastore.org/bowerrc" + "url": "https://www.schemastore.org/bowerrc" }, { "fileMatch": ".babelrc", - "url": "https://json.schemastore.org/babelrc" + "url": "https://www.schemastore.org/babelrc" }, { "fileMatch": ".babelrc.json", - "url": "https://json.schemastore.org/babelrc" + "url": "https://www.schemastore.org/babelrc" }, { "fileMatch": "babel.config.json", - "url": "https://json.schemastore.org/babelrc" + "url": "https://www.schemastore.org/babelrc" }, { "fileMatch": "jsconfig.json", - "url": "https://json.schemastore.org/jsconfig" + "url": "https://www.schemastore.org/jsconfig" }, { "fileMatch": "jsconfig.json", @@ -133,7 +133,7 @@ }, { "fileMatch": "jsconfig.*.json", - "url": "https://json.schemastore.org/jsconfig" + "url": "https://www.schemastore.org/jsconfig" }, { "fileMatch": "jsconfig.*.json", @@ -1512,7 +1512,8 @@ "off", "terse", "normal", - "verbose" + "verbose", + "requestTime" ], "default": "off", "description": "%typescript.tsserver.log%", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 6a0a2e78beb..881e0cd1ad4 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -12,6 +12,7 @@ export enum TsServerLogLevel { Normal, Terse, Verbose, + RequestTime } export namespace TsServerLogLevel { @@ -23,6 +24,8 @@ export namespace TsServerLogLevel { return TsServerLogLevel.Terse; case 'verbose': return TsServerLogLevel.Verbose; + case 'requestTime': + return TsServerLogLevel.RequestTime; case 'off': default: return TsServerLogLevel.Off; @@ -37,6 +40,8 @@ export namespace TsServerLogLevel { return 'terse'; case TsServerLogLevel.Verbose: return 'verbose'; + case TsServerLogLevel.RequestTime: + return 'requestTime'; case TsServerLogLevel.Off: default: return 'off'; diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index f39740bab23..6cba39e0c0a 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -61,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('5.8.3'))); + API.fromSimpleString('5.9.0'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 63613fcf34a..6da5bb74cd7 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -208,7 +208,6 @@ export default class FileConfigurationManager extends Disposable { includeCompletionsForModuleExports: config.get('suggest.autoImports'), ...getInlayHintsPreferences(config), ...this.getOrganizeImportsPreferences(preferencesConfig), - // @ts-expect-error until TS 5.9 maximumHoverLength: this.getMaximumHoverLength(document), }; diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index ff7719c0986..cc08b7f5b94 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -56,7 +56,6 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { new vscode.VerboseHover( contents, range, - // @ts-expect-error /*canIncreaseVerbosity*/ response.body.canIncreaseVerbosityLevel, /*canDecreaseVerbosity*/ verbosityLevel !== 0 ) : new vscode.Hover( diff --git a/extensions/typescript-language-features/src/test/unit/index.ts b/extensions/typescript-language-features/src/test/unit/index.ts index 16d163fa241..116465a8c85 100644 --- a/extensions/typescript-language-features/src/test/unit/index.ts +++ b/extensions/typescript-language-features/src/test/unit/index.ts @@ -15,14 +15,28 @@ // to report the results back to the caller. When the tests are finished, return // a possible error to the callback or null if none. +const path = require('path'); const testRunner = require('../../../../../test/integration/electron/testrunner'); -// You can directly control Mocha options by uncommenting the following lines -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info -testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) +const suite = 'Integration TypeScript Tests'; + +const options: import('mocha').MochaOptions = { + ui: 'tdd', color: true, - timeout: 60000, -}); + timeout: 60000 +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); export = testRunner; diff --git a/extensions/typescript-language-features/web/src/pathMapper.ts b/extensions/typescript-language-features/web/src/pathMapper.ts index e92548950fc..dbe9aada758 100644 --- a/extensions/typescript-language-features/web/src/pathMapper.ts +++ b/extensions/typescript-language-features/web/src/pathMapper.ts @@ -16,7 +16,7 @@ export class PathMapper { * Copied from toResource in typescriptServiceClient.ts */ toResource(filepath: string): URI { - if (looksLikeLibDtsPath(filepath)) { + if (looksLikeLibDtsPath(filepath) || looksLikeLocaleResourcePath(filepath)) { return URI.from({ scheme: this.extensionUri.scheme, authority: this.extensionUri.authority, @@ -83,6 +83,10 @@ export function looksLikeLibDtsPath(filepath: string) { return filepath.startsWith('/lib.') && filepath.endsWith('.d.ts'); } +export function looksLikeLocaleResourcePath(filepath: string) { + return !!filepath.match(/^\/[a-zA-Z]+(-[a-zA-Z]+)?\/diagnosticMessages\.generated\.json$/); +} + export function looksLikeNodeModules(filepath: string) { return filepath.includes('/node_modules'); } diff --git a/extensions/typescript-language-features/web/src/webServer.ts b/extensions/typescript-language-features/web/src/webServer.ts index 3d2d5f9dfeb..7bb1de3393f 100644 --- a/extensions/typescript-language-features/web/src/webServer.ts +++ b/extensions/typescript-language-features/web/src/webServer.ts @@ -41,6 +41,12 @@ async function initializeSession( removeEventListener('message', listener); }); setSys(sys); + + const localeStr = findArgument(args, '--locale'); + if (localeStr) { + ts.validateLocaleAndSetLanguage(localeStr, sys); + } + startWorkerSession(ts, sys, fs, sessionOptions, ports.tsserver, pathMapper, logger); } diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 440d4953a28..6f4ef625267 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -75,6 +75,9 @@ "name": "hello", "description": "Hello" } + ], + "modes": [ + "agent", "ask", "edit" ] }, { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index df90a035401..366da085b9b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -15,22 +15,25 @@ suite('chat', () => { disposables = []; // Register a dummy default model which is required for a participant request to go through - disposables.push(lm.registerChatModelProvider('test-lm', { - async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + disposables.push(lm.registerChatModelProvider('test-lm-vendor', { + async prepareLanguageModelChat(_options, _token) { + return [{ + id: 'test-lm', + name: 'test-lm', + family: 'test', + version: '1.0.0', + maxInputTokens: 100, + maxOutputTokens: 100, + isDefault: true, + isUserSelectable: true + }]; + }, + async provideLanguageModelChatResponse(_model, _messages, _options, _progress, _token) { return undefined; }, - async provideTokenCount(_text, _token) { + async provideTokenCount(_model, _text, _token) { return 1; }, - }, { - name: 'test-lm', - version: '1.0.0', - family: 'test', - vendor: 'test-lm-vendor', - maxInputTokens: 100, - maxOutputTokens: 100, - isDefault: true, - isUserSelectable: true })); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 60c2931418a..28f66a0f104 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -15,7 +15,7 @@ suite('vscode API - debug', function () { await closeAllEditors(); }); - test('breakpoints are available before accessing debug extension API', async () => { + test.skip('breakpoints are available before accessing debug extension API', async () => { const file = await createRandomFile(undefined, undefined, '.js'); const doc = await workspace.openTextDocument(file); await window.showTextDocument(doc); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts index b73025ab2e7..c3e28b4c303 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/ipynb.test.ts @@ -6,13 +6,40 @@ import * as assert from 'assert'; import 'mocha'; import * as vscode from 'vscode'; +import { assertNoRpc, closeAllEditors, createRandomFile } from '../utils'; + +const ipynbContent = JSON.stringify({ + "cells": [ + { + "cell_type": "markdown", + "source": ["## Header"], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": ["print('hello 1')\n", "print('hello 2')"], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": ["hello 1\n", "hello 2\n"] + } + ], + "metadata": {} + } + ] +}); + +suite('ipynb NotebookSerializer', function () { + teardown(async function () { + assertNoRpc(); + await closeAllEditors(); + }); -(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('ipynb NotebookSerializer', function () { test('Can open an ipynb notebook', async () => { - assert.ok(vscode.workspace.workspaceFolders); - const workspace = vscode.workspace.workspaceFolders[0]; - const uri = vscode.Uri.joinPath(workspace.uri, 'test.ipynb'); - const notebook = await vscode.workspace.openNotebookDocument(uri); + const file = await createRandomFile(ipynbContent, undefined, '.ipynb'); + const notebook = await vscode.workspace.openNotebookDocument(file); await vscode.window.showNotebookDocument(notebook); const notebookEditor = vscode.window.activeNotebookEditor; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts index a35759d415b..74b9ecefef5 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -13,11 +13,11 @@ suite('lm', function () { let disposables: vscode.Disposable[] = []; - const testProviderOptions: vscode.ChatResponseProviderMetadata = { + const testProviderOptions: vscode.LanguageModelChatInformation = { + id: 'test-lm', name: 'test-lm', version: '1.0.0', family: 'test', - vendor: 'test-lm-vendor', maxInputTokens: 100, maxOutputTokens: 100, }; @@ -38,15 +38,23 @@ suite('lm', function () { let p: vscode.Progress | undefined; const defer = new DeferredPromise(); - disposables.push(vscode.lm.registerChatModelProvider('test-lm', { - async provideLanguageModelResponse(_messages, _options, _extensionId, progress, _token) { - p = progress; - return defer.p; - }, - async provideTokenCount(_text, _token) { - return 1; - }, - }, testProviderOptions)); + try { + disposables.push(vscode.lm.registerChatModelProvider('test-lm-vendor', { + async prepareLanguageModelChat(_options, _token) { + return [testProviderOptions]; + }, + async provideLanguageModelChatResponse(_model, _messages, _options, progress, _token) { + p = progress; + return defer.p; + }, + async provideTokenCount(_model, _text, _token) { + return 1; + }, + })); + } catch (e) { + assert.fail(`Failed to register chat model provider: ${e}`); + } + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -83,14 +91,17 @@ suite('lm', function () { test('lm request fail', async function () { - disposables.push(vscode.lm.registerChatModelProvider('test-lm', { - async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + disposables.push(vscode.lm.registerChatModelProvider('test-lm-vendor', { + async prepareLanguageModelChat(_options, _token) { + return [testProviderOptions]; + }, + async provideLanguageModelChatResponse(_model, _messages, _options, _progress, _token) { throw new Error('BAD'); }, - async provideTokenCount(_text, _token) { + async provideTokenCount(_model, _text, _token) { return 1; }, - }, testProviderOptions)); + })); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -107,14 +118,17 @@ suite('lm', function () { const defer = new DeferredPromise(); - disposables.push(vscode.lm.registerChatModelProvider('test-lm', { - async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + disposables.push(vscode.lm.registerChatModelProvider('test-lm-vendor', { + async prepareLanguageModelChat(_options, _token) { + return [testProviderOptions]; + }, + async provideLanguageModelChatResponse(_model, _messages, _options, _progress, _token) { return defer.p; }, - async provideTokenCount(_text, _token) { + async provideTokenCount(_model, _text, _token) { return 1; } - }, testProviderOptions)); + })); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -142,14 +156,17 @@ suite('lm', function () { test('LanguageModelError instance is not thrown to extensions#235322 (SYNC)', async function () { - disposables.push(vscode.lm.registerChatModelProvider('test-lm', { - provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + disposables.push(vscode.lm.registerChatModelProvider('test-lm-vendor', { + async prepareLanguageModelChat(_options, _token) { + return [testProviderOptions]; + }, + provideLanguageModelChatResponse(_model, _messages, _options, _progress, _token) { throw vscode.LanguageModelError.Blocked('You have been blocked SYNC'); }, - async provideTokenCount(_text, _token) { + async provideTokenCount(_model, _text, _token) { return 1; } - }, testProviderOptions)); + })); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -165,14 +182,17 @@ suite('lm', function () { test('LanguageModelError instance is not thrown to extensions#235322 (ASYNC)', async function () { - disposables.push(vscode.lm.registerChatModelProvider('test-lm', { - async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + disposables.push(vscode.lm.registerChatModelProvider('test-lm-vendor', { + async prepareLanguageModelChat(_options, _token) { + return [testProviderOptions]; + }, + async provideLanguageModelChatResponse(_model, _messages, _options, _progress, _token) { throw vscode.LanguageModelError.Blocked('You have been blocked ASYNC'); }, - async provideTokenCount(_text, _token) { + async provideTokenCount(_model, _text, _token) { return 1; } - }, testProviderOptions)); + })); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index e4489090017..0903b12a14a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts @@ -244,7 +244,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { // no kernel -> no default language assert.strictEqual(getFocusedCell(editor)?.document.languageId, 'typescript'); - await vscode.commands.executeCommand('vscode.openWith', notebook.uri, 'default'); + await vscode.window.showNotebookDocument(await vscode.workspace.openNotebookDocument(notebook.uri)); assert.strictEqual(vscode.window.activeTextEditor?.document.uri.path, notebook.uri.path); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index d1fafae7591..b232de35ffb 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -213,7 +213,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { } })); - vscode.commands.executeCommand('notebook.cell.execute', { document: notebook.uri, ranges: [{ start: 0, end: 1 }, { start: 1, end: 2 }] }); + await vscode.commands.executeCommand('notebook.cell.execute', { document: notebook.uri, ranges: [{ start: 0, end: 1 }, { start: 1, end: 2 }] }); await def.p; await saveAllFilesAndCloseAll(); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 549bd426c13..e925641aeb4 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1178,7 +1178,7 @@ suite('vscode API - workspace', () => { }); - test('issue #110141 - TextEdit.setEndOfLine applies an edit and invalidates redo stack even when no change is made', async () => { + test.skip('issue #110141 - TextEdit.setEndOfLine applies an edit and invalidates redo stack even when no change is made', async () => { const file = await createRandomFile('hello\nworld'); const document = await vscode.workspace.openTextDocument(file); diff --git a/package-lock.json b/package-lock.json index 09d8dc7afb4..a78fb725f11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", + "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", "native-is-elevated": "0.7.0", @@ -94,7 +95,7 @@ "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.9.1", "debounce": "^1.0.0", - "deemon": "^1.13.4", + "deemon": "^1.13.5", "electron": "35.6.0", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", @@ -156,7 +157,8 @@ "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", - "yaserver": "^0.4.0" + "yaserver": "^0.4.0", + "zx": "^8.7.0" }, "optionalDependencies": { "windows-foreground-love": "0.5.0" @@ -1286,34 +1288,33 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2186,11 +2187,23 @@ "@types/json-schema": "*" } }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/expect": { "version": "1.20.4", @@ -3537,148 +3550,163 @@ "hasInstallScript": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -3830,13 +3858,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/abbrev": { "version": "1.1.1", @@ -3858,10 +3888,11 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3869,13 +3900,17 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", + "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/acorn-jsx": { @@ -5975,9 +6010,9 @@ } }, "node_modules/deemon": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.4.tgz", - "integrity": "sha512-O+7MRrNEddXeZXJusSSkFfBsJ5faVt+XpjfIosciuaK6StTjMi5Q4poYUxFlrIPHusrhrc4isC1gxt9nLijf6Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.5.tgz", + "integrity": "sha512-mmQi4Rz7ehx/xmUoDKzYAfzoEIv0HIGuDL88aj7iIxxYq0OwxRc/edFqso+aUdgame1ZBGLoV4Ucd/5/WCmu4w==", "dev": true, "license": "MIT", "dependencies": { @@ -6471,10 +6506,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -11137,6 +11173,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -11151,6 +11188,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -11160,6 +11198,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -11385,6 +11424,31 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/kerberos": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz", @@ -12263,7 +12327,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -15243,18 +15308,19 @@ "dev": true }, "node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -16682,13 +16748,14 @@ } }, "node_modules/terser": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.0.tgz", - "integrity": "sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16700,16 +16767,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -16733,35 +16801,19 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -18003,20 +18055,23 @@ "dev": true }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.100.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.0.tgz", + "integrity": "sha512-H8yBSBTk+BqxrINJnnRzaxU94SVP2bjd7WmA+PfCphoIdDpeQMJ77pq9/4I7xjLq38cB1bNKfzYPZu8pB3zKtg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "enhanced-resolve": "^5.17.2", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -18026,11 +18081,11 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -18137,10 +18192,11 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -18239,24 +18295,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -18597,6 +18635,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zx": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.7.0.tgz", + "integrity": "sha512-pArftqj5JV/er8p+czFZwF+k6SbCldl7kcfCR+rIiDIh3gUsLB0F3Xh05diP8PzToZ39D/GWeFoVFimjHQkbAg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } } } } diff --git a/package.json b/package.json index dd51bb0b769..7ee889841c3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.103.0", - "distro": "7fd50e9bdc1a124eb32a184ee4bc0d437cdae9f0", + "distro": "a5bd023116e1e912804ee6d9f1c0b51c0369e8e4", "author": { "name": "Microsoft Corporation" }, @@ -99,6 +99,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", + "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", "native-is-elevated": "0.7.0", @@ -153,7 +154,7 @@ "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.9.1", "debounce": "^1.0.0", - "deemon": "^1.13.4", + "deemon": "^1.13.5", "electron": "35.6.0", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", @@ -215,7 +216,8 @@ "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", - "yaserver": "^0.4.0" + "yaserver": "^0.4.0", + "zx": "^8.7.0" }, "overrides": { "node-gyp-build": "4.8.1", diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index e39b3218d80..9189ca495da 100755 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -111,7 +111,7 @@ deb [arch=amd64,arm64,armhf] https://packages.microsoft.com/repos/code stable ma # Sourced from https://packages.microsoft.com/keys/microsoft.asc if [ ! -f $CODE_TRUSTED_PART ]; then echo "-----BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.7 (GNU/Linux) +Version: BSN Pgp v1.1.0.0 mQENBFYxWIwBCADAKoZhZlJxGNGWzqV+1OG1xiQeoowKhssGAKvd+buXCGISZJwT LXZqIcIiLP7pqdcZWtE9bSc7yBY2MalDp9Liu0KekywQ6VVX1T72NPf5Ev6x6DLV @@ -119,15 +119,15 @@ LXZqIcIiLP7pqdcZWtE9bSc7yBY2MalDp9Liu0KekywQ6VVX1T72NPf5Ev6x6DLV OeizPXmRljMtUUttHQZnRhtlzkmwIrUivbfFPD+fEoHJ1+uIdfOzZX8/oKHKLe2j H632kvsNzJFlROVvGLYAk2WRcLu+RjjggixhwiB+Mu/A8Tf4V6b+YppS44q8EvVr M+QvY7LNSOffSO6Slsy9oisGTdfE39nC7pVRABEBAAG0N01pY3Jvc29mdCAoUmVs -ZWFzZSBzaWduaW5nKSA8Z3Bnc2VjdXJpdHlAbWljcm9zb2Z0LmNvbT6JATUEEwEC -AB8FAlYxWIwCGwMGCwkIBwMCBBUCCAMDFgIBAh4BAheAAAoJEOs+lK2+EinPGpsH -/32vKy29Hg51H9dfFJMx0/a/F+5vKeCeVqimvyTM04C+XENNuSbYZ3eRPHGHFLqe -MNGxsfb7C7ZxEeW7J/vSzRgHxm7ZvESisUYRFq2sgkJ+HFERNrqfci45bdhmrUsy -7SWw9ybxdFOkuQoyKD3tBmiGfONQMlBaOMWdAsic965rvJsd5zYaZZFI1UwTkFXV -KJt3bp3Ngn1vEYXwijGTa+FXz6GLHueJwF0I7ug34DgUkAFvAs8Hacr2DRYxL5RJ -XdNgj4Jd2/g6T9InmWT0hASljur+dJnzNiNCkbn9KbX7J/qK1IbR8y560yRmFsU+ -NdCFTW7wY0Fb1fWJ+/KTsC4= -=J6gs +ZWFzZSBzaWduaW5nKSA8Z3Bnc2VjdXJpdHlAbWljcm9zb2Z0LmNvbT6JATQEEwEI +AB4FAlYxWIwCGwMGCwkIBwMCAxUIAwMWAgECHgECF4AACgkQ6z6Urb4SKc+P9gf/ +diY2900wvWEgV7iMgrtGzx79W/PbwWiOkKoD9sdzhARXWiP8Q5teL/t5TUH6TZ3B +ENboDjwr705jLLPwuEDtPI9jz4kvdT86JwwG6N8gnWM8Ldi56SdJEtXrzwtlB/Fe +6tyfMT1E/PrJfgALUG9MWTIJkc0GhRJoyPpGZ6YWSLGXnk4c0HltYKDFR7q4wtI8 +4cBu4mjZHZbxIO6r8Cci+xxuJkpOTIpr4pdpQKpECM6x5SaT2gVnscbN0PE19KK9 +nPsBxyK4wW0AvAhed2qldBPTipgzPhqB2gu0jSryil95bKrSmlYJd1Y1XfNHno5D +xfn5JwgySBIdWWvtOI05gw== +=zPfd -----END PGP PUBLIC KEY BLOCK----- " | gpg --dearmor > $CODE_TRUSTED_PART if [ -f "$CODE_TRUSTED_PART_OLD" ]; then diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 1c7c66f76aa..d6168ffdcf1 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -10,10 +10,9 @@ import { IMouseEvent, StandardMouseEvent } from './mouseEvent.js'; import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadline } from '../common/async.js'; import { BugIndicatingError, onUnexpectedError } from '../common/errors.js'; import * as event from '../common/event.js'; -import dompurify from './dompurify/dompurify.js'; import { KeyCode } from '../common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../common/lifecycle.js'; -import { RemoteAuthorities, Schemas } from '../common/network.js'; +import { RemoteAuthorities } from '../common/network.js'; import * as platform from '../common/platform.js'; import { URI } from '../common/uri.js'; import { hash } from '../common/hash.js'; @@ -1589,6 +1588,35 @@ export function triggerUpload(): Promise { }); } +export interface INotification extends IDisposable { + readonly onClick: event.Event; +} + +export async function triggerNotification(message: string, options?: { detail?: string; sticky?: boolean }): Promise { + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + return; + } + + const disposables = new DisposableStore(); + + const notification = new Notification(message, { + body: options?.detail, + requireInteraction: options?.sticky + }); + + const onClick = new event.Emitter(); + disposables.add(addDisposableListener(notification, 'click', () => onClick.fire())); + disposables.add(addDisposableListener(notification, 'close', () => disposables.dispose())); + + disposables.add(toDisposable(() => notification.close())); + + return { + onClick: onClick.event, + dispose: () => disposables.dispose() + }; +} + export enum DetectedFullscreenMode { /** @@ -1652,171 +1680,6 @@ export function detectFullscreen(targetWindow: Window): IDetectedFullscreen | nu return null; } -// -- sanitize and trusted html - -/** - * Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src` - * attributes are valid. - */ -export function hookDomPurifyHrefAndSrcSanitizer(allowedProtocols: readonly string[], allowDataImages = false): IDisposable { - // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html - - // build an anchor to map URLs to - const anchor = document.createElement('a'); - - dompurify.addHook('afterSanitizeAttributes', (node) => { - // check all href/src attributes for validity - for (const attr of ['href', 'src']) { - if (node.hasAttribute(attr)) { - const attrValue = node.getAttribute(attr) as string; - if (attr === 'href' && attrValue.startsWith('#')) { - // Allow fragment links - continue; - } - - anchor.href = attrValue; - if (!allowedProtocols.includes(anchor.protocol.replace(/:$/, ''))) { - if (allowDataImages && attr === 'src' && anchor.href.startsWith('data:')) { - continue; - } - - node.removeAttribute(attr); - } - } - } - }); - - return toDisposable(() => { - dompurify.removeHook('afterSanitizeAttributes'); - }); -} - -const defaultSafeProtocols = [ - Schemas.http, - Schemas.https, - Schemas.command, -]; - -/** - * List of safe, non-input html tags. - */ -export const basicMarkupHtmlTags = Object.freeze([ - 'a', - 'abbr', - 'b', - 'bdo', - 'blockquote', - 'br', - 'caption', - 'cite', - 'code', - 'col', - 'colgroup', - 'dd', - 'del', - 'details', - 'dfn', - 'div', - 'dl', - 'dt', - 'em', - 'figcaption', - 'figure', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'li', - 'mark', - 'ol', - 'p', - 'pre', - 'q', - 'rp', - 'rt', - 'ruby', - 'samp', - 'small', - 'small', - 'source', - 'span', - 'strike', - 'strong', - 'sub', - 'summary', - 'sup', - 'table', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'time', - 'tr', - 'tt', - 'u', - 'ul', - 'var', - 'video', - 'wbr', -]); - -const defaultDomPurifyConfig = Object.freeze({ - ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], - ALLOWED_ATTR: ['href', 'data-href', 'data-command', 'target', 'title', 'name', 'src', 'alt', 'class', 'id', 'role', 'tabindex', 'style', 'data-code', 'width', 'height', 'align', 'x-dispatch', 'required', 'checked', 'placeholder', 'type', 'start'], - RETURN_DOM: false, - RETURN_DOM_FRAGMENT: false, - RETURN_TRUSTED_TYPE: true -}); - -/** - * Sanitizes the given `value` and reset the given `node` with it. - */ -export function safeInnerHtml(node: HTMLElement, value: string, extraDomPurifyConfig?: dompurify.Config): void { - const hook = hookDomPurifyHrefAndSrcSanitizer(defaultSafeProtocols); - try { - const html = dompurify.sanitize(value, { ...defaultDomPurifyConfig, ...extraDomPurifyConfig }); - node.innerHTML = html as unknown as string; - } finally { - hook.dispose(); - } -} - -/** - * Convert a Unicode string to a string in which each 16-bit unit occupies only one byte - * - * From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa - */ -function toBinary(str: string): string { - const codeUnits = new Uint16Array(str.length); - for (let i = 0; i < codeUnits.length; i++) { - codeUnits[i] = str.charCodeAt(i); - } - let binary = ''; - const uint8array = new Uint8Array(codeUnits.buffer); - for (let i = 0; i < uint8array.length; i++) { - binary += String.fromCharCode(uint8array[i]); - } - return binary; -} - -/** - * Version of the global `btoa` function that handles multi-byte characters instead - * of throwing an exception. - */ -export function multibyteAwareBtoa(str: string): string { - return btoa(toBinary(str)); -} - type ModifierKey = 'alt' | 'ctrl' | 'shift' | 'meta'; export interface IModifierKeyStatus { diff --git a/src/vs/base/browser/domSanitize.ts b/src/vs/base/browser/domSanitize.ts new file mode 100644 index 00000000000..86bd090f744 --- /dev/null +++ b/src/vs/base/browser/domSanitize.ts @@ -0,0 +1,263 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore, IDisposable, toDisposable } from '../common/lifecycle.js'; +import { Schemas } from '../common/network.js'; +import dompurify from './dompurify/dompurify.js'; + + +/** + * List of safe, non-input html tags. + */ +export const basicMarkupHtmlTags = Object.freeze([ + 'a', + 'abbr', + 'b', + 'bdo', + 'blockquote', + 'br', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'dd', + 'del', + 'details', + 'dfn', + 'div', + 'dl', + 'dt', + 'em', + 'figcaption', + 'figure', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'img', + 'ins', + 'kbd', + 'label', + 'li', + 'mark', + 'ol', + 'p', + 'pre', + 'q', + 'rp', + 'rt', + 'ruby', + 'samp', + 'small', + 'small', + 'source', + 'span', + 'strike', + 'strong', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr', +]); + +export const defaultAllowedAttrs = Object.freeze([ + 'href', + 'target', + 'src', + 'alt', + 'title', + 'for', + 'name', + 'role', + 'tabindex', + 'x-dispatch', + 'required', + 'checked', + 'placeholder', + 'type', + 'start', + 'width', + 'height', + 'align', +]); + + +type UponSanitizeElementCb = (currentNode: Element, data: dompurify.SanitizeElementHookEvent, config: dompurify.Config) => void; +type UponSanitizeAttributeCb = (currentNode: Element, data: dompurify.SanitizeAttributeHookEvent, config: dompurify.Config) => void; + +function addDompurifyHook(hook: 'uponSanitizeElement', cb: UponSanitizeElementCb): IDisposable; +function addDompurifyHook(hook: 'uponSanitizeAttribute', cb: UponSanitizeAttributeCb): IDisposable; +function addDompurifyHook(hook: 'uponSanitizeElement' | 'uponSanitizeAttribute', cb: any): IDisposable { + dompurify.addHook(hook, cb); + return toDisposable(() => dompurify.removeHook(hook)); +} + +/** + * Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src` + * attributes are valid. + */ +function hookDomPurifyHrefAndSrcSanitizer(allowedLinkProtocols: readonly string[] | '*', allowedMediaProtocols: readonly string[]): IDisposable { + // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html + // build an anchor to map URLs to + const anchor = document.createElement('a'); + + function validateLink(value: string, allowedProtocols: readonly string[] | '*'): boolean { + if (allowedProtocols === '*') { + return true; // allow all protocols + } + + anchor.href = value; + return allowedProtocols.includes(anchor.protocol.replace(/:$/, '')); + } + + dompurify.addHook('afterSanitizeAttributes', (node) => { + // check all href/src attributes for validity + for (const attr of ['href', 'src']) { + if (node.hasAttribute(attr)) { + const attrValue = node.getAttribute(attr) as string; + if (attr === 'href') { + + if (!attrValue.startsWith('#') && !validateLink(attrValue, allowedLinkProtocols)) { + node.removeAttribute(attr); + } + + } else {// 'src' + if (!validateLink(attrValue, allowedMediaProtocols)) { + node.removeAttribute(attr); + } + } + } + } + }); + + return toDisposable(() => dompurify.removeHook('afterSanitizeAttributes')); +} + +export interface DomSanitizerConfig { + /** + * Configured the allowed html tags. + */ + readonly allowedTags?: { + readonly override?: readonly string[]; + readonly augment?: readonly string[]; + }; + + /** + * Configured the allowed html attributes. + */ + readonly allowedAttributes?: { + readonly override?: readonly string[]; + readonly augment?: readonly string[]; + }; + + /** + * List of allowed protocols for `href` attributes. + */ + readonly allowedLinkProtocols?: { + readonly override?: readonly string[] | '*'; + }; + + /** + * List of allowed protocols for `src` attributes. + */ + readonly allowedMediaProtocols?: { + readonly override?: readonly string[]; + }; + + // TODO: move these into more controlled api + readonly _do_not_use_hooks?: { + readonly uponSanitizeElement?: UponSanitizeElementCb; + readonly uponSanitizeAttribute?: UponSanitizeAttributeCb; + }; +} + +const defaultDomPurifyConfig = Object.freeze({ + ALLOWED_TAGS: [...basicMarkupHtmlTags], + ALLOWED_ATTR: [...defaultAllowedAttrs], + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: true, + // We sanitize the src/href attributes later if needed + ALLOW_UNKNOWN_PROTOCOLS: true, +} satisfies dompurify.Config); + +/** + * Sanitizes an html string. + * + * @param untrusted The HTML string to sanitize. + * @param config Optional configuration for sanitization. If not provided, defaults to a safe configuration. + * + * @returns A sanitized string of html. + */ +export function sanitizeHtml(untrusted: string, config?: DomSanitizerConfig): TrustedHTML { + const store = new DisposableStore(); + try { + const resolvedConfig: dompurify.Config = { ...defaultDomPurifyConfig }; + + if (config?.allowedTags) { + if (config.allowedTags.override) { + resolvedConfig.ALLOWED_TAGS = [...config.allowedTags.override]; + } + + if (config.allowedTags.augment) { + resolvedConfig.ALLOWED_TAGS = [...(resolvedConfig.ALLOWED_TAGS ?? []), ...config.allowedTags.augment]; + } + } + + if (config?.allowedAttributes) { + if (config.allowedAttributes.override) { + resolvedConfig.ALLOWED_ATTR = [...config.allowedAttributes.override]; + } + + if (config.allowedAttributes.augment) { + resolvedConfig.ALLOWED_ATTR = [...(resolvedConfig.ALLOWED_ATTR ?? []), ...config.allowedAttributes.augment]; + } + } + + store.add(hookDomPurifyHrefAndSrcSanitizer( + config?.allowedLinkProtocols?.override ?? [Schemas.http, Schemas.https], + config?.allowedMediaProtocols?.override ?? [Schemas.http, Schemas.https])); + + if (config?._do_not_use_hooks?.uponSanitizeElement) { + store.add(addDompurifyHook('uponSanitizeElement', config?._do_not_use_hooks.uponSanitizeElement)); + } + + if (config?._do_not_use_hooks?.uponSanitizeAttribute) { + store.add(addDompurifyHook('uponSanitizeAttribute', config._do_not_use_hooks.uponSanitizeAttribute)); + } + + return dompurify.sanitize(untrusted, { + ...resolvedConfig, + RETURN_TRUSTED_TYPE: true + }); + } finally { + store.dispose(); + } +} + +/** + * Sanitizes the given `value` and reset the given `node` with it. + */ +export function safeSetInnerHtml(node: HTMLElement, untrusted: string, config?: DomSanitizerConfig): void { + node.innerHTML = sanitizeHtml(untrusted, config) as any; +} diff --git a/src/vs/base/browser/formattedTextRenderer.ts b/src/vs/base/browser/formattedTextRenderer.ts index ed15b1316ab..14f7493fd46 100644 --- a/src/vs/base/browser/formattedTextRenderer.ts +++ b/src/vs/base/browser/formattedTextRenderer.ts @@ -14,30 +14,20 @@ export interface IContentActionHandler { } export interface FormattedTextRenderOptions { - readonly className?: string; - readonly inline?: boolean; readonly actionHandler?: IContentActionHandler; readonly renderCodeSegments?: boolean; } -export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement { - const element = createElement(options); +export function renderText(text: string, _options?: FormattedTextRenderOptions, target?: HTMLElement): HTMLElement { + const element = target ?? document.createElement('div'); element.textContent = text; return element; } -export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement { - const element = createElement(options); - _renderFormattedText(element, parseFormattedText(formattedText, !!options.renderCodeSegments), options.actionHandler, options.renderCodeSegments); - return element; -} - -export function createElement(options: FormattedTextRenderOptions): HTMLElement { - const tagName = options.inline ? 'span' : 'div'; - const element = document.createElement(tagName); - if (options.className) { - element.className = options.className; - } +export function renderFormattedText(formattedText: string, options?: FormattedTextRenderOptions, target?: HTMLElement): HTMLElement { + const element = target ?? document.createElement('div'); + element.textContent = ''; + _renderFormattedText(element, parseFormattedText(formattedText, !!options?.renderCodeSegments), options?.actionHandler, options?.renderCodeSegments); return element; } diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 04e4ea0ef9e..7cae9f4a07b 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -5,12 +5,12 @@ import { onUnexpectedError } from '../common/errors.js'; import { Event } from '../common/event.js'; -import { escapeDoubleQuotes, IMarkdownString, isMarkdownString, MarkdownStringTrustedOptions, parseHrefAndDimensions, removeMarkdownEscapes } from '../common/htmlContent.js'; +import { escapeDoubleQuotes, IMarkdownString, MarkdownStringTrustedOptions, parseHrefAndDimensions, removeMarkdownEscapes } from '../common/htmlContent.js'; import { markdownEscapeEscapedIcons } from '../common/iconLabels.js'; import { defaultGenerator } from '../common/idGenerator.js'; import { KeyCode } from '../common/keyCodes.js'; import { Lazy } from '../common/lazy.js'; -import { DisposableStore, IDisposable, toDisposable } from '../common/lifecycle.js'; +import { DisposableStore } from '../common/lifecycle.js'; import * as marked from '../common/marked/marked.js'; import { parse } from '../common/marshalling.js'; import { FileAccess, Schemas } from '../common/network.js'; @@ -19,30 +19,47 @@ import { dirname, resolvePath } from '../common/resources.js'; import { escape } from '../common/strings.js'; import { URI } from '../common/uri.js'; import * as DOM from './dom.js'; -import dompurify from './dompurify/dompurify.js'; +import * as domSanitize from './domSanitize.js'; import { DomEmitter } from './event.js'; -import { createElement, FormattedTextRenderOptions } from './formattedTextRenderer.js'; +import { FormattedTextRenderOptions } from './formattedTextRenderer.js'; import { StandardKeyboardEvent } from './keyboardEvent.js'; import { StandardMouseEvent } from './mouseEvent.js'; import { renderLabelWithIcons } from './ui/iconLabel/iconLabels.js'; -export interface MarkedOptions extends Readonly> { - readonly markedExtensions?: marked.MarkedExtension[]; -} - +/** + * Options for the rendering of markdown with {@link renderMarkdown}. + */ export interface MarkdownRenderOptions extends FormattedTextRenderOptions { readonly codeBlockRenderer?: (languageId: string, value: string) => Promise; readonly codeBlockRendererSync?: (languageId: string, value: string, raw?: string) => HTMLElement; readonly asyncRenderCallback?: () => void; + readonly fillInIncompleteTokens?: boolean; - readonly remoteImageIsAllowed?: (uri: URI) => boolean; - readonly sanitizerOptions?: ISanitizerOptions; + + readonly sanitizerConfig?: MarkdownSanitizerConfig; + + readonly markedOptions?: MarkdownRendererMarkedOptions; + readonly markedExtensions?: marked.MarkedExtension[]; } -export interface ISanitizerOptions { - replaceWithPlaintext?: boolean; - allowedTags?: string[]; - allowedProductProtocols?: string[]; +/** + * Subset of options passed to `Marked` for rendering markdown. + */ +export interface MarkdownRendererMarkedOptions { + readonly gfm?: boolean; + readonly breaks?: boolean; +} + +export interface MarkdownSanitizerConfig { + readonly replaceWithPlaintext?: boolean; + readonly allowedTags?: { + readonly override: readonly string[]; + }; + readonly customAttrSanitizer?: (attrName: string, attrValue: string) => boolean | string; + readonly allowedLinkSchemes?: { + readonly augment: readonly string[]; + }; + readonly remoteImageIsAllowed?: (uri: URI) => boolean; } const defaultMarkedRenderers = Object.freeze({ @@ -100,29 +117,27 @@ const defaultMarkedRenderers = Object.freeze({ * **Note** that for most cases you should be using {@link import('../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js').MarkdownRenderer MarkdownRenderer} * which comes with support for pretty code block rendering and which uses the default way of handling links. */ -export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): { element: HTMLElement; dispose: () => void } { +export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, target?: HTMLElement): { element: HTMLElement; dispose: () => void } { const disposables = new DisposableStore(); let isDisposed = false; - const element = createElement(options); - - const markedInstance = new marked.Marked(...(markedOptions.markedExtensions ?? [])); + const markedInstance = new marked.Marked(...(options.markedExtensions ?? [])); const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markdown); const value = preprocessMarkdownString(markdown); let renderedMarkdown: string; if (options.fillInIncompleteTokens) { // The defaults are applied by parse but not lexer()/parser(), and they need to be present - const opts: MarkedOptions = { - ...marked.defaults, - ...markedOptions, + const opts: marked.MarkedOptions = { + ...markedInstance.defaults, + ...options.markedOptions, renderer }; const tokens = markedInstance.lexer(value, opts); const newTokens = fillInIncompleteTokens(tokens); renderedMarkdown = markedInstance.parser(newTokens, opts); } else { - renderedMarkdown = markedInstance.parse(value, { ...markedOptions, renderer, async: false }); + renderedMarkdown = markedInstance.parse(value, { ...options?.markedOptions, renderer, async: false }); } // Rewrite theme icons @@ -132,11 +147,12 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } const htmlParser = new DOMParser(); - const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, renderedMarkdown) as unknown as string, 'text/html'); + const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown(renderedMarkdown, markdown.isTrusted ?? false, options.sanitizerConfig) as unknown as string, 'text/html'); rewriteRenderedLinks(markdown, options, markdownHtmlDoc.body); - element.innerHTML = sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, markdownHtmlDoc.body.innerHTML) as unknown as string; + const element = target ?? document.createElement('div'); + element.innerHTML = sanitizeRenderedMarkdown(markdownHtmlDoc.body.innerHTML, markdown.isTrusted ?? false, options.sanitizerConfig) as unknown as string; if (codeBlocks.length > 0) { Promise.all(codeBlocks).then((tuples) => { @@ -217,9 +233,9 @@ function rewriteRenderedLinks(markdown: IMarkdownString, options: MarkdownRender el.setAttribute('src', massageHref(markdown, href, true)); - if (options.remoteImageIsAllowed) { + if (options.sanitizerConfig?.remoteImageIsAllowed) { const uri = URI.parse(href); - if (uri.scheme !== Schemas.file && uri.scheme !== Schemas.data && !options.remoteImageIsAllowed(uri)) { + if (uri.scheme !== Schemas.file && uri.scheme !== Schemas.data && !options.sanitizerConfig.remoteImageIsAllowed(uri)) { el.replaceWith(DOM.$('', undefined, el.outerHTML)); } } @@ -246,7 +262,7 @@ function rewriteRenderedLinks(markdown: IMarkdownString, options: MarkdownRender } function createMarkdownRenderer(marked: marked.Marked, options: MarkdownRenderOptions, markdown: IMarkdownString): { renderer: marked.Renderer; codeBlocks: Promise<[string, HTMLElement]>[]; syncCodeBlocks: [string, HTMLElement][] } { - const renderer = new marked.Renderer(); + const renderer = new marked.Renderer(options.markedOptions); renderer.image = defaultMarkedRenderers.image; renderer.link = defaultMarkedRenderers.link; renderer.paragraph = defaultMarkedRenderers.paragraph; @@ -275,7 +291,7 @@ function createMarkdownRenderer(marked: marked.Marked, options: MarkdownRenderOp // Note: we always pass the output through dompurify after this so that we don't rely on // marked for real sanitization. renderer.html = ({ text }) => { - if (options.sanitizerOptions?.replaceWithPlaintext) { + if (options.sanitizerConfig?.replaceWithPlaintext) { return escape(text); } @@ -396,102 +412,19 @@ function resolveWithBaseUri(baseUri: URI, href: string): string { } } -interface IInternalSanitizerOptions extends ISanitizerOptions { - isTrusted?: boolean | MarkdownStringTrustedOptions; -} const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; function sanitizeRenderedMarkdown( - options: IInternalSanitizerOptions, renderedMarkdown: string, + isTrusted: boolean | MarkdownStringTrustedOptions, + options: MarkdownSanitizerConfig = {}, ): TrustedHTML { - const { config, allowedSchemes } = getSanitizerOptions(options); - const store = new DisposableStore(); - store.add(addDompurifyHook('uponSanitizeAttribute', (element, e) => { - if (e.attrName === 'style' || e.attrName === 'class') { - if (element.tagName === 'SPAN') { - if (e.attrName === 'style') { - e.keepAttr = /^(color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(background-color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(border-radius:[0-9]+px;)?$/.test(e.attrValue); - return; - } else if (e.attrName === 'class') { - e.keepAttr = /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(e.attrValue); - return; - } - } - e.keepAttr = false; - return; - } else if (element.tagName === 'INPUT' && element.attributes.getNamedItem('type')?.value === 'checkbox') { - if ((e.attrName === 'type' && e.attrValue === 'checkbox') || e.attrName === 'disabled' || e.attrName === 'checked') { - e.keepAttr = true; - return; - } - e.keepAttr = false; - } - })); - - store.add(addDompurifyHook('uponSanitizeElement', (element, e) => { - if (e.tagName === 'input') { - if (element.attributes.getNamedItem('type')?.value === 'checkbox') { - element.setAttribute('disabled', ''); - } else if (!options.replaceWithPlaintext) { - element.remove(); - } - } - - if (options.replaceWithPlaintext && !e.allowedTags[e.tagName] && e.tagName !== 'body') { - if (element.parentElement) { - let startTagText: string; - let endTagText: string | undefined; - if (e.tagName === '#comment') { - startTagText = ``; - } else { - const isSelfClosing = selfClosingTags.includes(e.tagName); - const attrString = element.attributes.length ? - ' ' + Array.from(element.attributes) - .map(attr => `${attr.name}="${attr.value}"`) - .join(' ') - : ''; - startTagText = `<${e.tagName}${attrString}>`; - if (!isSelfClosing) { - endTagText = ``; - } - } - - const fragment = document.createDocumentFragment(); - const textNode = element.parentElement.ownerDocument.createTextNode(startTagText); - fragment.appendChild(textNode); - const endTagTextNode = endTagText ? element.parentElement.ownerDocument.createTextNode(endTagText) : undefined; - while (element.firstChild) { - fragment.appendChild(element.firstChild); - } - - if (endTagTextNode) { - fragment.appendChild(endTagTextNode); - } - - if (element.nodeType === Node.COMMENT_NODE) { - // Workaround for https://github.com/cure53/DOMPurify/issues/1005 - // The comment will be deleted in the next phase. However if we try to remove it now, it will cause - // an exception. Instead we insert the text node before the comment. - element.parentElement.insertBefore(fragment, element); - } else { - element.parentElement.replaceChild(fragment, element); - } - } - } - })); - - store.add(DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes)); - - try { - return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); - } finally { - store.dispose(); - } + const sanitizerConfig = getSanitizerOptions(isTrusted, options); + return domSanitize.sanitizeHtml(renderedMarkdown, sanitizerConfig); } -export const allowedMarkdownAttr = [ +export const allowedMarkdownHtmlAttributes = [ 'align', 'autoplay', 'alt', @@ -499,8 +432,6 @@ export const allowedMarkdownAttr = [ 'class', 'colspan', 'controls', - 'data-code', - 'data-href', 'disabled', 'draggable', 'height', @@ -511,20 +442,26 @@ export const allowedMarkdownAttr = [ 'poster', 'rowspan', 'src', - 'style', 'target', 'title', 'type', 'width', 'start', + + // Custom markdown attributes + 'data-code', + 'data-href', + + // These attributes are sanitized in the hooks + 'style', + 'class', ]; -function getSanitizerOptions(options: IInternalSanitizerOptions): { config: dompurify.Config; allowedSchemes: string[] } { - const allowedSchemes = [ +function getSanitizerOptions(isTrusted: boolean | MarkdownStringTrustedOptions, options: MarkdownSanitizerConfig): domSanitize.DomSanitizerConfig { + const allowedLinkSchemes = [ Schemas.http, Schemas.https, Schemas.mailto, - Schemas.data, Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemote, @@ -532,52 +469,153 @@ function getSanitizerOptions(options: IInternalSanitizerOptions): { config: domp Schemas.vscodeNotebookCell ]; - if (options.isTrusted) { - allowedSchemes.push(Schemas.command); + if (isTrusted) { + allowedLinkSchemes.push(Schemas.command); } - if (options.allowedProductProtocols) { - allowedSchemes.push(...options.allowedProductProtocols); + if (options.allowedLinkSchemes?.augment) { + allowedLinkSchemes.push(...options.allowedLinkSchemes.augment); } return { - config: { - // allowedTags should included everything that markdown renders to. - // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure. - // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ - // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- - ALLOWED_TAGS: options.allowedTags ?? [...DOM.basicMarkupHtmlTags], - ALLOWED_ATTR: allowedMarkdownAttr, - ALLOW_UNKNOWN_PROTOCOLS: true, + // allowedTags should included everything that markdown renders to. + // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure. + // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ + // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- + allowedTags: { + override: options.allowedTags?.override ?? domSanitize.basicMarkupHtmlTags }, - allowedSchemes + allowedAttributes: { + override: allowedMarkdownHtmlAttributes, + }, + allowedLinkProtocols: { + override: allowedLinkSchemes, + }, + allowedMediaProtocols: { + override: [ + Schemas.http, + Schemas.https, + Schemas.data, + Schemas.file, + Schemas.vscodeFileResource, + Schemas.vscodeRemote, + Schemas.vscodeRemoteResource, + ] + }, + _do_not_use_hooks: { + uponSanitizeAttribute: (element, e) => { + if (options.customAttrSanitizer) { + const result = options.customAttrSanitizer(e.attrName, e.attrValue); + if (typeof result === 'string') { + if (result) { + e.attrValue = result; + e.keepAttr = true; + } else { + e.keepAttr = false; + } + } else { + e.keepAttr = result; + } + return; + } + + if (e.attrName === 'style' || e.attrName === 'class') { + if (element.tagName === 'SPAN') { + if (e.attrName === 'style') { + e.keepAttr = /^(color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(background-color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(border-radius:[0-9]+px;)?$/.test(e.attrValue); + return; + } else if (e.attrName === 'class') { + e.keepAttr = /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(e.attrValue); + return; + } + } + + e.keepAttr = false; + return; + } else if (element.tagName === 'INPUT' && element.attributes.getNamedItem('type')?.value === 'checkbox') { + if ((e.attrName === 'type' && e.attrValue === 'checkbox') || e.attrName === 'disabled' || e.attrName === 'checked') { + e.keepAttr = true; + return; + } + e.keepAttr = false; + } + }, + uponSanitizeElement: (element, e) => { + if (e.tagName === 'input') { + if (element.attributes.getNamedItem('type')?.value === 'checkbox') { + element.setAttribute('disabled', ''); + } else if (!options.replaceWithPlaintext) { + element.remove(); + } + } + + if (options.replaceWithPlaintext && !e.allowedTags[e.tagName] && e.tagName !== 'body') { + if (element.parentElement) { + let startTagText: string; + let endTagText: string | undefined; + if (e.tagName === '#comment') { + startTagText = ``; + } else { + const isSelfClosing = selfClosingTags.includes(e.tagName); + const attrString = element.attributes.length ? + ' ' + Array.from(element.attributes) + .map(attr => `${attr.name}="${attr.value}"`) + .join(' ') + : ''; + startTagText = `<${e.tagName}${attrString}>`; + if (!isSelfClosing) { + endTagText = ``; + } + } + + const fragment = document.createDocumentFragment(); + const textNode = element.parentElement.ownerDocument.createTextNode(startTagText); + fragment.appendChild(textNode); + const endTagTextNode = endTagText ? element.parentElement.ownerDocument.createTextNode(endTagText) : undefined; + while (element.firstChild) { + fragment.appendChild(element.firstChild); + } + + if (endTagTextNode) { + fragment.appendChild(endTagTextNode); + } + + if (element.nodeType === Node.COMMENT_NODE) { + // Workaround for https://github.com/cure53/DOMPurify/issues/1005 + // The comment will be deleted in the next phase. However if we try to remove it now, it will cause + // an exception. Instead we insert the text node before the comment. + element.parentElement.insertBefore(fragment, element); + } else { + element.parentElement.replaceChild(fragment, element); + } + } + } + } + } }; } /** - * Strips all markdown from `string`, if it's an IMarkdownString. For example - * `# Header` would be output as `Header`. If it's not, the string is returned. - */ -export function renderStringAsPlaintext(string: IMarkdownString | string) { - return isMarkdownString(string) ? renderMarkdownAsPlaintext(string) : string; -} - -/** - * Strips all markdown from `markdown` + * Renders `str` as plaintext, stripping out Markdown syntax if it's a {@link IMarkdownString}. * * For example `# Header` would be output as `Header`. - * - * @param withCodeBlocks Include the ``` of code blocks as well */ -export function renderMarkdownAsPlaintext(markdown: IMarkdownString, withCodeBlocks?: boolean) { +export function renderAsPlaintext(str: IMarkdownString | string, options?: { + /** Controls if the ``` of code blocks should be preserved in the output or not */ + readonly includeCodeBlocksFences?: boolean; +}) { + if (typeof str === 'string') { + return str; + } + // values that are too long will freeze the UI - let value = markdown.value ?? ''; + let value = str.value ?? ''; if (value.length > 100_000) { value = `${value.substr(0, 100_000)}…`; } - const html = marked.parse(value, { async: false, renderer: withCodeBlocks ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }); - return sanitizeRenderedMarkdown({ isTrusted: false }, html) + const html = marked.parse(value, { async: false, renderer: options?.includeCodeBlocksFences ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }); + return sanitizeRenderedMarkdown(html, /* isTrusted */ false, {}) .toString() .replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m) .trim(); @@ -978,15 +1016,3 @@ function completeTable(tokens: marked.Token[]): marked.Token[] | undefined { return undefined; } -function addDompurifyHook( - hook: 'uponSanitizeElement', - cb: (currentNode: Element, data: dompurify.SanitizeElementHookEvent, config: dompurify.Config) => void, -): IDisposable; -function addDompurifyHook( - hook: 'uponSanitizeAttribute', - cb: (currentNode: Element, data: dompurify.SanitizeAttributeHookEvent, config: dompurify.Config) => void, -): IDisposable; -function addDompurifyHook(hook: 'uponSanitizeElement' | 'uponSanitizeAttribute', cb: any): IDisposable { - dompurify.addHook(hook, cb); - return toDisposable(() => dompurify.removeHook(hook)); -} diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index b6f4840005e..81764fa9960 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -5,9 +5,8 @@ import { IContextMenuProvider } from '../../contextmenu.js'; import { addDisposableListener, EventHelper, EventType, IFocusTracker, isActiveElement, reset, trackFocus, $ } from '../../dom.js'; -import dompurify from '../../dompurify/dompurify.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; -import { renderMarkdown, renderStringAsPlaintext } from '../../markdownRenderer.js'; +import { renderMarkdown, renderAsPlaintext } from '../../markdownRenderer.js'; import { Gesture, EventType as TouchEventType } from '../../touch.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import { IHoverDelegate } from '../hover/hoverDelegate.js'; @@ -25,6 +24,7 @@ import { localize } from '../../../../nls.js'; import type { IManagedHover } from '../hover/hover.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; import { IActionProvider } from '../dropdown/dropdown.js'; +import { safeSetInnerHtml, DomSanitizerConfig } from '../../domSanitize.js'; export interface IButtonOptions extends Partial { readonly title?: boolean | string; @@ -78,6 +78,16 @@ export interface IButtonWithDescription extends IButton { description: string; } +// Only allow a very limited set of inline html tags +const buttonSanitizerConfig = Object.freeze({ + allowedTags: { + override: ['b', 'i', 'u', 'code', 'span'], + }, + allowedAttributes: { + override: ['class'], + }, +}); + export class Button extends Disposable implements IButton { protected options: IButtonOptions; @@ -237,15 +247,13 @@ export class Button extends Disposable implements IButton { const labelElement = this.options.supportShortLabel ? this._labelElement! : this._element; if (isMarkdownString(value)) { - const rendered = renderMarkdown(value, { inline: true }); + const rendered = renderMarkdown(value, undefined, document.createElement('span')); rendered.dispose(); // Don't include outer `

` const root = rendered.element.querySelector('p')?.innerHTML; if (root) { - // Only allow a very limited set of inline html tags - const sanitized = dompurify.sanitize(root, { ADD_TAGS: ['b', 'i', 'u', 'code', 'span'], ALLOWED_ATTR: ['class'], RETURN_TRUSTED_TYPE: true }); - labelElement.innerHTML = sanitized as unknown as string; + safeSetInnerHtml(labelElement, root, buttonSanitizerConfig); } else { reset(labelElement); } @@ -261,7 +269,7 @@ export class Button extends Disposable implements IButton { if (typeof this.options.title === 'string') { title = this.options.title; } else if (this.options.title) { - title = renderStringAsPlaintext(value); + title = renderAsPlaintext(value); } this.setTitle(title); @@ -385,7 +393,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.primaryButton = this._register(new Button(this.element, options)); this._register(this.primaryButton.onDidClick(e => this._onDidClick.fire(e))); - this.action = this._register(new Action('primaryAction', renderStringAsPlaintext(this.primaryButton.label), undefined, true, async () => this._onDidClick.fire(undefined))); + this.action = this._register(new Action('primaryAction', renderAsPlaintext(this.primaryButton.label), undefined, true, async () => this._onDidClick.fire(undefined))); this.separatorContainer = document.createElement('div'); this.separatorContainer.classList.add('monaco-button-dropdown-separator'); @@ -626,6 +634,10 @@ export class ButtonWithIcon extends Button { this._element.append(this._iconElement, this._mdlabelElement); } + override get label(): IMarkdownString | string { + return super.label; + } + override set label(value: IMarkdownString | string) { if (this._label === value) { return; @@ -637,14 +649,12 @@ export class ButtonWithIcon extends Button { this._element.classList.add('monaco-text-button'); if (isMarkdownString(value)) { - const rendered = renderMarkdown(value, { inline: true }); + const rendered = renderMarkdown(value, undefined, document.createElement('span')); rendered.dispose(); const root = rendered.element.querySelector('p')?.innerHTML; if (root) { - // Only allow a very limited set of inline html tags - const sanitized = dompurify.sanitize(root, { ADD_TAGS: ['b', 'i', 'u', 'code', 'span'], ALLOWED_ATTR: ['class'], RETURN_TRUSTED_TYPE: true }); - this._mdlabelElement.innerHTML = sanitized as unknown as string; + safeSetInnerHtml(this._mdlabelElement, root, buttonSanitizerConfig); } else { reset(this._mdlabelElement); } @@ -660,7 +670,7 @@ export class ButtonWithIcon extends Button { if (typeof this.options.title === 'string') { title = this.options.title; } else if (this.options.title) { - title = renderStringAsPlaintext(value); + title = renderAsPlaintext(value); } this.setTitle(title); @@ -668,6 +678,10 @@ export class ButtonWithIcon extends Button { this._label = value; } + override get icon(): ThemeIcon { + return super.icon; + } + override set icon(icon: ThemeIcon) { this._iconElement.classList.value = ''; this._iconElement.classList.add(...ThemeIcon.asClassNameArray(icon)); diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 4081c2f28b2..b1c6eae5279 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -8,7 +8,6 @@ import * as cssJs from '../../cssValue.js'; import { DomEmitter } from '../../event.js'; import { renderFormattedText, renderText } from '../../formattedTextRenderer.js'; import { IHistoryNavigationWidget } from '../../history.js'; -import { MarkdownRenderOptions } from '../../markdownRenderer.js'; import { ActionBar } from '../actionbar/actionbar.js'; import * as aria from '../aria/aria.js'; import { AnchorAlignment, IContextViewProvider } from '../contextview/contextview.js'; @@ -485,14 +484,14 @@ export class InputBox extends Widget { div = dom.append(container, $('.monaco-inputbox-container')); layout(); - const renderOptions: MarkdownRenderOptions = { - inline: true, - className: 'monaco-inputbox-message' - }; - const spanElement = (this.message.formatContent - ? renderFormattedText(this.message.content!, renderOptions) - : renderText(this.message.content!, renderOptions)); + const spanElement = $('span.monaco-inputbox-message'); + if (this.message.formatContent) { + renderFormattedText(this.message.content!, undefined, spanElement); + } else { + renderText(this.message.content!, undefined, spanElement); + } + spanElement.classList.add(this.classForType(this.message.type)); const styles = this.stylesForType(this.message.type); diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index 0f667f6270d..42ba7550ba4 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -218,14 +218,19 @@ export abstract class AbstractScrollbar extends Widget { offsetY = e.pageY - domNodePosition.top; } - const offset = this._pointerDownRelativePosition(offsetX, offsetY); - this._setDesiredScrollPositionNow( - this._scrollByPage - ? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset) - : this._scrollbarState.getDesiredScrollPositionFromOffset(offset) - ); + const isMouse = (e.pointerType === 'mouse'); + const isLeftClick = (e.button === 0); - if (e.button === 0) { + if (isLeftClick || !isMouse) { + const offset = this._pointerDownRelativePosition(offsetX, offsetY); + this._setDesiredScrollPositionNow( + this._scrollByPage + ? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset) + : this._scrollbarState.getDesiredScrollPositionFromOffset(offset) + ); + } + + if (isLeftClick) { // left button e.preventDefault(); this._sliderPointerDown(e); diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 3e32a3c313f..2ba232ad999 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -165,8 +165,9 @@ export class MouseWheelClassifier { } private _isAlmostInt(value: number): boolean { + const epsilon = Number.EPSILON * 100; // Use a small tolerance factor for floating-point errors const delta = Math.abs(Math.round(value) - value); - return (delta < 0.01); + return (delta < 0.01 + epsilon); } } @@ -194,6 +195,9 @@ export abstract class AbstractScrollableElement extends Widget { private _revealOnScroll: boolean; + private _inertialTimeout: TimeoutTimer | null = null; + private _inertialSpeed: { X: number; Y: number } = { X: 0, Y: 0 }; + private readonly _onScroll = this._register(new Emitter()); public readonly onScroll: Event = this._onScroll.event; @@ -270,6 +274,10 @@ export abstract class AbstractScrollableElement extends Widget { public override dispose(): void { this._mouseWheelToDispose = dispose(this._mouseWheelToDispose); + if (this._inertialTimeout) { + this._inertialTimeout.dispose(); + this._inertialTimeout = null; + } super.dispose(); } @@ -363,6 +371,37 @@ export abstract class AbstractScrollableElement extends Widget { this._onMouseWheel(new StandardWheelEvent(browserEvent)); } + private async _periodicSync(): Promise { + let scheduleAgain = false; + + if (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0) { + this._scrollable.setScrollPositionNow({ + scrollTop: this._scrollable.getCurrentScrollPosition().scrollTop - this._inertialSpeed.Y * 100, + scrollLeft: this._scrollable.getCurrentScrollPosition().scrollLeft - this._inertialSpeed.X * 100 + }); + this._inertialSpeed.X *= 0.9; + this._inertialSpeed.Y *= 0.9; + if (Math.abs(this._inertialSpeed.X) < 0.01) { + this._inertialSpeed.X = 0; + } + if (Math.abs(this._inertialSpeed.Y) < 0.01) { + this._inertialSpeed.Y = 0; + } + + scheduleAgain = (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0); + } + + if (scheduleAgain) { + if (!this._inertialTimeout) { + this._inertialTimeout = new TimeoutTimer(); + } + this._inertialTimeout.cancelAndSet(() => this._periodicSync(), 1000 / 60); + } else { + this._inertialTimeout?.dispose(); + this._inertialTimeout = null; + } + } + // -------------------- mouse wheel scrolling -------------------- private _setListeningToMouseWheel(shouldListen: boolean): void { @@ -456,6 +495,19 @@ export abstract class AbstractScrollableElement extends Widget { // Check that we are scrolling towards a location which is valid desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition); + if (this._options.inertialScroll && (deltaX || deltaY)) { + let startPeriodic = false; + // Only start periodic if it's not running + if (this._inertialSpeed.X === 0 && this._inertialSpeed.Y === 0) { + startPeriodic = true; + } + this._inertialSpeed.Y = (deltaY < 0 ? -1 : 1) * (Math.abs(deltaY) ** 1.02); + this._inertialSpeed.X = (deltaX < 0 ? -1 : 1) * (Math.abs(deltaX) ** 1.02); + if (startPeriodic) { + this._periodicSync(); + } + } + if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) { const canPerformSmoothScroll = ( @@ -689,6 +741,7 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5), scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true), mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true), + inertialScroll: (typeof opts.inertialScroll !== 'undefined' ? opts.inertialScroll : false), arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11), listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null), diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts index a39e6053110..16f1fe3a777 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts @@ -30,6 +30,10 @@ export interface ScrollableElementCreationOptions { * Defaults to true. */ mouseWheelSmoothScroll?: boolean; + /** + * Make scrolling inertial - mostly useful with touchpad on linux. + */ + inertialScroll?: boolean; /** * Flip axes. Treat vertical scrolling like horizontal and vice-versa. * Defaults to false. @@ -151,6 +155,7 @@ export interface ScrollableElementResolvedOptions { fastScrollSensitivity: number; scrollPredominantAxis: boolean; mouseWheelSmoothScroll: boolean; + inertialScroll: boolean; arrowSize: number; listenOnDomNode: HTMLElement | null; horizontal: ScrollbarVisibility; diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 44735ae4b66..015088e035e 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -32,7 +32,7 @@ interface IAsyncDataTreeNode { readonly parent: IAsyncDataTreeNode | null; readonly children: IAsyncDataTreeNode[]; readonly id?: string | null; - refreshPromise: Promise | undefined; + refreshPromise: CancelablePromise | undefined; hasChildren: boolean; stale: boolean; slow: boolean; @@ -528,7 +528,7 @@ export class AsyncDataTree implements IDisposable private readonly findController?: AsyncFindController; private readonly getDefaultCollapseState: { (e: T): undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded }; - private readonly subTreeRefreshPromises = new Map, Promise>(); + private readonly subTreeRefreshPromises = new Map, CancelablePromise>(); private readonly refreshPromises = new Map, CancelablePromise>>(); protected readonly identityProvider?: IIdentityProvider; @@ -769,8 +769,7 @@ export class AsyncDataTree implements IDisposable } async setInput(input: TInput, viewState?: IAsyncDataTreeViewState): Promise { - this.refreshPromises.forEach(promise => promise.cancel()); - this.refreshPromises.clear(); + this.cancelAllRefreshPromises(); this.root.element = input!; @@ -792,6 +791,14 @@ export class AsyncDataTree implements IDisposable await this._updateChildren(element, recursive, rerender, undefined, options); } + cancelAllRefreshPromises(): void { + this.refreshPromises.forEach(promise => promise.cancel()); + this.refreshPromises.clear(); + + this.subTreeRefreshPromises.forEach(promise => promise.cancel()); + this.subTreeRefreshPromises.clear(); + } + private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext, options?: IAsyncDataTreeUpdateChildrenOptions): Promise { if (typeof this.root.element === 'undefined') { throw new TreeError(this.user, 'Tree input not set'); @@ -875,7 +882,7 @@ export class AsyncDataTree implements IDisposable } if (node.refreshPromise) { - await this.root.refreshPromise; + await node.refreshPromise; await Event.toPromise(this._onDidRender.event); } @@ -886,7 +893,7 @@ export class AsyncDataTree implements IDisposable const result = this.tree.expand(node === this.root ? null : node, recursive); if (node.refreshPromise) { - await this.root.refreshPromise; + await node.refreshPromise; await Event.toPromise(this._onDidRender.event); } @@ -1088,28 +1095,26 @@ export class AsyncDataTree implements IDisposable return; } } - return this.doRefreshSubTree(node, recursive, viewStateContext); } private async doRefreshSubTree(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext): Promise { - let done: () => void; - node.refreshPromise = new Promise(c => done = c); - this.subTreeRefreshPromises.set(node, node.refreshPromise); - - node.refreshPromise.finally(() => { - node.refreshPromise = undefined; - this.subTreeRefreshPromises.delete(node); - }); - - try { + const cancelablePromise = createCancelablePromise(async () => { const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext); node.stale = false; await Promises.settled(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext))); - } finally { - done!(); - } + }); + + node.refreshPromise = cancelablePromise; + this.subTreeRefreshPromises.set(node, cancelablePromise); + + cancelablePromise.finally(() => { + node.refreshPromise = undefined; + this.subTreeRefreshPromises.delete(node); + }); + + return cancelablePromise; } private async doRefreshNode(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext): Promise[]> { diff --git a/src/vs/base/browser/webWorkerFactory.ts b/src/vs/base/browser/webWorkerFactory.ts index 58a43681228..d55ef7601cc 100644 --- a/src/vs/base/browser/webWorkerFactory.ts +++ b/src/vs/base/browser/webWorkerFactory.ts @@ -129,13 +129,14 @@ class WebWorker extends Disposable implements IWebWorker { private readonly _onError = this._register(new Emitter()); public readonly onError = this._onError.event; - constructor(descriptorOrWorker: IWebWorkerDescriptor | Worker) { + constructor(descriptorOrWorker: IWebWorkerDescriptor | Worker | Promise) { super(); this.id = ++WebWorker.LAST_WORKER_ID; const workerOrPromise = ( descriptorOrWorker instanceof Worker - ? descriptorOrWorker - : getWorker(descriptorOrWorker, this.id) + ? descriptorOrWorker : + 'then' in descriptorOrWorker ? descriptorOrWorker + : getWorker(descriptorOrWorker, this.id) ); if (isPromiseLike(workerOrPromise)) { this.worker = workerOrPromise; @@ -197,8 +198,8 @@ export class WebWorkerDescriptor implements IWebWorkerDescriptor { } export function createWebWorker(esmModuleLocation: URI, label: string | undefined): IWebWorkerClient; -export function createWebWorker(workerDescriptor: IWebWorkerDescriptor | Worker): IWebWorkerClient; -export function createWebWorker(arg0: URI | IWebWorkerDescriptor | Worker, arg1?: string | undefined): IWebWorkerClient { +export function createWebWorker(workerDescriptor: IWebWorkerDescriptor | Worker | Promise): IWebWorkerClient; +export function createWebWorker(arg0: URI | IWebWorkerDescriptor | Worker | Promise, arg1?: string | undefined): IWebWorkerClient { const workerDescriptorOrWorker = (URI.isUri(arg0) ? new WebWorkerDescriptor(arg0, arg1) : arg0); return new WebWorkerClient(new WebWorker(workerDescriptorOrWorker)); } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index cfa0f8d9b92..10dd0439f2e 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -2305,4 +2305,226 @@ export function cancellableIterable(iterableOrIterator: AsyncIterator | As }; } +type ProducerConsumerValue = { + ok: true; + value: T; +} | { + ok: false; + error: Error; +}; + +class ProducerConsumer { + private readonly _unsatisfiedConsumers: DeferredPromise[] = []; + private readonly _unconsumedValues: ProducerConsumerValue[] = []; + private _finalValue: ProducerConsumerValue | undefined; + + public get hasFinalValue(): boolean { + return !!this._finalValue; + } + + produce(value: ProducerConsumerValue): void { + this._ensureNoFinalValue(); + if (this._unsatisfiedConsumers.length > 0) { + const deferred = this._unsatisfiedConsumers.shift()!; + this._resolveOrRejectDeferred(deferred, value); + } else { + this._unconsumedValues.push(value); + } + } + + produceFinal(value: ProducerConsumerValue): void { + this._ensureNoFinalValue(); + this._finalValue = value; + for (const deferred of this._unsatisfiedConsumers) { + this._resolveOrRejectDeferred(deferred, value); + } + this._unsatisfiedConsumers.length = 0; + } + + private _ensureNoFinalValue(): void { + if (this._finalValue) { + throw new BugIndicatingError('ProducerConsumer: cannot produce after final value has been set'); + } + } + + private _resolveOrRejectDeferred(deferred: DeferredPromise, value: ProducerConsumerValue): void { + if (value.ok) { + deferred.complete(value.value); + } else { + deferred.error(value.error); + } + } + + consume(): Promise { + if (this._unconsumedValues.length > 0 || this._finalValue) { + const value = this._unconsumedValues.length > 0 ? this._unconsumedValues.shift()! : this._finalValue!; + if (value.ok) { + return Promise.resolve(value.value); + } else { + return Promise.reject(value.error); + } + } else { + const deferred = new DeferredPromise(); + this._unsatisfiedConsumers.push(deferred); + return deferred.p; + } + } +} + +/** + * Important difference to AsyncIterableObject: + * If it is iterated two times, the second iterator will not see the values emitted by the first iterator. + */ +export class AsyncIterableProducer implements AsyncIterable { + private readonly _producerConsumer = new ProducerConsumer>(); + + constructor(executor: AsyncIterableExecutor) { + queueMicrotask(async () => { + const p = executor({ + emitOne: value => this._producerConsumer.produce({ ok: true, value: { done: false, value: value } }), + emitMany: values => { + for (const value of values) { + this._producerConsumer.produce({ ok: true, value: { done: false, value: value } }); + } + }, + reject: error => this._finishError(error), + }); + + if (!this._producerConsumer.hasFinalValue) { + try { + await p; + this._finishOk(); + } catch (error) { + this._finishError(error); + } + } + }); + } + + private _finishOk(): void { + this._producerConsumer.produceFinal({ ok: true, value: { done: true, value: undefined } }); + } + + private _finishError(error: Error): void { + this._producerConsumer.produceFinal({ ok: false, error: error }); + } + + private readonly _iterator: AsyncIterator = { + next: () => this._producerConsumer.consume(), + throw: async (e) => { + this._finishError(e); + return { done: true, value: undefined }; + }, + }; + + [Symbol.asyncIterator](): AsyncIterator { + return this._iterator; + } +} + //#endregion + +export const AsyncReaderEndOfStream = Symbol('AsyncReaderEndOfStream'); + +export class AsyncReader { + private _buffer: T[] = []; + private _atEnd = false; + + public get endOfStream(): boolean { return this._buffer.length === 0 && this._atEnd; } + private _extendBufferPromise: Promise | undefined; + + constructor( + private readonly _source: AsyncIterator + ) { + } + + public async read(): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await this._extendBuffer(); + } + if (this._buffer.length === 0) { + return AsyncReaderEndOfStream; + } + return this._buffer.shift()!; + } + + public async readWhile(predicate: (value: T) => boolean, callback: (element: T) => unknown): Promise { + do { + const piece = await this.peek(); + if (piece === AsyncReaderEndOfStream) { + break; + } + if (!predicate(piece)) { + break; + } + await this.read(); // consume + await callback(piece); + } while (true); + } + + public readBufferedOrThrow(): T | typeof AsyncReaderEndOfStream { + const value = this.peekBufferedOrThrow(); + this._buffer.shift(); + return value; + } + + public async consumeToEnd(): Promise { + while (!this.endOfStream) { + await this.read(); + } + } + + public async peek(): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await this._extendBuffer(); + } + if (this._buffer.length === 0) { + return AsyncReaderEndOfStream; + } + return this._buffer[0]; + } + + public peekBufferedOrThrow(): T | typeof AsyncReaderEndOfStream { + if (this._buffer.length === 0) { + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + throw new BugIndicatingError('No buffered elements'); + } + + return this._buffer[0]; + } + + public async peekTimeout(timeoutMs: number): Promise { + if (this._buffer.length === 0 && !this._atEnd) { + await raceTimeout(this._extendBuffer(), timeoutMs); + } + if (this._atEnd) { + return AsyncReaderEndOfStream; + } + if (this._buffer.length === 0) { + return undefined; + } + return this._buffer[0]; + } + + private _extendBuffer(): Promise { + if (this._atEnd) { + return Promise.resolve(); + } + + if (!this._extendBufferPromise) { + this._extendBufferPromise = (async () => { + const { value, done } = await this._source.next(); + this._extendBufferPromise = undefined; + if (done) { + this._atEnd = true; + } else { + this._buffer.push(value); + } + })(); + } + + return this._extendBufferPromise; + } +} diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index 272aa3594e5..c4b7fac045e 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -27,4 +27,5 @@ export const enum MarshalledId { LanguageModelTextPart, LanguageModelPromptTsxPart, LanguageModelDataPart, + ChatSessionContext, } diff --git a/src/vs/base/common/oauth.ts b/src/vs/base/common/oauth.ts index 45288003e34..77b639aeef5 100644 --- a/src/vs/base/common/oauth.ts +++ b/src/vs/base/common/oauth.ts @@ -8,6 +8,7 @@ import { decodeBase64 } from './buffer.js'; const WELL_KNOWN_ROUTE = '/.well-known'; export const AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH = `${WELL_KNOWN_ROUTE}/oauth-protected-resource`; export const AUTH_SERVER_METADATA_DISCOVERY_PATH = `${WELL_KNOWN_ROUTE}/oauth-authorization-server`; +export const OPENID_CONNECT_DISCOVERY_PATH = `${WELL_KNOWN_ROUTE}/openid-configuration`; export const AUTH_SCOPE_SEPARATOR = ' '; //#region types @@ -258,12 +259,6 @@ export interface IAuthorizationServerMetadata { code_challenge_methods_supported?: string[]; } -export interface IRequiredAuthorizationServerMetadata extends IAuthorizationServerMetadata { - authorization_endpoint: string; - token_endpoint: string; - registration_endpoint: string; -} - /** * Response from the dynamic client registration endpoint. */ @@ -697,7 +692,7 @@ export function isAuthorizationRegistrationErrorResponse(obj: unknown): obj is I //#endregion -export function getDefaultMetadataForUrl(authorizationServer: URL): IRequiredAuthorizationServerMetadata & IRequiredAuthorizationServerMetadata { +export function getDefaultMetadataForUrl(authorizationServer: URL): IAuthorizationServerMetadata { return { issuer: authorizationServer.toString(), authorization_endpoint: new URL('/authorize', authorizationServer).toString(), @@ -709,16 +704,6 @@ export function getDefaultMetadataForUrl(authorizationServer: URL): IRequiredAut }; } -export function getMetadataWithDefaultValues(metadata: IAuthorizationServerMetadata): IAuthorizationServerMetadata & IRequiredAuthorizationServerMetadata { - const issuer = new URL(metadata.issuer); - return { - ...metadata, - authorization_endpoint: metadata.authorization_endpoint ?? new URL('/authorize', issuer).toString(), - token_endpoint: metadata.token_endpoint ?? new URL('/token', issuer).toString(), - registration_endpoint: metadata.registration_endpoint ?? new URL('/register', issuer).toString(), - }; -} - /** * The grant types that we support */ @@ -751,14 +736,14 @@ export async function fetchDynamicRegistration(serverMetadata: IAuthorizationSer redirect_uris: [ 'https://insiders.vscode.dev/redirect', 'https://vscode.dev/redirect', - 'http://localhost/', - 'http://127.0.0.1/', + 'http://localhost', + 'http://127.0.0.1', // Added these for any server that might do // only exact match on the redirect URI even // though the spec says it should not care // about the port. - `http://localhost:${DEFAULT_AUTH_FLOW_PORT}/`, - `http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}/` + `http://localhost:${DEFAULT_AUTH_FLOW_PORT}`, + `http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}` ], scope: scopes?.join(AUTH_SCOPE_SEPARATOR), token_endpoint_auth_method: 'none', diff --git a/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts b/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts index 138732f44b2..10557a75864 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts @@ -24,9 +24,15 @@ export type ObsDebuggerApi = { getDerivedInfo(instanceId: ObsInstanceId): IDerivedObservableDetailedInfo; getAutorunInfo(instanceId: ObsInstanceId): IAutorunDetailedInfo; getObservableValueInfo(instanceId: ObsInstanceId): IObservableValueInfo; + setValue(instanceId: ObsInstanceId, jsonValue: unknown): void; getValue(instanceId: ObsInstanceId): unknown; + // For autorun and deriveds + rerun(instanceId: ObsInstanceId): void; + + logValue(instanceId: ObsInstanceId): void; + getTransactionState(): ITransactionState | undefined; } }; diff --git a/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts b/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts index 2405a11138c..513921408a5 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts @@ -135,7 +135,25 @@ export class DevToolsLogger implements IObservableLogger { } return undefined; - } + }, + logValue: (instanceId) => { + const obs = this._aliveInstances.get(instanceId); + if (obs && 'get' in obs) { + console.log('Logged Value:', obs.get()); + } else { + throw new BugIndicatingError('Observable is not supported'); + } + }, + rerun: (instanceId) => { + const obs = this._aliveInstances.get(instanceId); + if (obs instanceof Derived) { + obs.debugRecompute(); + } else if (obs instanceof AutorunObserver) { + obs.debugRerun(); + } else { + throw new BugIndicatingError('Observable is not supported'); + } + }, } }; }); diff --git a/src/vs/base/common/observableInternal/observables/derivedImpl.ts b/src/vs/base/common/observableInternal/observables/derivedImpl.ts index bbedd1518a6..011e9c76f18 100644 --- a/src/vs/base/common/observableInternal/observables/derivedImpl.ts +++ b/src/vs/base/common/observableInternal/observables/derivedImpl.ts @@ -400,6 +400,14 @@ export class Derived extends BaseObserv this._value = newValue as any; } + public debugRecompute(): void { + if (!this._isComputing) { + this._recompute(); + } else { + this._state = DerivedState.stale; + } + } + public setValue(newValue: T, tx: ITransaction, change: TChange): void { this._value = newValue; const observers = this._observers; diff --git a/src/vs/base/common/policy.ts b/src/vs/base/common/policy.ts index e814b6e8e12..bfe18e31959 100644 --- a/src/vs/base/common/policy.ts +++ b/src/vs/base/common/policy.ts @@ -5,6 +5,12 @@ export type PolicyName = string; +export enum PolicyTag { + Account = 'ACCOUNT', + MCP = 'MCP', + Preview = 'PREVIEW' +} + export interface IPolicy { /** @@ -23,24 +29,24 @@ export interface IPolicy { readonly description?: string; /** - * Is preview feature - */ - readonly previewFeature?: boolean; - - /** - * The value that a preview feature will use when its corresponding policy is active. + * The value that an ACCOUNT-based feature will use when its corresponding policy is active. * - * Only applicable when `previewFeature: true`. When a preview feature's policy is enabled, + * Only applicable when policy is tagged with ACCOUNT. When an account-based feature's policy is enabled, * this value determines what value the feature receives. * * For example: * - If `defaultValue: true`, the feature's setting is locked to `true` WHEN the policy is in effect. - * - If `defaultValue: 'foo'`, the feature's setting is locked to 'foo' WHEN the policy is in effect. + * - If `defaultValue: 'foo'`, the feature's setting is locked to 'foo' WHEN the policy is in effect. * * If omitted, 'false' is the assumed value. * - * Note: This is unrelated to VS Code settings and their default values. This specifically controls - * the value of a preview feature's setting when policy is overriding it. - */ + * Note: This is unrelated to the default value of the VS Code setting itself. This specifically controls + * the value of an account-based feature's setting WHEN the policy is overriding it. + */ readonly defaultValue?: string | number | boolean; + + /** + * Tags for categorizing policies + */ + readonly tags?: PolicyTag[]; } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 4f18173228a..0743b45df6c 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -337,12 +337,13 @@ export interface IDefaultChatAgent { readonly upgradePlanUrl: string; readonly signUpUrl: string; - readonly providerId: string; - readonly providerName: string; - readonly enterpriseProviderId: string; - readonly enterpriseProviderName: string; - readonly alternativeProviderId: string; - readonly alternativeProviderName: string; + readonly provider: { + default: { id: string; name: string }; + enterprise: { id: string; name: string }; + google: { id: string; name: string }; + apple: { id: string; name: string }; + }; + readonly providerUriSetting: string; readonly providerScopes: string[][]; diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 5d4d135ca89..58f94f52efc 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -1361,3 +1361,30 @@ export class InvisibleCharacters { } export const Ellipsis = '\u2026'; + +/** + * Convert a Unicode string to a string in which each 16-bit unit occupies only one byte + * + * From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa + */ +function toBinary(str: string): string { + const codeUnits = new Uint16Array(str.length); + for (let i = 0; i < codeUnits.length; i++) { + codeUnits[i] = str.charCodeAt(i); + } + let binary = ''; + const uint8array = new Uint8Array(codeUnits.buffer); + for (let i = 0; i < uint8array.length; i++) { + binary += String.fromCharCode(uint8array[i]); + } + return binary; +} + +/** + * Version of the global `btoa` function that handles multi-byte characters instead + * of throwing an exception. + */ + +export function multibyteAwareBtoa(str: string): string { + return btoa(toBinary(str)); +} diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index cf76e466400..8c03f478170 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -104,6 +104,23 @@ export interface IDirent { async function readdir(path: string): Promise; async function readdir(path: string, options: { withFileTypes: true }): Promise; async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { + try { + return await doReaddir(path, options); + } catch (error) { + // TODO@bpasero workaround for #252361 that should be removed + // once the upstream issue in node.js is resolved + if (error.code === 'ENOENT' && isWindows && isRootOrDriveLetter(path)) { + try { + return await doReaddir(path.slice(0, -1), options); + } catch (e) { + // ignore + } + } + throw error; + } +} + +async function doReaddir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); } diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 92aed203498..345efd026e8 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { $, h, multibyteAwareBtoa, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement, SafeTriangle } from '../../browser/dom.js'; +import { $, h, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement, SafeTriangle } from '../../browser/dom.js'; import { asCssValueWithDefault } from '../../../base/browser/cssValue.js'; import { ensureCodeWindow, isAuxiliaryWindow, mainWindow } from '../../browser/window.js'; import { DeferredPromise, timeout } from '../../common/async.js'; @@ -76,12 +76,6 @@ suite('dom', () => { assert(!element.classList.contains('bar')); }); - test('multibyteAwareBtoa', () => { - assert.ok(multibyteAwareBtoa('hello world').length > 0); - assert.ok(multibyteAwareBtoa('平仮名').length > 0); - assert.ok(multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 - }); - suite('$', () => { test('should build simple nodes', () => { const div = $('div'); diff --git a/src/vs/base/test/browser/domSanitize.test.ts b/src/vs/base/test/browser/domSanitize.test.ts new file mode 100644 index 00000000000..f50b7c190b7 --- /dev/null +++ b/src/vs/base/test/browser/domSanitize.test.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; +import { sanitizeHtml } from '../../browser/domSanitize.js'; +import * as assert from 'assert'; +import { Schemas } from '../../common/network.js'; + +suite('DomSanitize', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('removes unsupported tags by default', () => { + const html = '

safecontent
'; + const result = sanitizeHtml(html); + const str = result.toString(); + + assert.ok(str.includes('
')); + assert.ok(str.includes('safe')); + assert.ok(str.includes('content')); + assert.ok(!str.includes('