diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 377caec98ce0..412a40107b42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,7 @@ jobs: prebuild: name: Pre-Build checks runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index ce8a3eaf43bc..fe788d7c886d 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -82,10 +82,12 @@ bundle_vscode() { rsync "$VSCODE_SRC_PATH/resources/linux/code.png" "$VSCODE_OUT_PATH/resources/linux/code.png" rsync "$VSCODE_SRC_PATH/resources/web/callback.html" "$VSCODE_OUT_PATH/resources/web/callback.html" - # Adds the commit and date to product.json + # Add the commit and date and enable telemetry. This just makes telemetry + # available; telemetry can still be disabled by flag or setting. jq --slurp '.[0] * .[1]' "$VSCODE_SRC_PATH/product.json" <( cat << EOF { + "enableTelemetry": true, "commit": "$(git rev-parse HEAD)", "date": $(jq -n 'now | todate') } diff --git a/ci/build/build-standalone-release.sh b/ci/build/build-standalone-release.sh index c91debe1ee8a..f12f240d0087 100755 --- a/ci/build/build-standalone-release.sh +++ b/ci/build/build-standalone-release.sh @@ -27,6 +27,12 @@ main() { cd "$RELEASE_PATH" yarn --production --frozen-lockfile + + # HACK: the version of Typescript vscode 1.57 uses in extensions/ + # leaves a few stray symlinks. Clean them up so nfpm does not fail. + # Remove this line when its no longer needed. + + rm -fr "$RELEASE_PATH/lib/vscode/extensions/node_modules/.bin" } main "$@" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d4bf6f07a10b..f49f07daa038 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -198,16 +198,62 @@ a Git subtree to fork and modify VS Code. This code lives under Some noteworthy changes in our version of VS Code include: -- Adding our build file, which includes our code and VS Code's web code -- Allowing multiple extension directories (both user and built-in) -- Modifying the loader, WebSocket, webview, service worker, and asset requests to - use the URL of the page as a base (and TLS, if necessary for the WebSocket) -- Sending client-side telemetry through the server -- Allowing modification of the display language -- Making it possible for us to load code on the client -- Making it possible to install extensions of any kind -- Fixing issue with getting disconnected when your machine sleeps or hibernates -- Adding connection type to web socket query parameters +- Adding our build file, [`lib/vscode/coder.js`](../lib/vscode/coder.js), which includes build steps specific to code-server +- Node.js version detection changes in [`build/lib/node.ts`](../lib/vscode/build/lib/node.ts) and [`build/lib/util.ts`](../lib/vscode/build/lib/util.ts) +- Allowing extra extension directories + - Added extra arguments to [`src/vs/platform/environment/common/argv.ts`](../lib/vscode/src/vs/platform/environment/common/argv.ts) and to [`src/vs/platform/environment/node/argv.ts`](../lib/vscode/src/vs/platform/environment/node/argv.ts) + - Added extra environment state to [`src/vs/platform/environment/common/environment.ts`](../lib/vscode/src/vs/platform/environment/common/environment.ts); + - Added extra getters to [`src/vs/platform/environment/common/environmentService.ts`](../lib/vscode/src/vs/platform/environment/common/environmentService.ts) + - Added extra scanning paths to [`src/vs/platform/extensionManagement/node/extensionsScanner.ts`](../lib/vscode/src/vs/platform/extensionManagement/node/extensionsScanner.ts) +- Additions/removals from [`package.json`](../lib/vscode/package.json): + - Removing `electron`, `keytar` and `native-keymap` to avoid pulling in desktop dependencies during build on Linux + - Removing `gulp-azure-storage` and `gulp-tar` (unsued in our build process, may pull in outdated dependencies) + - Adding `proxy-agent`, `proxy-from-env` (for proxying) and `rimraf` (used during build/install steps) +- Adding our branding/custom URLs/version: + - [`product.json`](../lib/vscode/product.json) + - [`src/vs/base/common/product.ts`](../lib/vscode/src/vs/base/common/product.ts) + - [`src/vs/workbench/browser/parts/dialogs/dialogHandler.ts`](../lib/vscode/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts) + - [`src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts`](../lib/vscode/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts) + - [`src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts`](../lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts) +- Removing azure/macOS signing related dependencies from [`build/package.json`](../lib/vscode/build/package.json) +- Modifying `.gitignore` to allow us to add files to `src/vs/server` and modifying `.eslintignore` to ignore lint on the shared files below (we use different formatter settings than VS Code). +- Sharing some files with our codebase via symlinks: + - [`src/vs/base/common/ipc.d.ts`](../lib/vscode/src/vs/base/common/ipc.d.ts) points to [`typings/ipc.d.ts`](../typings/ipc.d.ts) + - [`src/vs/base/common/util.ts`](../lib/vscode/src/vs/base/common/util.ts) points to [`src/common/util.ts`](../src/common/util.ts) + - [`src/vs/base/node/proxy_agent.ts`](../lib/vscode/src/vs/base/node/proxy_agent.ts) points to [`src/node/proxy_agent.ts`](../src/node/proxy_agent.ts) +- Allowing socket changes by adding `setSocket` in [`src/vs/base/parts/ipc/common/ipc.net.ts`](../lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts) + - We use this for connection persistence in our server-side code. +- Added our server-side Node.JS code to `src/vs/server`. + - This code includes the logic to spawn the various services (extension host, terminal, etc.) and some glue +- Added [`src/vs/workbench/browser/client.ts`](../lib/vscode/src/vs/workbench/browser/client.ts) to hold some server customizations. + - Includes the functionality for the Log Out command and menu item + - Also, imported and called `initialize` from the main web file, [`src/vs/workbench/browser/web.main.ts`](../lib/vscode/src/vs/workbench/browser/web.main.ts) +- Added a (hopefully temporary) hotfix to [`src/vs/workbench/common/resources.ts`](../lib/vscode/src/vs/workbench/common/resources.ts) to get context menu actions working for the Git integration. +- Added connection type to WebSocket query parameters in [`src/vs/platform/remote/common/remoteAgentConnection.ts`](../lib/vscode/src/vs/platform/remote/common/remoteAgentConnection.ts) +- Added `CODE_SERVER*` variables to the sanitization list in [`src/vs/base/common/processes.ts`](../lib/vscode/src/vs/base/common/processes.ts) +- Fix localization support: + - Added file [`src/vs/workbench/services/localizations/browser/localizationsService.ts`](../lib/vscode/src/vs/workbench/services/localizations/browser/localizationsService.ts). + - Modified file [`src/vs/base/common/platform.ts`](../lib/vscode/src/vs/base/common/platform.ts) + - Modified file [`src/vs/base/node/languagePacks.js`](../lib/vscode/src/vs/base/node/languagePacks.js) +- Added code to allow server to inject settings to [`src/vs/platform/product/common/product.ts`](../lib/vscode/src/vs/platform/product/common/product.ts) +- Extension fixes: + - Avoid disabling extensions by extensionKind in [`src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts`](../lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts) (Needed for vscode-icons) + - Remove broken symlinks in [`extensions/postinstall.js`](../lib/vscode/extensions/postinstall.js) + - Add tip about extension gallery in [`src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts`](../lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts) + - Use our own server for GitHub authentication in [`extensions/github-authentication/src/githubServer.ts`](../lib/vscode/extensions/github-authentication/src/githubServer.ts) + - Settings persistence on the server in [`src/vs/workbench/services/environment/browser/environmentService.ts`](../lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts) + - Add extension install fallback in [`src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts`](../lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts) + - Add proxy-agent monkeypatch and keep extension host indefinitely running in [`src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts`](../lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts) + - Patch build system to avoid removing extension dependencies for `yarn global add` users in [`build/lib/extensions.ts`](../lib/vscode/build/lib/extensions.ts) + - Allow all extensions to use proposed APIs in [`src/vs/workbench/services/environment/browser/environmentService.ts`](../lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts) + - Make storage writes async to allow extensions to wait for them to complete in [`src/vs/platform/storage/common/storage.ts`](../lib/vscode/src/vs/platform/storage/common/storage.ts) +- Specify webview path in [`src/vs/code/browser/workbench/workbench.ts`](../lib/vscode/src/vs/code/browser/workbench/workbench.ts) +- URL readability improvements for folder/workspace in [`src/vs/code/browser/workbench/workbench.ts`](../lib/vscode/src/vs/code/browser/workbench/workbench.ts) +- Socket/Authority-related fixes (for remote proxying etc.): + - [`src/vs/code/browser/workbench/workbench.ts`](../lib/vscode/src/vs/code/browser/workbench/workbench.ts) + - [`src/vs/platform/remote/browser/browserSocketFactory.ts`](../lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts) + - [`src/vs/base/common/network.ts`](../lib/vscode/src/vs/base/common/network.ts) +- Added code to write out IPC path in [`src/vs/workbench/api/node/extHostCLIServer.ts`](../lib/vscode/src/vs/workbench/api/node/extHostCLIServer.ts) As the web portion of VS Code matures, we'll be able to shrink and possibly eliminate our modifications. In the meantime, upgrading the VS Code version requires diff --git a/lib/vscode/.devcontainer/README.md b/lib/vscode/.devcontainer/README.md index 8262d411570b..827166823d73 100644 --- a/lib/vscode/.devcontainer/README.md +++ b/lib/vscode/.devcontainer/README.md @@ -1,14 +1,14 @@ # Code - OSS Development Container -This repository includes configuration for a development container for working with Code - OSS in an isolated local container or using [GitHub Codespaces](https://github.com/features/codespaces). +This repository includes configuration for a development container for working with Code - OSS in a local container or using [GitHub Codespaces](https://github.com/features/codespaces). -> **Tip:** The default VNC password is `vscode`. The VNC server runs on port `5901` with a web client at `6080`. For better performance, we recommend using a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/). Applications like the macOS Screen Sharing app will not perform as well. +> **Tip:** The default VNC password is `vscode`. The VNC server runs on port `5901` and a web client is available on port `6080`. ## Quick start - local 1. Install Docker Desktop or Docker for Linux on your local machine. (See [docs](https://aka.ms/vscode-remote/containers/getting-started) for additional details.) -2. **Important**: Docker needs at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run full build. If you on macOS, or using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item, going to **Preferences/Settings > Resources > Advanced**. +2. **Important**: Docker needs at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run a full build. If you are on macOS, or are using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item and going to **Preferences/Settings > Resources > Advanced**. > **Note:** The [Resource Monitor](https://marketplace.visualstudio.com/items?itemName=mutantdino.resourcemonitor) extension is included in the container so you can keep an eye on CPU/Memory in the status bar. @@ -16,53 +16,56 @@ This repository includes configuration for a development container for working w ![Image of Remote - Containers extension](https://microsoft.github.io/vscode-remote-release/images/remote-containers-extn.png) - > Note that the Remote - Containers extension requires the Visual Studio Code distribution of Code - OSS. See the [FAQ](https://aka.ms/vscode-remote/faq/license) for details. + > **Note:** The Remote - Containers extension requires the Visual Studio Code distribution of Code - OSS. See the [FAQ](https://aka.ms/vscode-remote/faq/license) for details. -4. Press Ctrl/Cmd + Shift + P and select **Remote-Containers: Clone Repository in Container Volume...**. +4. Press Ctrl/Cmd + Shift + P or F1 and select **Remote-Containers: Clone Repository in Container Volume...**. - > **Tip:** While you can use your local source tree instead, operations like `yarn install` can be slow on macOS or using the Hyper-V engine on Windows. We recommend the "clone repository in container" approach instead since it uses "named volume" rather than the local filesystem. + > **Tip:** While you can use your local source tree instead, operations like `yarn install` can be slow on macOS or when using the Hyper-V engine on Windows. We recommend the "clone repository in container" approach instead since it uses "named volume" rather than the local filesystem. 5. Type `https://github.com/microsoft/vscode` (or a branch or PR URL) in the input box and press Enter. -6. After the container is running, open a web browser and go to [http://localhost:6080](http://localhost:6080) or use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. +6. After the container is running, open a web browser and go to [http://localhost:6080](http://localhost:6080), or use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. -Anything you start in VS Code or the integrated terminal will appear here. +Anything you start in VS Code, or the integrated terminal, will appear here. Next: **[Try it out!](#try-it)** ## Quick start - GitHub Codespaces -> **IMPORTANT:** You need to use a "Standard" sized codespace or larger (4-core, 8GB) since VS Code needs 6GB of RAM to compile. This is now the default for GitHub Codespaces, but do not downgrade to "Basic" unless you do not intend to compile. +1. From the [microsoft/vscode GitHub repository](https://github.com/microsoft/vscode), click on the **Code** dropdown, select **Open with Codespaces**, and then click on **New codespace**. If prompted, select the **Standard** machine size (which is also the default). -1. From the [microsoft/vscode GitHub repository](https://github.com/microsoft/vscode), click on the **Code** dropdown, select **Open with Codespaces**, and the **New codespace** + > **Note:** You will not see these options within GitHub if you are not in the Codespaces beta. - > Note that you will not see these options if you are not in the beta yet. +2. After the codespace is up and running in your browser, press Ctrl/Cmd + Shift + P or F1 and select **Ports: Focus on Ports View**. -2. After the codespace is up and running in your browser, press F1 and select **Ports: Focus on Ports View**. +3. You should see **VNC web client (6080)** under in the list of ports. Select the line and click on the globe icon to open it in a browser tab. -3. You should see port `6080` under **Forwarded Ports**. Select the line and click on the globe icon to open it in a browser tab. - - > If you do not see port `6080`, press F1, select **Forward a Port** and enter port `6080`. + > **Tip:** If you do not see the port, Ctrl/Cmd + Shift + P or F1, select **Forward a Port** and enter port `6080`. 4. In the new tab, you should see noVNC. Click **Connect** and enter `vscode` as the password. -Anything you start in VS Code or the integrated terminal will appear here. +Anything you start in VS Code, or the integrated terminal, will appear here. Next: **[Try it out!](#try-it)** ### Using VS Code with GitHub Codespaces -You will likely see better performance when accessing the codespace you created from VS Code since you can use a[VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/). Here's how to do it. +You may see improved VNC responsiveness when accessing a codespace from VS Code client since you can use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/). Here's how to do it. + +1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces). -1. [Create a codespace](#quick-start---github-codespaces) if you have not already. + > **Note:** The GitHub Codespaces extension requires the Visual Studio Code distribution of Code - OSS. -2. Set up [VS Code for use with GitHub Codespaces](https://docs.github.com/github/developing-online-with-codespaces/using-codespaces-in-visual-studio-code) +2. After the VS Code is up and running, press Ctrl/Cmd + Shift + P or F1, choose **Codespaces: Create New Codespace**, and use the following settings: + - `microsoft/vscode` for the repository. + - Select any branch (e.g. **main**) - you select a different one later. + - Choose **Standard** (4-core, 8GB) as the size. -3. After the VS Code is up and running, press F1, choose **Codespaces: Connect to Codespace**, and select the codespace you created. +4. After you have connected to the codespace, you can use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. -4. After you've connected to the codespace, use a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to `localhost:5901` and enter `vscode` as the password. + > **Tip:** You may also need change your VNC client's **Picture Quaility** setting to **High** to get a full color desktop. -5. Anything you start in VS Code or the integrated terminal will appear here. +5. Anything you start in VS Code, or the integrated terminal, will appear here. Next: **[Try it out!](#try-it)** @@ -70,20 +73,18 @@ Next: **[Try it out!](#try-it)** This container uses the [Fluxbox](http://fluxbox.org/) window manager to keep things lean. **Right-click on the desktop** to see menu options. It works with GNOME and GTK applications, so other tools can be installed if needed. -Note you can also set the resolution from the command line by typing `set-resolution`. +> **Note:** You can also set the resolution from the command line by typing `set-resolution`. To start working with Code - OSS, follow these steps: -1. In your local VS Code, open a terminal (Ctrl/Cmd + Shift + \`) and type the following commands: +1. In your local VS Code client, open a terminal (Ctrl/Cmd + Shift + \`) and type the following commands: ```bash yarn install bash scripts/code.sh ``` - Note that a previous run of `yarn install` will already be cached, so this step should simply pick up any recent differences. - -2. After the build is complete, open a web browser or a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to the desktop environnement as described in the quick start and enter `vscode` as the password. +2. After the build is complete, open a web browser or a [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) to connect to the desktop environment as described in the quick start and enter `vscode` as the password. 3. You should now see Code - OSS! @@ -91,7 +92,7 @@ Next, let's try debugging. 1. Shut down Code - OSS by clicking the box in the upper right corner of the Code - OSS window through your browser or VNC viewer. -2. Go to your local VS Code client, and use Run / Debug view to launch the **VS Code** configuration. (Typically the default, so you can likely just press F5). +2. Go to your local VS Code client, and use the **Run / Debug** view to launch the **VS Code** configuration. (Typically the default, so you can likely just press F5). > **Note:** If launching times out, you can increase the value of `timeout` in the "VS Code", "Attach Main Process", "Attach Extension Host", and "Attach to Shared Process" configurations in [launch.json](../.vscode/launch.json). However, running `scripts/code.sh` first will set up Electron which will usually solve timeout issues. diff --git a/lib/vscode/.devcontainer/devcontainer.json b/lib/vscode/.devcontainer/devcontainer.json index 3b82cd9028d2..d66344eccf65 100644 --- a/lib/vscode/.devcontainer/devcontainer.json +++ b/lib/vscode/.devcontainer/devcontainer.json @@ -3,20 +3,26 @@ // Image contents: https://github.com/microsoft/vscode-dev-containers/blob/master/repository-containers/images/github.com/microsoft/vscode/.devcontainer/base.Dockerfile "image": "mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:branch-main", - - "workspaceMount": "source=${localWorkspaceFolder},target=/home/node/workspace/vscode,type=bind,consistency=cached", - "workspaceFolder": "/home/node/workspace/vscode", "overrideCommand": false, "runArgs": [ "--init", "--security-opt", "seccomp=unconfined"], "settings": { - "terminal.integrated.shell.linux": "/bin/bash", "resmon.show.battery": false, "resmon.show.cpufreq": false }, - // noVNC, VNC, debug ports - "forwardPorts": [6080, 5901, 9222], + // noVNC, VNC + "forwardPorts": [6080, 5901], + "portsAttributes": { + "6080": { + "label": "VNC web client (noVNC)", + "onAutoForward": "silent" + }, + "5901": { + "label": "VNC TCP port", + "onAutoForward": "silent" + } + }, "extensions": [ "dbaeumer.vscode-eslint", diff --git a/lib/vscode/.eslintignore b/lib/vscode/.eslintignore index b67a816156a4..f9c117426c75 100644 --- a/lib/vscode/.eslintignore +++ b/lib/vscode/.eslintignore @@ -17,6 +17,7 @@ **/extensions/typescript-basics/test/colorize-fixtures/** **/extensions/**/dist/** # These are code-server code symlinks. +src/vs/base/common/util.ts +src/vs/base/common/ipc.d.ts src/vs/base/node/proxy_agent.ts -src/vs/ipc.d.ts -src/vs/server/common/util.ts +src/vs/server/uriTransformer.ts diff --git a/lib/vscode/.eslintrc.json b/lib/vscode/.eslintrc.json index e7ff9be00a1f..dac80d296476 100644 --- a/lib/vscode/.eslintrc.json +++ b/lib/vscode/.eslintrc.json @@ -62,7 +62,7 @@ "code-no-standalone-editor": "warn", "code-no-unexternalized-strings": "warn", "code-layering": [ - "off", + "warn", { "common": [], "node": [ @@ -88,7 +88,7 @@ } ], "code-import-patterns": [ - "off", + "warn", // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!! Do not relax these rules !!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -501,7 +501,7 @@ "**/vs/platform/**", "**/vs/editor/**", "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", - "vs/workbench/contrib/files/common/editors/fileEditorInput", + "vs/workbench/contrib/files/browser/editors/fileEditorInput", "**/vs/workbench/services/**", "**/vs/workbench/test/**", "*" // node modules @@ -807,6 +807,7 @@ "**/vs/platform/**/{common,node}/**", "**/vs/workbench/**/{common,node}/**", "**/vs/server/**", + "@coder/logger", // NOTE@coder: add logger "*" // node modules ] }, diff --git a/lib/vscode/.github/ISSUE_TEMPLATE/bug_report.md b/lib/vscode/.github/ISSUE_TEMPLATE/bug_report.md index b40ba5ddd483..8a44ce8c7ac1 100644 --- a/lib/vscode/.github/ISSUE_TEMPLATE/bug_report.md +++ b/lib/vscode/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,6 +8,11 @@ about: Create a report to help us improve + +Does this issue occur when all extensions are disabled?: Yes/No + + + - VS Code Version: - OS Version: @@ -15,9 +20,3 @@ Steps to Reproduce: 1. 2. - - -Does this issue occur when all extensions are disabled?: Yes/No - - - diff --git a/lib/vscode/.github/classifier.json b/lib/vscode/.github/classifier.json index 5adabb9aa9c9..1783b82c8797 100644 --- a/lib/vscode/.github/classifier.json +++ b/lib/vscode/.github/classifier.json @@ -1,16 +1,17 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vscode-github-triage-actions/master/classifier-deep/apply/apply-labels/deep-classifier-config.schema.json", - "vacation": [], + "vacation": ["RMacfarlane"], "assignees": { "JacksonKearl": {"accuracy": 0.5} }, "labels": { "L10N": {"assign": []}, "VIM": {"assign": []}, + "accessibility": { "assign": ["isidorn"]}, "api": {"assign": ["jrieken"]}, "api-finalization": {"assign": []}, "api-proposal": {"assign": ["jrieken"]}, - "authentication": {"assign": ["RMacfarlane"]}, + "authentication": {"assign": ["TylerLeonhardt"]}, "breadcrumbs": {"assign": ["jrieken"]}, "callhierarchy": {"assign": ["jrieken"]}, "code-lens": {"assign": ["jrieken"]}, @@ -20,8 +21,7 @@ "context-keys": {"assign": []}, "css-less-scss": {"assign": ["aeschli"]}, "custom-editors": {"assign": ["mjbvz"]}, - "debug": {"assign": ["isidorn"]}, - "debug-console": {"assign": ["isidorn"]}, + "debug": {"assign": ["weinand"]}, "dialogs": {"assign": ["sbatten"]}, "diff-editor": {"assign": []}, "dropdown": {"assign": []}, @@ -81,14 +81,15 @@ "icon-brand": {"assign": []}, "icons-product": {"assign": ["misolori"]}, "install-update": {"assign": []}, - "integrated-terminal": {"assign": ["meganrogge"]}, - "integrated-terminal-conpty": {"assign": ["meganrogge"]}, - "integrated-terminal-links": {"assign": ["meganrogge"]}, + "terminal": {"assign": ["meganrogge"]}, + "terminal-conpty": {"assign": ["meganrogge"]}, + "terminal-links": {"assign": ["meganrogge"]}, + "terminal-external": {"assign": ["meganrogge"]}, "integration-test": {"assign": []}, "intellisense-config": {"assign": []}, "ipc": {"assign": ["joaomoreno"]}, "issue-bot": {"assign": ["chrmarti"]}, - "issue-reporter": {"assign": ["RMacfarlane"]}, + "issue-reporter": {"assign": ["TylerLeonhardt"]}, "javascript": {"assign": ["mjbvz"]}, "json": {"assign": ["aeschli"]}, "keybindings": {"assign": []}, @@ -113,7 +114,7 @@ "php": {"assign": ["roblourens"]}, "portable-mode": {"assign": ["joaomoreno"]}, "proxy": {"assign": []}, - "quick-pick": {"assign": ["chrmarti"]}, + "quick-pick": {"assign": ["TylerLeonhardt"]}, "references-viewlet": {"assign": ["jrieken"]}, "release-notes": {"assign": []}, "remote": {"assign": []}, @@ -152,7 +153,7 @@ "web": {"assign": ["bpasero"]}, "webview": {"assign": ["mjbvz"]}, "workbench-cli": {"assign": []}, - "workbench-diagnostics": {"assign": ["RMacfarlane"]}, + "workbench-diagnostics": {"assign": ["Tyriar"]}, "workbench-dnd": {"assign": ["bpasero"]}, "workbench-editor-grid": {"assign": ["sbatten"]}, "workbench-editors": {"assign": ["bpasero"]}, diff --git a/lib/vscode/.github/commands.json b/lib/vscode/.github/commands.json index de0643d56c92..388a9c3dbb33 100644 --- a/lib/vscode/.github/commands.json +++ b/lib/vscode/.github/commands.json @@ -90,7 +90,7 @@ "@author" ], "action": "updateLabels", - "addLabel": "z-author-verified", + "addLabel": "verified", "removeLabel": "author-verification-requested", "requireLabel": "author-verification-requested", "disallowLabel": "unreleased" @@ -133,6 +133,18 @@ "action": "updateLabels", "addLabel": "~needs more info" }, + { + "type": "comment", + "name": "needsPerfInfo", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "addLabel": "needs more info", + "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" + }, { "type": "comment", "name": "jsDebugLogs", diff --git a/lib/vscode/.github/subscribers.json b/lib/vscode/.github/subscribers.json index 7ee6e5cdadd3..25c676a47c74 100644 --- a/lib/vscode/.github/subscribers.json +++ b/lib/vscode/.github/subscribers.json @@ -4,6 +4,7 @@ "rchiodo", "greazer", "donjayamanne", - "jilljac" + "jilljac", + "IanMatthewHuff" ] } diff --git a/lib/vscode/.github/workflows/ci.yml b/lib/vscode/.github/workflows/ci.yml index d08dac3c03b9..faac7802508d 100644 --- a/lib/vscode/.github/workflows/ci.yml +++ b/lib/vscode/.github/workflows/ci.yml @@ -244,6 +244,9 @@ jobs: - name: Run Valid Layers Checks run: yarn valid-layers-check + - name: Compile /build/ + run: yarn --cwd build compile + - name: Run Monaco Editor Checks run: yarn monaco-compile-check diff --git a/lib/vscode/.gitignore b/lib/vscode/.gitignore index c53681396d35..ed42d401d9cd 100644 --- a/lib/vscode/.gitignore +++ b/lib/vscode/.gitignore @@ -7,7 +7,8 @@ node_modules/ extensions/**/dist/ /out*/ /extensions/**/out/ -# src/vs/server NOTE@coder: So our code isn't ignored. +# NOTE@coder: remove to provide our own server +# src/vs/server resources/server build/node_modules coverage/ diff --git a/lib/vscode/.vscode/notebooks/api.github-issues b/lib/vscode/.vscode/notebooks/api.github-issues index b9e25a7c914f..112920287601 100644 --- a/lib/vscode/.vscode/notebooks/api.github-issues +++ b/lib/vscode/.vscode/notebooks/api.github-issues @@ -2,37 +2,31 @@ { "kind": 1, "language": "markdown", - "value": "#### Config", - "editable": true + "value": "#### Config" }, { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"April 2021\"", - "editable": true + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"May 2021\"" }, { "kind": 1, "language": "markdown", - "value": "### Finalization", - "editable": true + "value": "### Finalization" }, { "kind": 2, "language": "github-issues", - "value": "$repo $milestone label:api-finalization", - "editable": true + "value": "$repo $milestone label:api-finalization" }, { "kind": 1, "language": "markdown", - "value": "### Proposals", - "editable": true + "value": "### Proposals" }, { "kind": 2, "language": "github-issues", - "value": "$repo $milestone is:open label:api-proposal ", - "editable": true + "value": "$repo $milestone is:open label:api-proposal " } ] \ No newline at end of file diff --git a/lib/vscode/.vscode/notebooks/endgame.github-issues b/lib/vscode/.vscode/notebooks/endgame.github-issues index 881af2c14b43..bc2fba29ddf1 100644 --- a/lib/vscode/.vscode/notebooks/endgame.github-issues +++ b/lib/vscode/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-remotehub\n\n$MILESTONE=milestone:\"April 2021\"" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-remotehub\n\n$MILESTONE=milestone:\"May 2021\"" }, { "kind": 1, diff --git a/lib/vscode/.vscode/notebooks/my-endgame.github-issues b/lib/vscode/.vscode/notebooks/my-endgame.github-issues index c435ee77500b..aad3a8db3a90 100644 --- a/lib/vscode/.vscode/notebooks/my-endgame.github-issues +++ b/lib/vscode/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remotehub\n\n$MILESTONE=milestone:\"April 2021\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remotehub\n\n$MILESTONE=milestone:\"May 2021\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -157,7 +157,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed sort:updated-asc label:bug -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:btholt -author:chrisdias -author:chrmarti -author:Chuxel -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:eamodio -author:egamma -author:fiveisprime -author:gregvanl -author:isidorn -author:ItalyPaleAle -author:JacksonKearl -author:joaomoreno -author:jrieken -author:kieferrm -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:ornellaalt -author:orta -author:rebornix -author:RMacfarlane -author:roblourens -author:rzhao271 -author:sana-ajani -author:sandy081 -author:sbatten -author:stevencl -author:Tyriar -author:weinand -author:TylerLeonhardt -author:lramos15" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed sort:updated-asc label:bug -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:btholt -author:chrisdias -author:chrmarti -author:Chuxel -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:eamodio -author:egamma -author:fiveisprime -author:gregvanl -author:isidorn -author:ItalyPaleAle -author:JacksonKearl -author:joaomoreno -author:jrieken -author:kieferrm -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:ornellaalt -author:orta -author:rebornix -author:RMacfarlane -author:roblourens -author:rzhao271 -author:sana-ajani -author:sandy081 -author:sbatten -author:stevencl -author:Tyriar -author:weinand -author:TylerLeonhardt -author:lramos15 -author:hediet" }, { "kind": 1, diff --git a/lib/vscode/.vscode/notebooks/my-work.github-issues b/lib/vscode/.vscode/notebooks/my-work.github-issues index 4e288133b7a2..77ca0e0443b5 100644 --- a/lib/vscode/.vscode/notebooks/my-work.github-issues +++ b/lib/vscode/.vscode/notebooks/my-work.github-issues @@ -2,115 +2,96 @@ { "kind": 1, "language": "markdown", - "value": "##### `Config`: This should be changed every month/milestone", - "editable": true + "value": "##### `Config`: This should be changed every month/milestone" }, { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"April 2021\"", - "editable": true + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"May 2021\"" }, { "kind": 1, "language": "github-issues", - "value": "## Milestone Work", - "editable": true + "value": "## Milestone Work" }, { "kind": 2, "language": "github-issues", - "value": "$repos $milestone assignee:@me is:open", - "editable": true + "value": "$repos $milestone assignee:@me is:open" }, { "kind": 1, "language": "github-issues", - "value": "## Bugs, Debt, Features...", - "editable": true + "value": "## Bugs, Debt, Features..." }, { "kind": 1, "language": "markdown", - "value": "#### My Bugs", - "editable": true + "value": "#### My Bugs" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:bug", - "editable": true + "value": "$repos assignee:@me is:open label:bug" }, { "kind": 1, "language": "markdown", - "value": "#### Debt & Engineering", - "editable": true + "value": "#### Debt & Engineering" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering", - "editable": true + "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering" }, { "kind": 1, "language": "markdown", - "value": "#### Performance ๐ŸŒ ๐Ÿ”œ ๐ŸŽ", - "editable": true + "value": "#### Performance ๐ŸŒ ๐Ÿ”œ ๐ŸŽ" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak", - "editable": true + "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak" }, { "kind": 1, "language": "markdown", - "value": "#### Feature Requests", - "editable": true + "value": "#### Feature Requests" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:feature-request milestone:Backlog sort:reactions-+1-desc", - "editable": true + "value": "$repos assignee:@me is:open label:feature-request milestone:Backlog sort:reactions-+1-desc" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open milestone:\"Backlog Candidates\"", - "editable": true + "value": "$repos assignee:@me is:open milestone:\"Backlog Candidates\"" }, { "kind": 1, "language": "markdown", - "value": "### Personal Inbox\n", - "editable": true + "value": "### Personal Inbox\n" }, { "kind": 1, "language": "markdown", - "value": "\n#### Missing Type label", - "editable": true + "value": "\n#### Missing Type label" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream", - "editable": true + "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream" }, { "kind": 1, "language": "markdown", - "value": "#### Not Actionable", - "editable": true + "value": "#### Not Actionable" }, { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:\"needs more info\"", - "editable": true + "value": "$repos assignee:@me is:open label:\"needs more info\"" } ] \ No newline at end of file diff --git a/lib/vscode/.vscode/tasks.json b/lib/vscode/.vscode/tasks.json index 8fb5cb440b79..c0b290afa729 100644 --- a/lib/vscode/.vscode/tasks.json +++ b/lib/vscode/.vscode/tasks.json @@ -55,39 +55,11 @@ } } }, - { - "type": "npm", - "script": "watch-extension-mediad", - "label": "Ext Media - Build", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "buildWatchers" - }, - "problemMatcher": { - "owner": "typescript", - "applyTo": "closedDocuments", - "fileLocation": [ - "absolute" - ], - "pattern": { - "regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "beginsPattern": "Starting compilation", - "endsPattern": "Finished compilation" - } - } - }, { "label": "VS Code - Build", "dependsOn": [ "Core - Build", - "Ext - Build", - "Ext Media - Build", + "Ext - Build" ], "group": { "kind": "build", @@ -102,7 +74,8 @@ "group": "build", "presentation": { "reveal": "never", - "group": "buildKillers" + "group": "buildKillers", + "close": true }, "problemMatcher": "$tsc" }, @@ -113,18 +86,8 @@ "group": "build", "presentation": { "reveal": "never", - "group": "buildKillers" - }, - "problemMatcher": "$tsc" - }, - { - "type": "npm", - "script": "kill-watch-extension-mediad", - "label": "Kill Ext Media - Build", - "group": "build", - "presentation": { - "reveal": "never", - "group": "buildKillers" + "group": "buildKillers", + "close": true }, "problemMatcher": "$tsc" }, @@ -132,8 +95,7 @@ "label": "Kill VS Code - Build", "dependsOn": [ "Kill Core - Build", - "Kill Ext - Build", - "Kill Ext Media - Build", + "Kill Ext - Build" ], "group": "build", "problemMatcher": [] @@ -238,7 +200,8 @@ "command": "node build/lib/preLaunch.js", "label": "Ensure Prelaunch Dependencies", "presentation": { - "reveal": "silent" + "reveal": "silent", + "close": true } }, { diff --git a/lib/vscode/.yarnrc b/lib/vscode/.yarnrc deleted file mode 100644 index 1965e671993f..000000000000 --- a/lib/vscode/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "https://electronjs.org/headers" -target "12.0.4" -runtime "electron" diff --git a/lib/vscode/README.md b/lib/vscode/README.md index 0a9a62d9b7f3..ac7f2194233f 100644 --- a/lib/vscode/README.md +++ b/lib/vscode/README.md @@ -10,7 +10,7 @@ This repository ("`Code - OSS`") is where we (Microsoft) develop the [Visual Stu ## Visual Studio Code

- VS Code in action + VS Code in action

[Visual Studio Code](https://code.visualstudio.com) is a distribution of the `Code - OSS` repository with Microsoft specific customizations released under a traditional [Microsoft product license](https://code.visualstudio.com/License/). @@ -21,11 +21,11 @@ Visual Studio Code is updated monthly with new features and bug fixes. You can d ## Contributing -There are many ways in which you can participate in the project, for example: +There are many ways in which you can participate in this project, for example: * [Submit bugs and feature requests](https://github.com/microsoft/vscode/issues), and help us verify as they are checked in * Review [source code changes](https://github.com/microsoft/vscode/pulls) -* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to new content +* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to additional and new content If you are interested in fixing issues and contributing directly to the code base, please see the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute), which covers the following: @@ -57,10 +57,10 @@ VS Code includes a set of built-in extensions located in the [extensions](extens ## Development Container -This repository includes a Visual Studio Code Remote - Containers / Codespaces development container. +This repository includes a Visual Studio Code Remote - Containers / GitHub Codespaces development container. -- For [Remote - Containers](https://aka.ms/vscode-remote/download/containers), use the **Remote-Containers: Open Repository in Container...** command which creates a Docker volume for better disk I/O on macOS and Windows. -- For Codespaces, install the [Visual Studio Codespaces](https://aka.ms/vscs-ext-vscode) extension in VS Code, and use the **Codespaces: Create New Codespace** command. +- For [Remote - Containers](https://aka.ms/vscode-remote/download/containers), use the **Remote-Containers: Clone Repository in Container Volume...** command which creates a Docker volume for better disk I/O on macOS and Windows. +- For Codespaces, install the [Github Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespacese) extension in VS Code, and use the **Codespaces: Create New Codespace** command. Docker / the Codespace should have at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run full build. See the [development container README](.devcontainer/README.md) for more information. diff --git a/lib/vscode/ThirdPartyNotices.txt b/lib/vscode/ThirdPartyNotices.txt index e30e71ee4a21..d929c6b59e78 100644 --- a/lib/vscode/ThirdPartyNotices.txt +++ b/lib/vscode/ThirdPartyNotices.txt @@ -5,17 +5,17 @@ Do Not Translate or Localize This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. -1. JuliaEditorSupport/atom-language-julia version 0.21.0 (https://github.com/JuliaEditorSupport/atom-language-julia) -2. atom/language-clojure version 0.22.7 (https://github.com/atom/language-clojure) -3. atom/language-coffee-script version 0.49.3 (https://github.com/atom/language-coffee-script) -4. atom/language-css version 0.44.4 (https://github.com/atom/language-css) -5. atom/language-java version 0.32.1 (https://github.com/atom/language-java) -6. atom/language-sass version 0.61.4 (https://github.com/atom/language-sass) -7. atom/language-shellscript version 0.26.0 (https://github.com/atom/language-shellscript) -8. atom/language-xml version 0.35.2 (https://github.com/atom/language-xml) -9. better-go-syntax version 1.0.0 (https://github.com/jeff-hykin/better-go-syntax/ ) -10. Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) -11. daaain/Handlebars version 1.8.0 (https://github.com/daaain/Handlebars) +1. atom/language-clojure version 0.22.7 (https://github.com/atom/language-clojure) +2. atom/language-coffee-script version 0.49.3 (https://github.com/atom/language-coffee-script) +3. atom/language-css version 0.44.4 (https://github.com/atom/language-css) +4. atom/language-java version 0.32.1 (https://github.com/atom/language-java) +5. atom/language-sass version 0.62.1 (https://github.com/atom/language-sass) +6. atom/language-shellscript version 0.26.0 (https://github.com/atom/language-shellscript) +7. atom/language-xml version 0.35.2 (https://github.com/atom/language-xml) +8. better-go-syntax version 1.0.0 (https://github.com/jeff-hykin/better-go-syntax/ ) +9. Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) +10. daaain/Handlebars version 1.8.0 (https://github.com/daaain/Handlebars) +11. dart-lang/dart-syntax-highlight (https://github.com/dart-lang/dart-syntax-highlight) 12. davidrios/pug-tmbundle (https://github.com/davidrios/pug-tmbundle) 13. definitelytyped (https://github.com/DefinitelyTyped/DefinitelyTyped) 14. demyte/language-cshtml version 0.3.0 (https://github.com/demyte/language-cshtml) @@ -32,71 +32,45 @@ This project incorporates components from the projects listed below. The origina 25. jeff-hykin/cpp-textmate-grammar version 1.12.11 (https://github.com/jeff-hykin/cpp-textmate-grammar) 26. jeff-hykin/cpp-textmate-grammar version 1.15.5 (https://github.com/jeff-hykin/cpp-textmate-grammar) 27. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) -28. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) -29. language-docker (https://github.com/moby/moby) -30. language-less version 0.34.2 (https://github.com/atom/language-less) -31. language-php version 0.46.0 (https://github.com/atom/language-php) -32. MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) -33. marked version 1.1.0 (https://github.com/markedjs/marked) -34. mdn-data version 1.1.12 (https://github.com/mdn/data) -35. microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/microsoft/TypeScript-TmLanguage) -36. microsoft/vscode-JSON.tmLanguage (https://github.com/microsoft/vscode-JSON.tmLanguage) -37. microsoft/vscode-markdown-tm-grammar version 1.0.0 (https://github.com/microsoft/vscode-markdown-tm-grammar) -38. microsoft/vscode-mssql version 1.9.0 (https://github.com/microsoft/vscode-mssql) -39. mmims/language-batchfile version 0.7.5 (https://github.com/mmims/language-batchfile) -40. NVIDIA/cuda-cpp-grammar (https://github.com/NVIDIA/cuda-cpp-grammar) -41. PowerShell/EditorSyntax version 1.0.0 (https://github.com/PowerShell/EditorSyntax) -42. rust-syntax version 0.4.3 (https://github.com/dustypomerleau/rust-syntax) -43. seti-ui version 0.1.0 (https://github.com/jesseweed/seti-ui) -44. shaders-tmLanguage version 0.1.0 (https://github.com/tgjones/shaders-tmLanguage) -45. textmate/asp.vb.net.tmbundle (https://github.com/textmate/asp.vb.net.tmbundle) -46. textmate/c.tmbundle (https://github.com/textmate/c.tmbundle) -47. textmate/diff.tmbundle (https://github.com/textmate/diff.tmbundle) -48. textmate/git.tmbundle (https://github.com/textmate/git.tmbundle) -49. textmate/groovy.tmbundle (https://github.com/textmate/groovy.tmbundle) -50. textmate/html.tmbundle (https://github.com/textmate/html.tmbundle) -51. textmate/ini.tmbundle (https://github.com/textmate/ini.tmbundle) -52. textmate/javascript.tmbundle (https://github.com/textmate/javascript.tmbundle) -53. textmate/lua.tmbundle (https://github.com/textmate/lua.tmbundle) -54. textmate/markdown.tmbundle (https://github.com/textmate/markdown.tmbundle) -55. textmate/perl.tmbundle (https://github.com/textmate/perl.tmbundle) -56. textmate/ruby.tmbundle (https://github.com/textmate/ruby.tmbundle) -57. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) -58. TypeScript-TmLanguage version 0.1.8 (https://github.com/microsoft/TypeScript-TmLanguage) -59. TypeScript-TmLanguage version 1.0.0 (https://github.com/microsoft/TypeScript-TmLanguage) -60. Unicode version 12.0.0 (https://home.unicode.org/) -61. vscode-codicons version 0.0.14 (https://github.com/microsoft/vscode-codicons) -62. vscode-logfile-highlighter version 2.11.0 (https://github.com/emilast/vscode-logfile-highlighter) -63. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) -64. Web Background Synchronization (https://github.com/WICG/background-sync) - - -%% JuliaEditorSupport/atom-language-julia NOTICES AND INFORMATION BEGIN HERE -========================================= -The atom-language-julia package is licensed under the MIT "Expat" License: +28. JuliaEditorSupport/atom-language-julia version 0.21.0 (https://github.com/JuliaEditorSupport/atom-language-julia) +29. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) +30. language-docker (https://github.com/moby/moby) +31. language-less version 0.34.2 (https://github.com/atom/language-less) +32. language-php version 0.46.2 (https://github.com/atom/language-php) +33. MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) +34. marked version 1.1.0 (https://github.com/markedjs/marked) +35. mdn-data version 1.1.12 (https://github.com/mdn/data) +36. microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/microsoft/TypeScript-TmLanguage) +37. microsoft/vscode-JSON.tmLanguage (https://github.com/microsoft/vscode-JSON.tmLanguage) +38. microsoft/vscode-markdown-tm-grammar version 1.0.0 (https://github.com/microsoft/vscode-markdown-tm-grammar) +39. microsoft/vscode-mssql version 1.9.0 (https://github.com/microsoft/vscode-mssql) +40. mmims/language-batchfile version 0.7.6 (https://github.com/mmims/language-batchfile) +41. NVIDIA/cuda-cpp-grammar (https://github.com/NVIDIA/cuda-cpp-grammar) +42. PowerShell/EditorSyntax version 1.0.0 (https://github.com/PowerShell/EditorSyntax) +43. rust-syntax version 0.4.3 (https://github.com/dustypomerleau/rust-syntax) +44. seti-ui version 0.1.0 (https://github.com/jesseweed/seti-ui) +45. shaders-tmLanguage version 0.1.0 (https://github.com/tgjones/shaders-tmLanguage) +46. textmate/asp.vb.net.tmbundle (https://github.com/textmate/asp.vb.net.tmbundle) +47. textmate/c.tmbundle (https://github.com/textmate/c.tmbundle) +48. textmate/diff.tmbundle (https://github.com/textmate/diff.tmbundle) +49. textmate/git.tmbundle (https://github.com/textmate/git.tmbundle) +50. textmate/groovy.tmbundle (https://github.com/textmate/groovy.tmbundle) +51. textmate/html.tmbundle (https://github.com/textmate/html.tmbundle) +52. textmate/ini.tmbundle (https://github.com/textmate/ini.tmbundle) +53. textmate/javascript.tmbundle (https://github.com/textmate/javascript.tmbundle) +54. textmate/lua.tmbundle (https://github.com/textmate/lua.tmbundle) +55. textmate/markdown.tmbundle (https://github.com/textmate/markdown.tmbundle) +56. textmate/perl.tmbundle (https://github.com/textmate/perl.tmbundle) +57. textmate/ruby.tmbundle (https://github.com/textmate/ruby.tmbundle) +58. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) +59. TypeScript-TmLanguage version 0.1.8 (https://github.com/microsoft/TypeScript-TmLanguage) +60. TypeScript-TmLanguage version 1.0.0 (https://github.com/microsoft/TypeScript-TmLanguage) +61. Unicode version 12.0.0 (https://home.unicode.org/) +62. vscode-codicons version 0.0.14 (https://github.com/microsoft/vscode-codicons) +63. vscode-logfile-highlighter version 2.11.0 (https://github.com/emilast/vscode-logfile-highlighter) +64. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) +65. Web Background Synchronization (https://github.com/WICG/background-sync) -> Copyright (c) 2015 -> -> Permission is hereby granted, free of charge, to any person obtaining -> a copy of this software and associated documentation files (the -> "Software"), to deal in the Software without restriction, including -> without limitation the rights to use, copy, modify, merge, publish, -> distribute, sublicense, and/or sell copies of the Software, and to -> permit persons to whom the Software is furnished to do so, subject to -> the following conditions: -> -> The above copyright notice and this permission notice shall be -> included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -========================================= -END OF JuliaEditorSupport/atom-language-julia NOTICES AND INFORMATION %% atom/language-clojure NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -477,6 +451,38 @@ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF daaain/Handlebars NOTICES AND INFORMATION +%% dart-lang/dart-syntax-highlight NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright 2020, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF dart-lang/dart-syntax-highlight NOTICES AND INFORMATION + %% davidrios/pug-tmbundle NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -855,6 +861,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF js-beautify NOTICES AND INFORMATION +%% JuliaEditorSupport/atom-language-julia NOTICES AND INFORMATION BEGIN HERE +========================================= +The atom-language-julia package is licensed under the MIT "Expat" License: + +> Copyright (c) 2015 +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF JuliaEditorSupport/atom-language-julia NOTICES AND INFORMATION + %% Jxck/assert NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) diff --git a/lib/vscode/build/azure-pipelines/common/createAsset.js b/lib/vscode/build/azure-pipelines/common/createAsset.js index 3038ff62b825..340c6fd7e5d9 100644 --- a/lib/vscode/build/azure-pipelines/common/createAsset.js +++ b/lib/vscode/build/azure-pipelines/common/createAsset.js @@ -5,15 +5,101 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const fs = require("fs"); +const url = require("url"); const crypto = require("crypto"); const azure = require("azure-storage"); const mime = require("mime"); const cosmos_1 = require("@azure/cosmos"); const retry_1 = require("./retry"); -if (process.argv.length !== 6) { - console.error('Usage: node createAsset.js PLATFORM TYPE NAME FILE'); +if (process.argv.length !== 8) { + console.error('Usage: node createAsset.js PRODUCT OS ARCH TYPE NAME FILE'); process.exit(-1); } +// Contains all of the logic for mapping details to our actual product names in CosmosDB +function getPlatform(product, os, arch, type) { + switch (os) { + case 'win32': + switch (product) { + case 'client': + const asset = arch === 'ia32' ? 'win32' : `win32-${arch}`; + switch (type) { + case 'archive': + return `${asset}-archive`; + case 'setup': + return asset; + case 'user-setup': + return `${asset}-user`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'server': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32' : `server-win32-${arch}`; + case 'web': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32-web' : `server-win32-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'linux': + switch (type) { + case 'snap': + return `linux-snap-${arch}`; + case 'archive-unsigned': + switch (product) { + case 'client': + return `linux-${arch}`; + case 'server': + return `server-linux-${arch}`; + case 'web': + return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'deb-package': + return `linux-deb-${arch}`; + case 'rpm-package': + return `linux-rpm-${arch}`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'darwin': + switch (product) { + case 'client': + if (arch === 'x64') { + return 'darwin'; + } + return `darwin-${arch}`; + case 'server': + return 'server-darwin'; + case 'web': + if (arch !== 'x64') { + throw `What should the platform be?: ${product} ${os} ${arch} ${type}`; + } + return 'server-darwin-web'; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } +} +// Contains all of the logic for mapping types to our actual types in CosmosDB +function getRealType(type) { + switch (type) { + case 'user-setup': + return 'setup'; + case 'deb-package': + case 'rpm-package': + return 'package'; + default: + return type; + } +} function hashStream(hashName, stream) { return new Promise((c, e) => { const shasum = crypto.createHash(hashName); @@ -45,7 +131,10 @@ function getEnv(name) { return result; } async function main() { - const [, , platform, type, fileName, filePath] = process.argv; + const [, , product, os, arch, unprocessedType, fileName, filePath] = process.argv; + // getPlatform needs the unprocessedType + const platform = getPlatform(product, os, arch, unprocessedType); + const type = getRealType(unprocessedType); const quality = getEnv('VSCODE_QUALITY'); const commit = getEnv('BUILD_SOURCEVERSION'); console.log('Creating asset...'); @@ -65,14 +154,27 @@ async function main() { console.log(`Blob ${quality}, ${blobName} already exists, not publishing again.`); return; } - console.log('Uploading blobs to Azure storage...'); - await uploadBlob(blobService, quality, blobName, filePath, fileName); + const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY'], `${storageAccount}.blob.core.chinacloudapi.cn`) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + // mooncake is fussy and far away, this is needed! + blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + console.log('Uploading blobs to Azure storage and Mooncake Azure storage...'); + await retry_1.retry(() => Promise.all([ + uploadBlob(blobService, quality, blobName, filePath, fileName), + uploadBlob(mooncakeBlobService, quality, blobName, filePath, fileName) + ])); console.log('Blobs successfully uploaded.'); + // TODO: Understand if blobName and blobPath are the same and replace blobPath with blobName if so. + const assetUrl = `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`; + const blobPath = url.parse(assetUrl).path; + const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; const asset = { platform, type, - url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, + url: assetUrl, hash: sha1hash, + mooncakeUrl, sha256hash, size }; @@ -84,6 +186,7 @@ async function main() { const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); const scripts = client.database('builds').container(quality).scripts; await retry_1.retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); + console.log(` Done โœ”๏ธ`); } main().then(() => { console.log('Asset successfully created'); diff --git a/lib/vscode/build/azure-pipelines/common/createAsset.ts b/lib/vscode/build/azure-pipelines/common/createAsset.ts index daf60d710eec..5fe93c566bb1 100644 --- a/lib/vscode/build/azure-pipelines/common/createAsset.ts +++ b/lib/vscode/build/azure-pipelines/common/createAsset.ts @@ -6,6 +6,7 @@ 'use strict'; import * as fs from 'fs'; +import * as url from 'url'; import { Readable } from 'stream'; import * as crypto from 'crypto'; import * as azure from 'azure-storage'; @@ -24,11 +25,98 @@ interface Asset { supportsFastUpdate?: boolean; } -if (process.argv.length !== 6) { - console.error('Usage: node createAsset.js PLATFORM TYPE NAME FILE'); +if (process.argv.length !== 8) { + console.error('Usage: node createAsset.js PRODUCT OS ARCH TYPE NAME FILE'); process.exit(-1); } +// Contains all of the logic for mapping details to our actual product names in CosmosDB +function getPlatform(product: string, os: string, arch: string, type: string): string { + switch (os) { + case 'win32': + switch (product) { + case 'client': + const asset = arch === 'ia32' ? 'win32' : `win32-${arch}`; + switch (type) { + case 'archive': + return `${asset}-archive`; + case 'setup': + return asset; + case 'user-setup': + return `${asset}-user`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'server': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32' : `server-win32-${arch}`; + case 'web': + if (arch === 'arm64') { + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + return arch === 'ia32' ? 'server-win32-web' : `server-win32-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'linux': + switch (type) { + case 'snap': + return `linux-snap-${arch}`; + case 'archive-unsigned': + switch (product) { + case 'client': + return `linux-${arch}`; + case 'server': + return `server-linux-${arch}`; + case 'web': + return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'deb-package': + return `linux-deb-${arch}`; + case 'rpm-package': + return `linux-rpm-${arch}`; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + case 'darwin': + switch (product) { + case 'client': + if (arch === 'x64') { + return 'darwin'; + } + return `darwin-${arch}`; + case 'server': + return 'server-darwin'; + case 'web': + if (arch !== 'x64') { + throw `What should the platform be?: ${product} ${os} ${arch} ${type}`; + } + return 'server-darwin-web'; + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } + default: + throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + } +} + +// Contains all of the logic for mapping types to our actual types in CosmosDB +function getRealType(type: string) { + switch (type) { + case 'user-setup': + return 'setup'; + case 'deb-package': + case 'rpm-package': + return 'package'; + default: + return type; + } +} + function hashStream(hashName: string, stream: Readable): Promise { return new Promise((c, e) => { const shasum = crypto.createHash(hashName); @@ -68,7 +156,10 @@ function getEnv(name: string): string { } async function main(): Promise { - const [, , platform, type, fileName, filePath] = process.argv; + const [, , product, os, arch, unprocessedType, fileName, filePath] = process.argv; + // getPlatform needs the unprocessedType + const platform = getPlatform(product, os, arch, unprocessedType); + const type = getRealType(unprocessedType); const quality = getEnv('VSCODE_QUALITY'); const commit = getEnv('BUILD_SOURCEVERSION'); @@ -98,17 +189,33 @@ async function main(): Promise { return; } - console.log('Uploading blobs to Azure storage...'); + const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY']!, `${storageAccount}.blob.core.chinacloudapi.cn`) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + + // mooncake is fussy and far away, this is needed! + blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + + console.log('Uploading blobs to Azure storage and Mooncake Azure storage...'); - await uploadBlob(blobService, quality, blobName, filePath, fileName); + await retry(() => Promise.all([ + uploadBlob(blobService, quality, blobName, filePath, fileName), + uploadBlob(mooncakeBlobService, quality, blobName, filePath, fileName) + ])); console.log('Blobs successfully uploaded.'); + // TODO: Understand if blobName and blobPath are the same and replace blobPath with blobName if so. + const assetUrl = `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`; + const blobPath = url.parse(assetUrl).path; + const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; + const asset: Asset = { platform, type, - url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, + url: assetUrl, hash: sha1hash, + mooncakeUrl, sha256hash, size }; @@ -123,6 +230,8 @@ async function main(): Promise { const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); const scripts = client.database('builds').container(quality).scripts; await retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); + + console.log(` Done โœ”๏ธ`); } main().then(() => { diff --git a/lib/vscode/build/azure-pipelines/common/sync-mooncake.js b/lib/vscode/build/azure-pipelines/common/sync-mooncake.js deleted file mode 100644 index 1f3354226519..000000000000 --- a/lib/vscode/build/azure-pipelines/common/sync-mooncake.js +++ /dev/null @@ -1,87 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const url = require("url"); -const azure = require("azure-storage"); -const mime = require("mime"); -const cosmos_1 = require("@azure/cosmos"); -const retry_1 = require("./retry"); -function log(...args) { - console.log(...[`[${new Date().toISOString()}]`, ...args]); -} -function error(...args) { - console.error(...[`[${new Date().toISOString()}]`, ...args]); -} -if (process.argv.length < 3) { - error('Usage: node sync-mooncake.js '); - process.exit(-1); -} -async function sync(commit, quality) { - log(`Synchronizing Mooncake assets for ${quality}, ${commit}...`); - const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); - const container = client.database('builds').container(quality); - const query = `SELECT TOP 1 * FROM c WHERE c.id = "${commit}"`; - const res = await container.items.query(query, {}).fetchAll(); - if (res.resources.length !== 1) { - throw new Error(`No builds found for ${commit}`); - } - const build = res.resources[0]; - log(`Found build for ${commit}, with ${build.assets.length} assets`); - const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']; - const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']) - .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY'], `${storageAccount}.blob.core.chinacloudapi.cn`) - .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - // mooncake is fussy and far away, this is needed! - blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; - mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; - for (const asset of build.assets) { - try { - const blobPath = url.parse(asset.url).path; - if (!blobPath) { - throw new Error(`Failed to parse URL: ${asset.url}`); - } - const blobName = blobPath.replace(/^\/\w+\//, ''); - log(`Found ${blobName}`); - if (asset.mooncakeUrl) { - log(` Already in Mooncake โœ”๏ธ`); - continue; - } - const readStream = blobService.createReadStream(quality, blobName, undefined); - const blobOptions = { - contentSettings: { - contentType: mime.lookup(blobPath), - cacheControl: 'max-age=31536000, public' - } - }; - const writeStream = mooncakeBlobService.createWriteStreamToBlockBlob(quality, blobName, blobOptions, undefined); - log(` Uploading to Mooncake...`); - await new Promise((c, e) => readStream.pipe(writeStream).on('finish', c).on('error', e)); - log(` Updating build in DB...`); - const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; - await retry_1.retry(() => container.scripts.storedProcedure('setAssetMooncakeUrl') - .execute('', [commit, asset.platform, asset.type, mooncakeUrl])); - log(` Done โœ”๏ธ`); - } - catch (err) { - error(err); - } - } - log(`All done โœ”๏ธ`); -} -function main() { - const commit = process.env['BUILD_SOURCEVERSION']; - if (!commit) { - error('Skipping publish due to missing BUILD_SOURCEVERSION'); - return; - } - const quality = process.argv[2]; - sync(commit, quality).catch(err => { - error(err); - process.exit(1); - }); -} -main(); diff --git a/lib/vscode/build/azure-pipelines/common/sync-mooncake.ts b/lib/vscode/build/azure-pipelines/common/sync-mooncake.ts deleted file mode 100644 index 4ffe7a8f15bb..000000000000 --- a/lib/vscode/build/azure-pipelines/common/sync-mooncake.ts +++ /dev/null @@ -1,131 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as url from 'url'; -import * as azure from 'azure-storage'; -import * as mime from 'mime'; -import { CosmosClient } from '@azure/cosmos'; -import { retry } from './retry'; - -function log(...args: any[]) { - console.log(...[`[${new Date().toISOString()}]`, ...args]); -} - -function error(...args: any[]) { - console.error(...[`[${new Date().toISOString()}]`, ...args]); -} - -if (process.argv.length < 3) { - error('Usage: node sync-mooncake.js '); - process.exit(-1); -} - -interface Build { - assets: Asset[]; -} - -interface Asset { - platform: string; - type: string; - url: string; - mooncakeUrl: string; - hash: string; - sha256hash: string; - size: number; - supportsFastUpdate?: boolean; -} - -async function sync(commit: string, quality: string): Promise { - log(`Synchronizing Mooncake assets for ${quality}, ${commit}...`); - - const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); - const container = client.database('builds').container(quality); - - const query = `SELECT TOP 1 * FROM c WHERE c.id = "${commit}"`; - const res = await container.items.query(query, {}).fetchAll(); - - if (res.resources.length !== 1) { - throw new Error(`No builds found for ${commit}`); - } - - const build = res.resources[0]; - - log(`Found build for ${commit}, with ${build.assets.length} assets`); - - const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']!; - - const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']!) - .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - - const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY']!, `${storageAccount}.blob.core.chinacloudapi.cn`) - .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - - // mooncake is fussy and far away, this is needed! - blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; - mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; - - for (const asset of build.assets) { - try { - const blobPath = url.parse(asset.url).path; - - if (!blobPath) { - throw new Error(`Failed to parse URL: ${asset.url}`); - } - - const blobName = blobPath.replace(/^\/\w+\//, ''); - - log(`Found ${blobName}`); - - if (asset.mooncakeUrl) { - log(` Already in Mooncake โœ”๏ธ`); - continue; - } - - const readStream = blobService.createReadStream(quality, blobName, undefined!); - const blobOptions: azure.BlobService.CreateBlockBlobRequestOptions = { - contentSettings: { - contentType: mime.lookup(blobPath), - cacheControl: 'max-age=31536000, public' - } - }; - - const writeStream = mooncakeBlobService.createWriteStreamToBlockBlob(quality, blobName, blobOptions, undefined); - - log(` Uploading to Mooncake...`); - await new Promise((c, e) => readStream.pipe(writeStream).on('finish', c).on('error', e)); - - log(` Updating build in DB...`); - const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; - await retry(() => container.scripts.storedProcedure('setAssetMooncakeUrl') - .execute('', [commit, asset.platform, asset.type, mooncakeUrl])); - - log(` Done โœ”๏ธ`); - } catch (err) { - error(err); - } - } - - log(`All done โœ”๏ธ`); -} - -function main(): void { - const commit = process.env['BUILD_SOURCEVERSION']; - - if (!commit) { - error('Skipping publish due to missing BUILD_SOURCEVERSION'); - return; - } - - const quality = process.argv[2]; - - sync(commit, quality).catch(err => { - error(err); - process.exit(1); - }); -} - -main(); diff --git a/lib/vscode/build/azure-pipelines/darwin/product-build-darwin-sign.yml b/lib/vscode/build/azure-pipelines/darwin/product-build-darwin-sign.yml index 4ad8349c51a8..49f74b55c933 100644 --- a/lib/vscode/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ b/lib/vscode/build/azure-pipelines/darwin/product-build-darwin-sign.yml @@ -35,13 +35,13 @@ steps: displayName: Restore modules for just build folder and compile it - download: current - artifact: vscode-darwin-$(VSCODE_ARCH) + artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive displayName: Download $(VSCODE_ARCH) artifact - script: | set -e - unzip $(Pipeline.Workspace)/vscode-darwin-$(VSCODE_ARCH)/VSCode-darwin-$(VSCODE_ARCH).zip -d $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - mv $(Pipeline.Workspace)/vscode-darwin-$(VSCODE_ARCH)/VSCode-darwin-$(VSCODE_ARCH).zip $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip + unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + mv $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip displayName: Unzip & move - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 @@ -108,22 +108,18 @@ steps: condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - script: | - set -e - # For legacy purposes, arch for x64 is just 'darwin' case $VSCODE_ARCH in x64) ASSET_ID="darwin" ;; arm64) ASSET_ID="darwin-arm64" ;; universal) ASSET_ID="darwin-universal" ;; esac + echo "##vso[task.setvariable variable=ASSET_ID]$ASSET_ID" + displayName: Set asset id variable + + - script: mv $(agent.builddirectory)/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin.zip + displayName: Rename x64 build to it's legacy name + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - node build/azure-pipelines/common/createAsset.js \ - "$ASSET_ID" \ - archive \ - "VSCode-$ASSET_ID.zip" \ - ../VSCode-darwin-$(VSCODE_ARCH).zip - displayName: Publish Clients + - publish: $(Agent.BuildDirectory)/VSCode-$(ASSET_ID).zip + artifact: vscode_client_darwin_$(VSCODE_ARCH)_archive diff --git a/lib/vscode/build/azure-pipelines/darwin/product-build-darwin.yml b/lib/vscode/build/azure-pipelines/darwin/product-build-darwin.yml index 186920fe96d6..566eeb805229 100644 --- a/lib/vscode/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/lib/vscode/build/azure-pipelines/darwin/product-build-darwin.yml @@ -138,19 +138,19 @@ steps: condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - download: current - artifact: vscode-darwin-x64 + artifact: unsigned_vscode_client_darwin_x64_archive displayName: Download x64 artifact condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - download: current - artifact: vscode-darwin-arm64 + artifact: unsigned_vscode_client_darwin_arm64_archive displayName: Download arm64 artifact condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - script: | set -e - cp $(Pipeline.Workspace)/vscode-darwin-x64/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin-x64.zip - cp $(Pipeline.Workspace)/vscode-darwin-arm64/VSCode-darwin-arm64.zip $(agent.builddirectory)/VSCode-darwin-arm64.zip + cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin-x64.zip + cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_arm64_archive/VSCode-darwin-arm64.zip $(agent.builddirectory)/VSCode-darwin-arm64.zip unzip $(agent.builddirectory)/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64 unzip $(agent.builddirectory)/VSCode-darwin-arm64.zip -d $(agent.builddirectory)/VSCode-darwin-arm64 DEBUG=* node build/darwin/create-universal-app.js @@ -280,26 +280,27 @@ steps: - script: | set -e - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_ARCH="$(VSCODE_ARCH)" ./build/azure-pipelines/darwin/publish-server.sh - displayName: Publish Servers + + # package Remote Extension Host + pushd .. && mv vscode-reh-darwin vscode-server-darwin && zip -Xry vscode-server-darwin.zip vscode-server-darwin && popd + + # package Remote Extension Host (Web) + pushd .. && mv vscode-reh-web-darwin vscode-server-darwin-web && zip -Xry vscode-server-darwin-web.zip vscode-server-darwin-web && popd + displayName: Prepare to publish servers condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip - artifact: vscode-darwin-$(VSCODE_ARCH) + artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive displayName: Publish client archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-darwin.zip - artifact: vscode-server-darwin-$(VSCODE_ARCH) + artifact: vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish server archive condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-darwin-web.zip - artifact: vscode-server-darwin-$(VSCODE_ARCH)-web + artifact: vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish web server archive condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) @@ -308,5 +309,5 @@ steps: VSCODE_ARCH="$(VSCODE_ARCH)" \ yarn gulp upload-vscode-configuration displayName: Upload configuration (for Bing settings search) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) continueOnError: true diff --git a/lib/vscode/build/azure-pipelines/darwin/publish-server.sh b/lib/vscode/build/azure-pipelines/darwin/publish-server.sh deleted file mode 100755 index 72a85942d5a5..000000000000 --- a/lib/vscode/build/azure-pipelines/darwin/publish-server.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -if [ "$VSCODE_ARCH" == "x64" ]; then - # package Remote Extension Host - pushd .. && mv vscode-reh-darwin vscode-server-darwin && zip -Xry vscode-server-darwin.zip vscode-server-darwin && popd - - # publish Remote Extension Host - node build/azure-pipelines/common/createAsset.js \ - server-darwin \ - archive-unsigned \ - "vscode-server-darwin.zip" \ - ../vscode-server-darwin.zip -fi diff --git a/lib/vscode/build/azure-pipelines/linux/alpine/publish.sh b/lib/vscode/build/azure-pipelines/linux/alpine/publish.sh deleted file mode 100755 index 2f5647d1ea36..000000000000 --- a/lib/vscode/build/azure-pipelines/linux/alpine/publish.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -e -REPO="$(pwd)" -ROOT="$REPO/.." - -PLATFORM_LINUX="linux-alpine" - -# Publish Remote Extension Host -LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" -SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX" -SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX.tar.gz" -SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" - -rm -rf $ROOT/vscode-server-*.tar.* -(cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) - -node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" - -# Publish Remote Extension Host (Web) -LEGACY_SERVER_BUILD_NAME="vscode-reh-web-$PLATFORM_LINUX" -SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX-web" -SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX-web.tar.gz" -SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" - -rm -rf $ROOT/vscode-server-*-web.tar.* -(cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) - -node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX-web" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" diff --git a/lib/vscode/build/azure-pipelines/linux/publish.sh b/lib/vscode/build/azure-pipelines/linux/prepare-publish.sh similarity index 79% rename from lib/vscode/build/azure-pipelines/linux/publish.sh rename to lib/vscode/build/azure-pipelines/linux/prepare-publish.sh index 6d748c6e340d..891fa8024ef5 100755 --- a/lib/vscode/build/azure-pipelines/linux/publish.sh +++ b/lib/vscode/build/azure-pipelines/linux/prepare-publish.sh @@ -13,8 +13,6 @@ TARBALL_PATH="$ROOT/$TARBALL_FILENAME" rm -rf $ROOT/code-*.tar.* (cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) -node build/azure-pipelines/common/createAsset.js "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$TARBALL_PATH" - # Publish Remote Extension Host LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX" @@ -24,8 +22,6 @@ SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" rm -rf $ROOT/vscode-server-*.tar.* (cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) -node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" - # Publish Remote Extension Host (Web) LEGACY_SERVER_BUILD_NAME="vscode-reh-web-$PLATFORM_LINUX" SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX-web" @@ -35,8 +31,6 @@ SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" rm -rf $ROOT/vscode-server-*-web.tar.* (cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) -node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX-web" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" - # Publish DEB case $VSCODE_ARCH in x64) DEB_ARCH="amd64" ;; @@ -47,8 +41,6 @@ PLATFORM_DEB="linux-deb-$VSCODE_ARCH" DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" -node build/azure-pipelines/common/createAsset.js "$PLATFORM_DEB" package "$DEB_FILENAME" "$DEB_PATH" - # Publish RPM case $VSCODE_ARCH in x64) RPM_ARCH="x86_64" ;; @@ -61,8 +53,6 @@ PLATFORM_RPM="linux-rpm-$VSCODE_ARCH" RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" -node build/azure-pipelines/common/createAsset.js "$PLATFORM_RPM" package "$RPM_FILENAME" "$RPM_PATH" - # Publish Snap # Pack snap tarball artifact, in order to preserve file perms mkdir -p $REPO/.build/linux/snap-tarball @@ -73,3 +63,4 @@ rm -rf $SNAP_TARBALL_PATH # Export DEB_PATH, RPM_PATH echo "##vso[task.setvariable variable=DEB_PATH]$DEB_PATH" echo "##vso[task.setvariable variable=RPM_PATH]$RPM_PATH" +echo "##vso[task.setvariable variable=TARBALL_PATH]$TARBALL_PATH" diff --git a/lib/vscode/build/azure-pipelines/linux/product-build-alpine.yml b/lib/vscode/build/azure-pipelines/linux/product-build-alpine.yml index 8376c079ce88..ed0c35346c70 100644 --- a/lib/vscode/build/azure-pipelines/linux/product-build-alpine.yml +++ b/lib/vscode/build/azure-pipelines/linux/product-build-alpine.yml @@ -117,19 +117,37 @@ steps: - script: | set -e - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - ./build/azure-pipelines/linux/alpine/publish.sh - displayName: Publish + REPO="$(pwd)" + ROOT="$REPO/.." + + PLATFORM_LINUX="linux-alpine" + + # Publish Remote Extension Host + LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" + SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX" + SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX.tar.gz" + SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" + + rm -rf $ROOT/vscode-server-*.tar.* + (cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) + + # Publish Remote Extension Host (Web) + LEGACY_SERVER_BUILD_NAME="vscode-reh-web-$PLATFORM_LINUX" + SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX-web" + SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX-web.tar.gz" + SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" + + rm -rf $ROOT/vscode-server-*-web.tar.* + (cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) + displayName: Prepare for publish condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine.tar.gz - artifact: vscode-server-linux-alpine + artifact: vscode_server_linux_alpine_archive-unsigned displayName: Publish server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine-web.tar.gz - artifact: vscode-server-linux-alpine-web + artifact: vscode_web_linux_alpine_archive-unsigned displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/lib/vscode/build/azure-pipelines/linux/product-build-linux.yml b/lib/vscode/build/azure-pipelines/linux/product-build-linux.yml index cb06bf6a7249..8181083d1f25 100644 --- a/lib/vscode/build/azure-pipelines/linux/product-build-linux.yml +++ b/lib/vscode/build/azure-pipelines/linux/product-build-linux.yml @@ -245,27 +245,32 @@ steps: AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ VSCODE_ARCH="$(VSCODE_ARCH)" \ - ./build/azure-pipelines/linux/publish.sh - displayName: Publish + ./build/azure-pipelines/linux/prepare-publish.sh + displayName: Prepare for Publish condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(DEB_PATH) - artifact: vscode-linux-deb-$(VSCODE_ARCH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_deb-package displayName: Publish deb package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(RPM_PATH) - artifact: vscode-linux-rpm-$(VSCODE_ARCH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_rpm-package displayName: Publish rpm package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - publish: $(TARBALL_PATH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_archive-unsigned + displayName: Publish client archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - publish: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH).tar.gz - artifact: vscode-server-linux-$(VSCODE_ARCH) + artifact: vscode_server_linux_$(VSCODE_ARCH)_archive-unsigned displayName: Publish server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web.tar.gz - artifact: vscode-server-linux-$(VSCODE_ARCH)-web + artifact: vscode_web_linux_$(VSCODE_ARCH)_archive-unsigned displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/lib/vscode/build/azure-pipelines/linux/snap-build-linux.yml b/lib/vscode/build/azure-pipelines/linux/snap-build-linux.yml index f5e0288f0b92..f7af900e1d0d 100644 --- a/lib/vscode/build/azure-pipelines/linux/snap-build-linux.yml +++ b/lib/vscode/build/azure-pipelines/linux/snap-build-linux.yml @@ -50,15 +50,11 @@ steps: esac (cd $SNAP_ROOT/code-* && sudo --preserve-env snapcraft prime $SNAPCRAFT_TARGET_ARGS && snap pack prime --compression=lzo --filename="$SNAP_PATH") - # Publish snap package - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - node build/azure-pipelines/common/createAsset.js "linux-snap-$(VSCODE_ARCH)" package "$SNAP_FILENAME" "$SNAP_PATH" - # Export SNAP_PATH echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH" + displayName: Prepare for publish - publish: $(SNAP_PATH) - artifact: vscode-linux-snap-$(VSCODE_ARCH) + artifact: vscode_client_linux_$(VSCODE_ARCH)_snap displayName: Publish snap package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/lib/vscode/build/azure-pipelines/product-build.yml b/lib/vscode/build/azure-pipelines/product-build.yml index fd698a0e7dfc..2c475b9deddd 100644 --- a/lib/vscode/build/azure-pipelines/product-build.yml +++ b/lib/vscode/build/azure-pipelines/product-build.yml @@ -86,6 +86,8 @@ variables: value: ${{ eq(parameters.ENABLE_TERRAPIN, true) }} - name: VSCODE_QUALITY value: ${{ parameters.VSCODE_QUALITY }} + - name: VSCODE_RELEASE + value: ${{ parameters.VSCODE_RELEASE }} - name: VSCODE_BUILD_STAGE_WINDOWS value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_32BIT, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_LINUX @@ -301,37 +303,30 @@ stages: steps: - template: darwin/product-build-darwin-sign.yml - - ${{ if and(eq(variables['VSCODE_PUBLISH'], true), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: - - stage: Mooncake + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), ne(variables['VSCODE_PUBLISH'], 'false')) }}: + - stage: Publish dependsOn: - - ${{ if eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true) }}: - - Windows - - ${{ if eq(variables['VSCODE_BUILD_STAGE_LINUX'], true) }}: - - Linux - - ${{ if eq(variables['VSCODE_BUILD_STAGE_MACOS'], true) }}: - - macOS - condition: succeededOrFailed() + - Compile pool: vmImage: "Ubuntu-18.04" + variables: + - name: BUILDS_API_URL + value: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ jobs: - - job: SyncMooncake - displayName: Sync Mooncake + - job: PublishBuild + timeoutInMinutes: 180 + displayName: Publish Build steps: - - template: sync-mooncake.yml + - template: product-publish.yml - - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), or(eq(parameters.VSCODE_RELEASE, true), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true)))) }}: - - stage: Release - dependsOn: - - ${{ if eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true) }}: - - Windows - - ${{ if eq(variables['VSCODE_BUILD_STAGE_LINUX'], true) }}: - - Linux - - ${{ if eq(variables['VSCODE_BUILD_STAGE_MACOS'], true) }}: - - macOS - pool: - vmImage: "Ubuntu-18.04" - jobs: - - job: ReleaseBuild - displayName: Release Build - steps: - - template: release.yml + - ${{ if or(eq(parameters.VSCODE_RELEASE, true), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: + - stage: Release + dependsOn: + - Publish + pool: + vmImage: "Ubuntu-18.04" + jobs: + - job: ReleaseBuild + displayName: Release Build + steps: + - template: product-release.yml diff --git a/lib/vscode/build/azure-pipelines/product-compile.yml b/lib/vscode/build/azure-pipelines/product-compile.yml index 52c7758cfdee..18c17639b830 100644 --- a/lib/vscode/build/azure-pipelines/product-compile.yml +++ b/lib/vscode/build/azure-pipelines/product-compile.yml @@ -118,14 +118,6 @@ steps: displayName: Publish Webview condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - script: | - set -e - VERSION=`node -p "require(\"./package.json\").version"` - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - node build/azure-pipelines/common/createBuild.js $VERSION - displayName: Create build - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - # we gotta tarball everything in order to preserve file permissions - script: | set -e diff --git a/lib/vscode/build/azure-pipelines/product-publish.ps1 b/lib/vscode/build/azure-pipelines/product-publish.ps1 new file mode 100644 index 000000000000..339002ab0c17 --- /dev/null +++ b/lib/vscode/build/azure-pipelines/product-publish.ps1 @@ -0,0 +1,114 @@ +. build/azure-pipelines/win32/exec.ps1 +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$ARTIFACT_PROCESSED_WILDCARD_PATH = "$env:PIPELINE_WORKSPACE/artifacts_processed_*/artifacts_processed_*" +$ARTIFACT_PROCESSED_FILE_PATH = "$env:PIPELINE_WORKSPACE/artifacts_processed_$env:SYSTEM_STAGEATTEMPT/artifacts_processed_$env:SYSTEM_STAGEATTEMPT.txt" + +function Get-PipelineArtifact { + param($Name = '*') + try { + $res = Invoke-RestMethod "$($env:BUILDS_API_URL)artifacts?api-version=6.0" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 + + if (!$res) { + return + } + + $res.value | Where-Object { $_.name -Like $Name } + } catch { + Write-Warning $_ + } +} + +# This set will keep track of which artifacts have already been processed +$set = [System.Collections.Generic.HashSet[string]]::new() + +if (Test-Path $ARTIFACT_PROCESSED_WILDCARD_PATH) { + # Grab the latest artifact_processed text file and load all assets already processed from that. + # This means that the latest artifact_processed_*.txt file has all of the contents of the previous ones. + # Note: The kusto-like syntax only works in PS7+ and only in scripts, not at the REPL. + Get-ChildItem $ARTIFACT_PROCESSED_WILDCARD_PATH + | Sort-Object + | Select-Object -Last 1 + | Get-Content + | ForEach-Object { + $set.Add($_) | Out-Null + Write-Host "Already processed artifact: $_" + } +} + +# Create the artifact file that will be used for this run +New-Item -Path $ARTIFACT_PROCESSED_FILE_PATH -Force | Out-Null + +# Determine which stages we need to watch +$stages = @( + if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } + if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } + if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } +) + +do { + Start-Sleep -Seconds 10 + + $artifacts = Get-PipelineArtifact -Name 'vscode_*' + if (!$artifacts) { + continue + } + + $artifacts | ForEach-Object { + $artifactName = $_.name + if($set.Add($artifactName)) { + Write-Host "Processing artifact: '$artifactName. Downloading from: $($_.resource.downloadUrl)" + + try { + Invoke-RestMethod $_.resource.downloadUrl -OutFile "$env:AGENT_TEMPDIRECTORY/$artifactName.zip" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 | Out-Null + + Expand-Archive -Path "$env:AGENT_TEMPDIRECTORY/$artifactName.zip" -DestinationPath $env:AGENT_TEMPDIRECTORY | Out-Null + } catch { + Write-Warning $_ + $set.Remove($artifactName) | Out-Null + continue + } + + $null,$product,$os,$arch,$type = $artifactName -split '_' + $asset = Get-ChildItem -rec "$env:AGENT_TEMPDIRECTORY/$artifactName" + Write-Host "Processing artifact with the following values:" + # turning in into an object just to log nicely + @{ + product = $product + os = $os + arch = $arch + type = $type + asset = $asset.Name + } | Format-Table + + exec { node build/azure-pipelines/common/createAsset.js $product $os $arch $type $asset.Name $asset.FullName } + $artifactName >> $ARTIFACT_PROCESSED_FILE_PATH + } + } + + # Get the timeline and see if it says the other stage completed + try { + $timeline = Invoke-RestMethod "$($env:BUILDS_API_URL)timeline?api-version=6.0" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 + } catch { + Write-Warning $_ + continue + } + + foreach ($stage in $stages) { + $otherStageFinished = $timeline.records | Where-Object { $_.name -eq $stage -and $_.type -eq 'stage' -and $_.state -eq 'completed' } + if (!$otherStageFinished) { + break + } + } + + $artifacts = Get-PipelineArtifact -Name 'vscode_*' + $artifactsStillToProcess = $artifacts.Count -ne $set.Count +} while (!$otherStageFinished -or $artifactsStillToProcess) + +Write-Host "Processed $($set.Count) artifacts." diff --git a/lib/vscode/build/azure-pipelines/product-publish.yml b/lib/vscode/build/azure-pipelines/product-publish.yml new file mode 100644 index 000000000000..de8cb216b8a1 --- /dev/null +++ b/lib/vscode/build/azure-pipelines/product-publish.yml @@ -0,0 +1,89 @@ +steps: + - task: NodeTool@0 + inputs: + versionSpec: "12.x" + + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.x" + + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode + + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + cd build + exec { yarn } + displayName: Install dependencies + + - download: current + patterns: '**/artifacts_processed_*.txt' + displayName: Download all artifacts_processed text files + + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + + if (Test-Path "$(Pipeline.Workspace)/artifacts_processed_*/artifacts_processed_*.txt") { + Write-Host "Artifacts already processed so a build must have already been created." + return + } + + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" + $VERSION = node -p "require('./package.json').version" + Write-Host "Creating build with version: $VERSION" + exec { node build/azure-pipelines/common/createBuild.js $VERSION } + displayName: Create build if it hasn't been created before + + - pwsh: | + $env:VSCODE_MIXIN_PASSWORD = "$(github-distro-mixin-password)" + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" + $env:AZURE_STORAGE_ACCESS_KEY = "$(ticino-storage-key)" + $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(vscode-storage-key)" + $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(vscode-mooncake-storage-key)" + build/azure-pipelines/product-publish.ps1 + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Process artifacts + + - publish: $(Pipeline.Workspace)/artifacts_processed_$(System.StageAttempt)/artifacts_processed_$(System.StageAttempt).txt + artifact: artifacts_processed_$(System.StageAttempt) + displayName: Publish what artifacts were published for this stage attempt + + - pwsh: | + $ErrorActionPreference = 'Stop' + + # Determine which stages we need to watch + $stages = @( + if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } + if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } + if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } + ) + Write-Host "Stages to check: $stages" + + # Get the timeline and see if it says the other stage completed + $timeline = Invoke-RestMethod "$($env:BUILDS_API_URL)timeline?api-version=6.0" -Headers @{ + Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" + } -MaximumRetryCount 5 -RetryIntervalSec 1 + + $failedStages = @() + foreach ($stage in $stages) { + $didStageFail = $timeline.records | Where-Object { + $_.name -eq $stage -and $_.type -eq 'stage' -and $_.result -ne 'succeeded' -and $_.result -ne 'succeededWithIssues' + } + + if($didStageFail) { + $failedStages += $stage + } else { + Write-Host "'$stage' did not fail." + } + } + + if ($failedStages.Length) { + throw "Failed stages: $($failedStages -join ', '). This stage will now fail so that it is easier to retry failed jobs." + } + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Determine if stage should succeed diff --git a/lib/vscode/build/azure-pipelines/release.yml b/lib/vscode/build/azure-pipelines/product-release.yml similarity index 100% rename from lib/vscode/build/azure-pipelines/release.yml rename to lib/vscode/build/azure-pipelines/product-release.yml diff --git a/lib/vscode/build/azure-pipelines/sync-mooncake.yml b/lib/vscode/build/azure-pipelines/sync-mooncake.yml deleted file mode 100644 index 6e379754f2f8..000000000000 --- a/lib/vscode/build/azure-pipelines/sync-mooncake.yml +++ /dev/null @@ -1,24 +0,0 @@ -steps: - - task: NodeTool@0 - inputs: - versionSpec: "14.x" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.x" - - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode - - - script: | - set -e - - (cd build ; yarn) - - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(vscode-mooncake-storage-key)" \ - node build/azure-pipelines/common/sync-mooncake.js "$VSCODE_QUALITY" diff --git a/lib/vscode/build/azure-pipelines/web/product-build-web.yml b/lib/vscode/build/azure-pipelines/web/product-build-web.yml index 772fe1c05abd..45dedea1b4c6 100644 --- a/lib/vscode/build/azure-pipelines/web/product-build-web.yml +++ b/lib/vscode/build/azure-pipelines/web/product-build-web.yml @@ -119,13 +119,19 @@ steps: - script: | set -e - AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - ./build/azure-pipelines/web/publish.sh - displayName: Publish + REPO="$(pwd)" + ROOT="$REPO/.." + + WEB_BUILD_NAME="vscode-web" + WEB_TARBALL_FILENAME="vscode-web.tar.gz" + WEB_TARBALL_PATH="$ROOT/$WEB_TARBALL_FILENAME" + + rm -rf $ROOT/vscode-web.tar.* + + cd $ROOT && tar --owner=0 --group=0 -czf $WEB_TARBALL_PATH $WEB_BUILD_NAME + displayName: Prepare for publish - publish: $(Agent.BuildDirectory)/vscode-web.tar.gz - artifact: vscode-web-standalone + artifact: vscode_web_linux_standalone_archive-unsigned displayName: Publish web archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/lib/vscode/build/azure-pipelines/web/publish.sh b/lib/vscode/build/azure-pipelines/web/publish.sh deleted file mode 100755 index 827edc2661bf..000000000000 --- a/lib/vscode/build/azure-pipelines/web/publish.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -e -REPO="$(pwd)" -ROOT="$REPO/.." - -# Publish Web Client -WEB_BUILD_NAME="vscode-web" -WEB_TARBALL_FILENAME="vscode-web.tar.gz" -WEB_TARBALL_PATH="$ROOT/$WEB_TARBALL_FILENAME" - -rm -rf $ROOT/vscode-web.tar.* - -(cd $ROOT && tar --owner=0 --group=0 -czf $WEB_TARBALL_PATH $WEB_BUILD_NAME) - -node build/azure-pipelines/common/createAsset.js web-standalone archive-unsigned "$WEB_TARBALL_FILENAME" "$WEB_TARBALL_PATH" diff --git a/lib/vscode/build/azure-pipelines/win32/publish.ps1 b/lib/vscode/build/azure-pipelines/win32/prepare-publish.ps1 similarity index 51% rename from lib/vscode/build/azure-pipelines/win32/publish.ps1 rename to lib/vscode/build/azure-pipelines/win32/prepare-publish.ps1 index a225f9d5fdf9..f80e1ca0ce9a 100644 --- a/lib/vscode/build/azure-pipelines/win32/publish.ps1 +++ b/lib/vscode/build/azure-pipelines/win32/prepare-publish.ps1 @@ -13,24 +13,31 @@ $Zip = "$Repo\.build\win32-$Arch\archive\VSCode-win32-$Arch.zip" $LegacyServer = "$Root\vscode-reh-win32-$Arch" $Server = "$Root\vscode-server-win32-$Arch" $ServerZip = "$Repo\.build\vscode-server-win32-$Arch.zip" +$LegacyWeb = "$Root\vscode-reh-web-win32-$Arch" +$Web = "$Root\vscode-server-win32-$Arch-web" +$WebZip = "$Repo\.build\vscode-server-win32-$Arch-web.zip" $Build = "$Root\VSCode-win32-$Arch" # Create server archive if ("$Arch" -ne "arm64") { exec { xcopy $LegacyServer $Server /H /E /I } exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $ServerZip $Server -r } + exec { xcopy $LegacyWeb $Web /H /E /I } + exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $WebZip $Web -r } } # get version $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json $Version = $PackageJson.version -$AssetPlatform = if ("$Arch" -eq "ia32") { "win32" } else { "win32-$Arch" } - -exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-archive" archive "VSCode-win32-$Arch-$Version.zip" $Zip } -exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform" setup "VSCodeSetup-$Arch-$Version.exe" $SystemExe } -exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-user" setup "VSCodeUserSetup-$Arch-$Version.exe" $UserExe } - -if ("$Arch" -ne "arm64") { - exec { node build/azure-pipelines/common/createAsset.js "server-$AssetPlatform" archive "vscode-server-win32-$Arch.zip" $ServerZip } -} +$ARCHIVE_NAME = "VSCode-win32-$Arch-$Version.zip" +$SYSTEM_SETUP_NAME = "VSCodeSetup-$Arch-$Version.exe" +$USER_SETUP_NAME = "VSCodeUserSetup-$Arch-$Version.exe" + +# Set variables for upload +Move-Item $Zip "$Repo\.build\win32-$Arch\archive\$ARCHIVE_NAME" +Write-Host "##vso[task.setvariable variable=ARCHIVE_NAME]$ARCHIVE_NAME" +Move-Item $SystemExe "$Repo\.build\win32-$Arch\system-setup\$SYSTEM_SETUP_NAME" +Write-Host "##vso[task.setvariable variable=SYSTEM_SETUP_NAME]$SYSTEM_SETUP_NAME" +Move-Item $UserExe "$Repo\.build\win32-$Arch\user-setup\$USER_SETUP_NAME" +Write-Host "##vso[task.setvariable variable=USER_SETUP_NAME]$USER_SETUP_NAME" diff --git a/lib/vscode/build/azure-pipelines/win32/product-build-win32.yml b/lib/vscode/build/azure-pipelines/win32/product-build-win32.yml index 2dcaf8b2e010..1f8514ae7e3e 100644 --- a/lib/vscode/build/azure-pipelines/win32/product-build-win32.yml +++ b/lib/vscode/build/azure-pipelines/win32/product-build-win32.yml @@ -295,31 +295,31 @@ steps: $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(vscode-storage-key)" $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - .\build\azure-pipelines\win32\publish.ps1 + .\build\azure-pipelines\win32\prepare-publish.ps1 displayName: Publish condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip - artifact: vscode-win32-$(VSCODE_ARCH) + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\archive\$(ARCHIVE_NAME) + artifact: vscode_client_win32_$(VSCODE_ARCH)_archive displayName: Publish archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe - artifact: vscode-win32-$(VSCODE_ARCH)-setup + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\system-setup\$(SYSTEM_SETUP_NAME) + artifact: vscode_client_win32_$(VSCODE_ARCH)_setup displayName: Publish system setup condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe - artifact: vscode-win32-$(VSCODE_ARCH)-user-setup + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\user-setup\$(USER_SETUP_NAME) + artifact: vscode_client_win32_$(VSCODE_ARCH)_user-setup displayName: Publish user setup condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(System.DefaultWorkingDirectory)\.build\vscode-server-win32-$(VSCODE_ARCH).zip - artifact: vscode-server-win32-$(VSCODE_ARCH) + artifact: vscode_server_win32_$(VSCODE_ARCH)_archive displayName: Publish server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - publish: $(System.DefaultWorkingDirectory)\.build\vscode-server-win32-$(VSCODE_ARCH)-web.zip - artifact: vscode-server-win32-$(VSCODE_ARCH)-web + artifact: vscode_web_win32_$(VSCODE_ARCH)_archive displayName: Publish web server archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) diff --git a/lib/vscode/build/darwin/create-universal-app.js b/lib/vscode/build/darwin/create-universal-app.js index d455a5cef703..d91064d41a60 100644 --- a/lib/vscode/build/darwin/create-universal-app.js +++ b/lib/vscode/build/darwin/create-universal-app.js @@ -33,7 +33,7 @@ async function main() { 'Credits.rtf', 'CodeResources', 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds + 'Info.plist', '.npmrc' ], outAppPath, diff --git a/lib/vscode/build/gulpfile.extensions.js b/lib/vscode/build/gulpfile.extensions.js index 9312f6c930a2..cbb7c486b13d 100644 --- a/lib/vscode/build/gulpfile.extensions.js +++ b/lib/vscode/build/gulpfile.extensions.js @@ -8,7 +8,6 @@ require('events').EventEmitter.defaultMaxListeners = 100; const gulp = require('gulp'); const path = require('path'); -const child_process = require('child_process'); const nodeUtil = require('util'); const es = require('event-stream'); const filter = require('gulp-filter'); @@ -20,8 +19,6 @@ const glob = require('glob'); const root = path.dirname(__dirname); const commit = util.getVersion(root); const plumber = require('gulp-plumber'); -const fancyLog = require('fancy-log'); -const ansiColors = require('ansi-colors'); const ext = require('./lib/extensions'); const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); @@ -201,45 +198,17 @@ gulp.task(compileExtensionsBuildLegacyTask); //#region Extension media -// Additional projects to webpack. These typically build code for webviews -const webpackMediaConfigFiles = [ - 'markdown-language-features/webpack.config.js', - 'simple-browser/webpack.config.js', -]; - -// Additional projects to run esbuild on. These typically build code for webviews -const esbuildMediaScripts = [ - 'markdown-language-features/esbuild.js', - 'notebook-markdown-extensions/esbuild.js', -]; - -const compileExtensionMediaTask = task.define('compile-extension-media', () => buildExtensionMedia(false)); +const compileExtensionMediaTask = task.define('compile-extension-media', () => ext.buildExtensionMedia(false)); gulp.task(compileExtensionMediaTask); exports.compileExtensionMediaTask = compileExtensionMediaTask; -const watchExtensionMedia = task.define('watch-extension-media', () => buildExtensionMedia(true)); +const watchExtensionMedia = task.define('watch-extension-media', () => ext.buildExtensionMedia(true)); gulp.task(watchExtensionMedia); exports.watchExtensionMedia = watchExtensionMedia; -const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => buildExtensionMedia(false, '.build/extensions')); +const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => ext.buildExtensionMedia(false, '.build/extensions')); gulp.task(compileExtensionMediaBuildTask); -async function buildExtensionMedia(isWatch, outputRoot) { - const webpackConfigLocations = webpackMediaConfigFiles.map(p => { - return { - configPath: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }; - }); - return Promise.all([ - webpackExtensions('webpacking extension media', isWatch, webpackConfigLocations), - esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }))), - ]); -} - //#endregion //#region Azure Pipelines @@ -271,121 +240,5 @@ async function buildWebExtensions(isWatch) { path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), { ignore: ['**/node_modules'] } ); - return webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))); -} - -/** - * @param {string} taskName - * @param {boolean} isWatch - * @param {{ configPath: string, outputRoot?: boolean}} webpackConfigLocations - */ -async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { - const webpack = require('webpack'); - - const webpackConfigs = []; - - for (const { configPath, outputRoot } of webpackConfigLocations) { - const configOrFnOrArray = require(configPath); - function addConfig(configOrFn) { - let config; - if (typeof configOrFn === 'function') { - config = configOrFn({}, {}); - webpackConfigs.push(config); - } else { - config = configOrFn; - } - - if (outputRoot) { - config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); - } - - webpackConfigs.push(configOrFn); - } - addConfig(configOrFnOrArray); - } - function reporter(fullStats) { - if (Array.isArray(fullStats.children)) { - for (const stats of fullStats.children) { - const outputPath = stats.outputPath; - if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); - const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`); - } - if (Array.isArray(stats.errors)) { - stats.errors.forEach(error => { - fancyLog.error(error); - }); - } - if (Array.isArray(stats.warnings)) { - stats.warnings.forEach(warning => { - fancyLog.warn(warning); - }); - } - } - } - } - return new Promise((resolve, reject) => { - if (isWatch) { - webpack(webpackConfigs).watch({}, (err, stats) => { - if (err) { - reject(); - } else { - reporter(stats.toJson()); - } - }); - } else { - webpack(webpackConfigs).run((err, stats) => { - if (err) { - fancyLog.error(err); - reject(); - } else { - reporter(stats.toJson()); - resolve(); - } - }); - } - }); -} - -/** - * @param {string} taskName - * @param {boolean} isWatch - * @param {{ script: string, outputRoot?: string }}} scripts - */ -async function esbuildExtensions(taskName, isWatch, scripts) { - function reporter(/** @type {string} */ stdError, /** @type {string} */script) { - const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); - fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); - for (const match of matches || []) { - fancyLog.error(match); - } - } - - const tasks = scripts.map(({ script, outputRoot }) => { - return new Promise((resolve, reject) => { - const args = [script]; - if (isWatch) { - args.push('--watch'); - } - if (outputRoot) { - args.push('--outputRoot', outputRoot); - } - const proc = child_process.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { - if (error) { - return reject(error); - } - reporter(stderr, script); - if (stderr) { - return reject(); - } - return resolve(); - }); - - proc.stdout.on('data', (data) => { - fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); - }); - }); - }); - return Promise.all(tasks); + return ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))); } diff --git a/lib/vscode/build/gulpfile.reh.js b/lib/vscode/build/gulpfile.reh.js index 6230916a1dc8..0704ab70e9d1 100644 --- a/lib/vscode/build/gulpfile.reh.js +++ b/lib/vscode/build/gulpfile.reh.js @@ -42,6 +42,7 @@ BUILD_TARGETS.forEach(({ platform, arch }) => { }); function getNodeVersion() { + // NOTE@coder: Fix version due to .yarnrc removal. return process.versions.node; const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; diff --git a/lib/vscode/build/gulpfile.vscode.js b/lib/vscode/build/gulpfile.vscode.js index 3eeba69d2581..a6e043f122cb 100644 --- a/lib/vscode/build/gulpfile.vscode.js +++ b/lib/vscode/build/gulpfile.vscode.js @@ -228,7 +228,14 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) .pipe(jsFilter.restore) - .pipe(createAsar(path.join(process.cwd(), 'node_modules'), ['**/*.node', '**/vscode-ripgrep/bin/*', '**/node-pty/build/Release/*', '**/*.wasm'], 'node_modules.asar')); + .pipe(createAsar(path.join(process.cwd(), 'node_modules'), [ + '**/*.node', + '**/vscode-ripgrep/bin/*', + '**/node-pty/build/Release/*', + '**/node-pty/lib/worker/conoutSocketWorker.js', + '**/node-pty/lib/shared/conout.js', + '**/*.wasm' + ], 'node_modules.asar')); let all = es.merge( packageJsonStream, @@ -383,8 +390,6 @@ BUILD_TARGETS.forEach(buildTarget => { } }); -// Transifex Localizations - const innoSetupConfig = { 'zh-cn': { codePage: 'CP936', defaultInfo: { name: 'Simplified Chinese', id: '$0804', } }, 'zh-tw': { codePage: 'CP950', defaultInfo: { name: 'Traditional Chinese', id: '$0404' } }, @@ -400,6 +405,8 @@ const innoSetupConfig = { 'tr': { codePage: 'CP1254' } }; +// Transifex Localizations + const apiHostname = process.env.TRANSIFEX_API_URL; const apiName = process.env.TRANSIFEX_API_NAME; const apiToken = process.env.TRANSIFEX_API_TOKEN; @@ -434,7 +441,7 @@ gulp.task(task.define( function () { const pathToMetadata = './out-vscode/nls.metadata.json'; const pathToExtensions = '.build/extensions/*'; - const pathToSetup = 'build/win32/**/{Default.isl,messages.en.isl}'; + const pathToSetup = 'build/win32/i18n/messages.en.isl'; return es.merge( gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), @@ -460,8 +467,8 @@ gulp.task('vscode-translations-import', function () { } }); return es.merge([...i18n.defaultLanguages, ...i18n.extraLanguages].map(language => { - let id = language.transifexId || language.id; - return gulp.src(`${options.location}/${id}/setup/*/*.xlf`) + let id = language.id; + return gulp.src(`${options.location}/${id}/vscode-setup/messages.xlf`) .pipe(i18n.prepareIslFiles(language, innoSetupConfig[language.id])) .pipe(vfs.dest(`./build/win32/i18n`)); })); diff --git a/lib/vscode/build/lib/builtInExtensions.js b/lib/vscode/build/lib/builtInExtensions.js index 9b8bcc9e0009..d851a6ee5f9a 100644 --- a/lib/vscode/build/lib/builtInExtensions.js +++ b/lib/vscode/build/lib/builtInExtensions.js @@ -18,8 +18,8 @@ const ansiColors = require("ansi-colors"); const mkdirp = require('mkdirp'); const root = path.dirname(path.dirname(__dirname)); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions; -const webBuiltInExtensions = productjson.webBuiltInExtensions; +const builtInExtensions = productjson.builtInExtensions || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; function log(...messages) { diff --git a/lib/vscode/build/lib/builtInExtensions.ts b/lib/vscode/build/lib/builtInExtensions.ts index af444defab88..2c1cb3abba2c 100644 --- a/lib/vscode/build/lib/builtInExtensions.ts +++ b/lib/vscode/build/lib/builtInExtensions.ts @@ -36,8 +36,8 @@ export interface IExtensionDefinition { const root = path.dirname(path.dirname(__dirname)); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions; -const webBuiltInExtensions = productjson.webBuiltInExtensions; +const builtInExtensions = productjson.builtInExtensions || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; diff --git a/lib/vscode/build/lib/builtInExtensionsCG.js b/lib/vscode/build/lib/builtInExtensionsCG.js index 10bf38f8c8e2..679663724c8b 100644 --- a/lib/vscode/build/lib/builtInExtensionsCG.js +++ b/lib/vscode/build/lib/builtInExtensionsCG.js @@ -12,8 +12,8 @@ const ansiColors = require("ansi-colors"); const root = path.dirname(path.dirname(__dirname)); const rootCG = path.join(root, 'extensionsCG'); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions; -const webBuiltInExtensions = productjson.webBuiltInExtensions; +const builtInExtensions = productjson.builtInExtensions || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const token = process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN'] || undefined; const contentBasePath = 'raw.githubusercontent.com'; const contentFileNames = ['package.json', 'package-lock.json', 'yarn.lock']; diff --git a/lib/vscode/build/lib/builtInExtensionsCG.ts b/lib/vscode/build/lib/builtInExtensionsCG.ts index 45785529b66d..2b758da5c912 100644 --- a/lib/vscode/build/lib/builtInExtensionsCG.ts +++ b/lib/vscode/build/lib/builtInExtensionsCG.ts @@ -13,8 +13,8 @@ import { IExtensionDefinition } from './builtInExtensions'; const root = path.dirname(path.dirname(__dirname)); const rootCG = path.join(root, 'extensionsCG'); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions; -const webBuiltInExtensions = productjson.webBuiltInExtensions; +const builtInExtensions = productjson.builtInExtensions || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const token = process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN'] || undefined; const contentBasePath = 'raw.githubusercontent.com'; diff --git a/lib/vscode/build/lib/extensions.js b/lib/vscode/build/lib/extensions.js index b25c3713ea83..3d6a2906178c 100644 --- a/lib/vscode/build/lib/extensions.js +++ b/lib/vscode/build/lib/extensions.js @@ -4,9 +4,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; +exports.buildExtensionMedia = exports.webpackExtensions = exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; const es = require("event-stream"); const fs = require("fs"); +const cp = require("child_process"); const glob = require("glob"); const gulp = require("gulp"); const path = require("path"); @@ -328,3 +329,132 @@ function translatePackageJSON(packageJSON, packageNLSPath) { return packageJSON; } exports.translatePackageJSON = translatePackageJSON; +const extensionsPath = path.join(root, 'extensions'); +// Additional projects to webpack. These typically build code for webviews +const webpackMediaConfigFiles = [ + 'markdown-language-features/webpack.config.js', + 'simple-browser/webpack.config.js', +]; +// Additional projects to run esbuild on. These typically build code for webviews +const esbuildMediaScripts = [ + 'markdown-language-features/esbuild.js', + 'notebook-markdown-extensions/esbuild.js', +]; +async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { + const webpack = require('webpack'); + const webpackConfigs = []; + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath); + function addConfig(configOrFn) { + let config; + if (typeof configOrFn === 'function') { + config = configOrFn({}, {}); + webpackConfigs.push(config); + } + else { + config = configOrFn; + } + if (outputRoot) { + config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + } + webpackConfigs.push(configOrFn); + } + addConfig(configOrFnOrArray); + } + function reporter(fullStats) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } + else { + reporter(stats.toJson()); + } + }); + } + else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } + else { + reporter(stats.toJson()); + resolve(); + } + }); + } + }); +} +exports.webpackExtensions = webpackExtensions; +async function esbuildExtensions(taskName, isWatch, scripts) { + function reporter(stdError, script) { + const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); + fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); + for (const match of matches || []) { + fancyLog.error(match); + } + } + const tasks = scripts.map(({ script, outputRoot }) => { + return new Promise((resolve, reject) => { + const args = [script]; + if (isWatch) { + args.push('--watch'); + } + if (outputRoot) { + args.push('--outputRoot', outputRoot); + } + const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { + if (error) { + return reject(error); + } + reporter(stderr, script); + if (stderr) { + return reject(); + } + return resolve(); + }); + proc.stdout.on('data', (data) => { + fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); + }); + }); + }); + return Promise.all(tasks); +} +async function buildExtensionMedia(isWatch, outputRoot) { + return Promise.all([ + webpackExtensions('webpacking extension media', isWatch, webpackMediaConfigFiles.map(p => { + return { + configPath: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }; + })), + esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ + script: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }))), + ]); +} +exports.buildExtensionMedia = buildExtensionMedia; diff --git a/lib/vscode/build/lib/extensions.ts b/lib/vscode/build/lib/extensions.ts index ec5c7d03b101..9281bb19e20b 100644 --- a/lib/vscode/build/lib/extensions.ts +++ b/lib/vscode/build/lib/extensions.ts @@ -5,6 +5,7 @@ import * as es from 'event-stream'; import * as fs from 'fs'; +import * as cp from 'child_process'; import * as glob from 'glob'; import * as gulp from 'gulp'; import * as path from 'path'; @@ -19,6 +20,7 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; const buffer = require('gulp-buffer'); import * as jsoncParser from 'jsonc-parser'; +import webpack = require('webpack'); const util = require('./util'); const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); @@ -403,3 +405,138 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string translate(packageJSON); return packageJSON; } + +const extensionsPath = path.join(root, 'extensions'); + +// Additional projects to webpack. These typically build code for webviews +const webpackMediaConfigFiles = [ + 'markdown-language-features/webpack.config.js', + 'simple-browser/webpack.config.js', +]; + +// Additional projects to run esbuild on. These typically build code for webviews +const esbuildMediaScripts = [ + 'markdown-language-features/esbuild.js', + 'notebook-markdown-extensions/esbuild.js', +]; + +export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string, outputRoot?: string }[]) { + const webpack = require('webpack') as typeof import('webpack'); + + const webpackConfigs: webpack.Configuration[] = []; + + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath); + function addConfig(configOrFn: webpack.Configuration | Function) { + let config; + if (typeof configOrFn === 'function') { + config = configOrFn({}, {}); + webpackConfigs.push(config); + } else { + config = configOrFn; + } + + if (outputRoot) { + config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + } + + webpackConfigs.push(configOrFn); + } + addConfig(configOrFnOrArray); + } + function reporter(fullStats: any) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error: any) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning: any) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } else { + reporter(stats.toJson()); + } + }); + } else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } else { + reporter(stats.toJson()); + resolve(); + } + }); + } + }); +} + +async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string, outputRoot?: string }[]) { + function reporter(stdError: string, script: string) { + const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); + fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); + for (const match of matches || []) { + fancyLog.error(match); + } + } + + const tasks = scripts.map(({ script, outputRoot }) => { + return new Promise((resolve, reject) => { + const args = [script]; + if (isWatch) { + args.push('--watch'); + } + if (outputRoot) { + args.push('--outputRoot', outputRoot); + } + const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { + if (error) { + return reject(error); + } + reporter(stderr, script); + if (stderr) { + return reject(); + } + return resolve(); + }); + + proc.stdout!.on('data', (data) => { + fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); + }); + }); + }); + return Promise.all(tasks); +} + +export async function buildExtensionMedia(isWatch: boolean, outputRoot?: string) { + return Promise.all([ + webpackExtensions('webpacking extension media', isWatch, webpackMediaConfigFiles.map(p => { + return { + configPath: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }; + })), + esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ + script: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }))), + ]); +} diff --git a/lib/vscode/build/lib/i18n.js b/lib/vscode/build/lib/i18n.js index 50ec3f3f45a0..4c7c5c32eb44 100644 --- a/lib/vscode/build/lib/i18n.js +++ b/lib/vscode/build/lib/i18n.js @@ -4,14 +4,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.pullI18nPackFiles = exports.prepareI18nFiles = exports.pullSetupXlfFiles = exports.pullCoreAndExtensionsXlfFiles = exports.findObsoleteResources = exports.pushXlfFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.Limiter = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0; +exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.prepareI18nFiles = exports.pullSetupXlfFiles = exports.findObsoleteResources = exports.pushXlfFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.Limiter = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0; const path = require("path"); const fs = require("fs"); const event_stream_1 = require("event-stream"); const File = require("vinyl"); const Is = require("is"); const xml2js = require("xml2js"); -const glob = require("glob"); const https = require("https"); const gulp = require("gulp"); const fancyLog = require("fancy-log"); @@ -19,1186 +18,1146 @@ const ansiColors = require("ansi-colors"); const iconv = require("iconv-lite-umd"); const NUMBER_OF_CONCURRENT_DOWNLOADS = 4; function log(message, ...rest) { - fancyLog(ansiColors.green('[i18n]'), message, ...rest); + fancyLog(ansiColors.green('[i18n]'), message, ...rest); } exports.defaultLanguages = [ - { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, - { id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' }, - { id: 'ja', folderName: 'jpn' }, - { id: 'ko', folderName: 'kor' }, - { id: 'de', folderName: 'deu' }, - { id: 'fr', folderName: 'fra' }, - { id: 'es', folderName: 'esn' }, - { id: 'ru', folderName: 'rus' }, - { id: 'it', folderName: 'ita' } + { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, + { id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' }, + { id: 'ja', folderName: 'jpn' }, + { id: 'ko', folderName: 'kor' }, + { id: 'de', folderName: 'deu' }, + { id: 'fr', folderName: 'fra' }, + { id: 'es', folderName: 'esn' }, + { id: 'ru', folderName: 'rus' }, + { id: 'it', folderName: 'ita' } ]; // languages requested by the community to non-stable builds exports.extraLanguages = [ - { id: 'pt-br', folderName: 'ptb' }, - { id: 'hu', folderName: 'hun' }, - { id: 'tr', folderName: 'trk' } + { id: 'pt-br', folderName: 'ptb' }, + { id: 'hu', folderName: 'hun' }, + { id: 'tr', folderName: 'trk' } ]; // non built-in extensions also that are transifex and need to be part of the language packs exports.externalExtensionsWithTranslations = { - 'vscode-chrome-debug': 'msjsdiag.debugger-for-chrome', - 'vscode-node-debug': 'ms-vscode.node-debug', - 'vscode-node-debug2': 'ms-vscode.node-debug2' + 'vscode-chrome-debug': 'msjsdiag.debugger-for-chrome', + 'vscode-node-debug': 'ms-vscode.node-debug', + 'vscode-node-debug2': 'ms-vscode.node-debug2' }; var LocalizeInfo; (function (LocalizeInfo) { - function is(value) { - let candidate = value; - return Is.defined(candidate) && Is.string(candidate.key) && (Is.undef(candidate.comment) || (Is.array(candidate.comment) && candidate.comment.every(element => Is.string(element)))); - } - LocalizeInfo.is = is; + function is(value) { + let candidate = value; + return Is.defined(candidate) && Is.string(candidate.key) && (Is.undef(candidate.comment) || (Is.array(candidate.comment) && candidate.comment.every(element => Is.string(element)))); + } + LocalizeInfo.is = is; })(LocalizeInfo || (LocalizeInfo = {})); var BundledFormat; (function (BundledFormat) { - function is(value) { - if (Is.undef(value)) { - return false; - } - let candidate = value; - let length = Object.keys(value).length; - return length === 3 && Is.defined(candidate.keys) && Is.defined(candidate.messages) && Is.defined(candidate.bundles); - } - BundledFormat.is = is; + function is(value) { + if (Is.undef(value)) { + return false; + } + let candidate = value; + let length = Object.keys(value).length; + return length === 3 && Is.defined(candidate.keys) && Is.defined(candidate.messages) && Is.defined(candidate.bundles); + } + BundledFormat.is = is; })(BundledFormat || (BundledFormat = {})); var PackageJsonFormat; (function (PackageJsonFormat) { - function is(value) { - if (Is.undef(value) || !Is.object(value)) { - return false; - } - return Object.keys(value).every(key => { - let element = value[key]; - return Is.string(element) || (Is.object(element) && Is.defined(element.message) && Is.defined(element.comment)); - }); - } - PackageJsonFormat.is = is; + function is(value) { + if (Is.undef(value) || !Is.object(value)) { + return false; + } + return Object.keys(value).every(key => { + let element = value[key]; + return Is.string(element) || (Is.object(element) && Is.defined(element.message) && Is.defined(element.comment)); + }); + } + PackageJsonFormat.is = is; })(PackageJsonFormat || (PackageJsonFormat = {})); class Line { - constructor(indent = 0) { - this.buffer = []; - if (indent > 0) { - this.buffer.push(new Array(indent + 1).join(' ')); - } - } - append(value) { - this.buffer.push(value); - return this; - } - toString() { - return this.buffer.join(''); - } + constructor(indent = 0) { + this.buffer = []; + if (indent > 0) { + this.buffer.push(new Array(indent + 1).join(' ')); + } + } + append(value) { + this.buffer.push(value); + return this; + } + toString() { + return this.buffer.join(''); + } } exports.Line = Line; class TextModel { - constructor(contents) { - this._lines = contents.split(/\r\n|\r|\n/); - } - get lines() { - return this._lines; - } + constructor(contents) { + this._lines = contents.split(/\r\n|\r|\n/); + } + get lines() { + return this._lines; + } } class XLF { - constructor(project) { - this.project = project; - this.buffer = []; - this.files = Object.create(null); - this.numberOfMessages = 0; - } - toString() { - this.appendHeader(); - for (let file in this.files) { - this.appendNewLine(``, 2); - for (let item of this.files[file]) { - this.addStringItem(file, item); - } - this.appendNewLine('', 2); - } - this.appendFooter(); - return this.buffer.join('\r\n'); - } - addFile(original, keys, messages) { - if (keys.length === 0) { - console.log('No keys in ' + original); - return; - } - if (keys.length !== messages.length) { - throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`); - } - this.numberOfMessages += keys.length; - this.files[original] = []; - let existingKeys = new Set(); - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - let realKey; - let comment; - if (Is.string(key)) { - realKey = key; - comment = undefined; - } - else if (LocalizeInfo.is(key)) { - realKey = key.key; - if (key.comment && key.comment.length > 0) { - comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n'); - } - } - if (!realKey || existingKeys.has(realKey)) { - continue; - } - existingKeys.add(realKey); - let message = encodeEntities(messages[i]); - this.files[original].push({ id: realKey, message: message, comment: comment }); - } - } - addStringItem(file, item) { - if (!item.id || item.message === undefined || item.message === null) { - throw new Error(`No item ID or value specified: ${JSON.stringify(item)}. File: ${file}`); - } - if (item.message.length === 0) { - log(`Item with id ${item.id} in file ${file} has an empty message.`); - } - this.appendNewLine(``, 4); - this.appendNewLine(`${item.message}`, 6); - if (item.comment) { - this.appendNewLine(`${item.comment}`, 6); - } - this.appendNewLine('', 4); - } - appendHeader() { - this.appendNewLine('', 0); - this.appendNewLine('', 0); - } - appendFooter() { - this.appendNewLine('', 0); - } - appendNewLine(content, indent) { - let line = new Line(indent); - line.append(content); - this.buffer.push(line.toString()); - } + constructor(project) { + this.project = project; + this.buffer = []; + this.files = Object.create(null); + this.numberOfMessages = 0; + } + toString() { + this.appendHeader(); + const files = Object.keys(this.files).sort(); + for (const file of files) { + this.appendNewLine(``, 2); + const items = this.files[file].sort((a, b) => { + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; + }); + for (const item of items) { + this.addStringItem(file, item); + } + this.appendNewLine(''); + } + this.appendFooter(); + return this.buffer.join('\r\n'); + } + addFile(original, keys, messages) { + if (keys.length === 0) { + console.log('No keys in ' + original); + return; + } + if (keys.length !== messages.length) { + throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`); + } + this.numberOfMessages += keys.length; + this.files[original] = []; + let existingKeys = new Set(); + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let realKey; + let comment; + if (Is.string(key)) { + realKey = key; + comment = undefined; + } + else if (LocalizeInfo.is(key)) { + realKey = key.key; + if (key.comment && key.comment.length > 0) { + comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n'); + } + } + if (!realKey || existingKeys.has(realKey)) { + continue; + } + existingKeys.add(realKey); + let message = encodeEntities(messages[i]); + this.files[original].push({ id: realKey, message: message, comment: comment }); + } + } + addStringItem(file, item) { + if (!item.id || item.message === undefined || item.message === null) { + throw new Error(`No item ID or value specified: ${JSON.stringify(item)}. File: ${file}`); + } + if (item.message.length === 0) { + log(`Item with id ${item.id} in file ${file} has an empty message.`); + } + this.appendNewLine(``, 4); + this.appendNewLine(`${item.message}`, 6); + if (item.comment) { + this.appendNewLine(`${item.comment}`, 6); + } + this.appendNewLine('', 4); + } + appendHeader() { + this.appendNewLine('', 0); + this.appendNewLine('', 0); + } + appendFooter() { + this.appendNewLine('', 0); + } + appendNewLine(content, indent) { + let line = new Line(indent); + line.append(content); + this.buffer.push(line.toString()); + } } exports.XLF = XLF; XLF.parsePseudo = function (xlfString) { - return new Promise((resolve) => { - let parser = new xml2js.Parser(); - let files = []; - parser.parseString(xlfString, function (_err, result) { - const fileNodes = result['xliff']['file']; - fileNodes.forEach(file => { - const originalFilePath = file.$.original; - const messages = {}; - const transUnits = file.body[0]['trans-unit']; - if (transUnits) { - transUnits.forEach((unit) => { - const key = unit.$.id; - const val = pseudify(unit.source[0]['_'].toString()); - if (key && val) { - messages[key] = decodeEntities(val); - } - }); - files.push({ messages: messages, originalFilePath: originalFilePath, language: 'ps' }); - } - }); - resolve(files); - }); - }); + return new Promise((resolve) => { + let parser = new xml2js.Parser(); + let files = []; + parser.parseString(xlfString, function (_err, result) { + const fileNodes = result['xliff']['file']; + fileNodes.forEach(file => { + const originalFilePath = file.$.original; + const messages = {}; + const transUnits = file.body[0]['trans-unit']; + if (transUnits) { + transUnits.forEach((unit) => { + const key = unit.$.id; + const val = pseudify(unit.source[0]['_'].toString()); + if (key && val) { + messages[key] = decodeEntities(val); + } + }); + files.push({ messages: messages, originalFilePath: originalFilePath, language: 'ps' }); + } + }); + resolve(files); + }); + }); }; XLF.parse = function (xlfString) { - return new Promise((resolve, reject) => { - let parser = new xml2js.Parser(); - let files = []; - parser.parseString(xlfString, function (err, result) { - if (err) { - reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`)); - } - const fileNodes = result['xliff']['file']; - if (!fileNodes) { - reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`)); - } - fileNodes.forEach((file) => { - const originalFilePath = file.$.original; - if (!originalFilePath) { - reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`)); - } - let language = file.$['target-language']; - if (!language) { - reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`)); - } - const messages = {}; - const transUnits = file.body[0]['trans-unit']; - if (transUnits) { - transUnits.forEach((unit) => { - const key = unit.$.id; - if (!unit.target) { - return; // No translation available - } - let val = unit.target[0]; - if (typeof val !== 'string') { - // We allow empty source values so support them for translations as well. - val = val._ ? val._ : ''; - } - if (!key) { - reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${originalFilePath} is missing the ID attribute.`)); - return; - } - messages[key] = decodeEntities(val); - }); - files.push({ messages: messages, originalFilePath: originalFilePath, language: language.toLowerCase() }); - } - }); - resolve(files); - }); - }); + return new Promise((resolve, reject) => { + let parser = new xml2js.Parser(); + let files = []; + parser.parseString(xlfString, function (err, result) { + if (err) { + reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`)); + } + const fileNodes = result['xliff']['file']; + if (!fileNodes) { + reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`)); + } + fileNodes.forEach((file) => { + const originalFilePath = file.$.original; + if (!originalFilePath) { + reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`)); + } + let language = file.$['target-language']; + if (!language) { + reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`)); + } + const messages = {}; + const transUnits = file.body[0]['trans-unit']; + if (transUnits) { + transUnits.forEach((unit) => { + const key = unit.$.id; + if (!unit.target) { + return; // No translation available + } + let val = unit.target[0]; + if (typeof val !== 'string') { + // We allow empty source values so support them for translations as well. + val = val._ ? val._ : ''; + } + if (!key) { + reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${originalFilePath} is missing the ID attribute.`)); + return; + } + messages[key] = decodeEntities(val); + }); + files.push({ messages: messages, originalFilePath: originalFilePath, language: language.toLowerCase() }); + } + }); + resolve(files); + }); + }); }; class Limiter { - constructor(maxDegreeOfParalellism) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - } - queue(factory) { - return new Promise((c, e) => { - this.outstandingPromises.push({ factory, c, e }); - this.consume(); - }); - } - consume() { - while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { - const iLimitedTask = this.outstandingPromises.shift(); - this.runningPromises++; - const promise = iLimitedTask.factory(); - promise.then(iLimitedTask.c).catch(iLimitedTask.e); - promise.then(() => this.consumed()).catch(() => this.consumed()); - } - } - consumed() { - this.runningPromises--; - this.consume(); - } + constructor(maxDegreeOfParalellism) { + this.maxDegreeOfParalellism = maxDegreeOfParalellism; + this.outstandingPromises = []; + this.runningPromises = 0; + } + queue(factory) { + return new Promise((c, e) => { + this.outstandingPromises.push({ factory, c, e }); + this.consume(); + }); + } + consume() { + while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { + const iLimitedTask = this.outstandingPromises.shift(); + this.runningPromises++; + const promise = iLimitedTask.factory(); + promise.then(iLimitedTask.c).catch(iLimitedTask.e); + promise.then(() => this.consumed()).catch(() => this.consumed()); + } + } + consumed() { + this.runningPromises--; + this.consume(); + } } exports.Limiter = Limiter; function sortLanguages(languages) { - return languages.sort((a, b) => { - return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0); - }); + return languages.sort((a, b) => { + return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0); + }); } function stripComments(content) { - /** - * First capturing group matches double quoted string - * Second matches single quotes string - * Third matches block comments - * Fourth matches line comments - */ - const regexp = /("(?:[^\\\"]*(?:\\.)?)*")|('(?:[^\\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; - let result = content.replace(regexp, (match, _m1, _m2, m3, m4) => { - // Only one of m1, m2, m3, m4 matches - if (m3) { - // A block comment. Replace with nothing - return ''; - } - else if (m4) { - // A line comment. If it ends in \r?\n then keep it. - let length = m4.length; - if (length > 2 && m4[length - 1] === '\n') { - return m4[length - 2] === '\r' ? '\r\n' : '\n'; - } - else { - return ''; - } - } - else { - // We match a string - return match; - } - }); - return result; + /** + * First capturing group matches double quoted string + * Second matches single quotes string + * Third matches block comments + * Fourth matches line comments + */ + const regexp = /("(?:[^\\\"]*(?:\\.)?)*")|('(?:[^\\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; + let result = content.replace(regexp, (match, _m1, _m2, m3, m4) => { + // Only one of m1, m2, m3, m4 matches + if (m3) { + // A block comment. Replace with nothing + return ''; + } + else if (m4) { + // A line comment. If it ends in \r?\n then keep it. + let length = m4.length; + if (length > 2 && m4[length - 1] === '\n') { + return m4[length - 2] === '\r' ? '\r\n' : '\n'; + } + else { + return ''; + } + } + else { + // We match a string + return match; + } + }); + return result; } function escapeCharacters(value) { - const result = []; - for (let i = 0; i < value.length; i++) { - const ch = value.charAt(i); - switch (ch) { - case '\'': - result.push('\\\''); - break; - case '"': - result.push('\\"'); - break; - case '\\': - result.push('\\\\'); - break; - case '\n': - result.push('\\n'); - break; - case '\r': - result.push('\\r'); - break; - case '\t': - result.push('\\t'); - break; - case '\b': - result.push('\\b'); - break; - case '\f': - result.push('\\f'); - break; - default: - result.push(ch); - } - } - return result.join(''); + const result = []; + for (let i = 0; i < value.length; i++) { + const ch = value.charAt(i); + switch (ch) { + case '\'': + result.push('\\\''); + break; + case '"': + result.push('\\"'); + break; + case '\\': + result.push('\\\\'); + break; + case '\n': + result.push('\\n'); + break; + case '\r': + result.push('\\r'); + break; + case '\t': + result.push('\\t'); + break; + case '\b': + result.push('\\b'); + break; + case '\f': + result.push('\\f'); + break; + default: + result.push(ch); + } + } + return result.join(''); } function processCoreBundleFormat(fileHeader, languages, json, emitter) { - let keysSection = json.keys; - let messageSection = json.messages; - let bundleSection = json.bundles; - let statistics = Object.create(null); - let defaultMessages = Object.create(null); - let modules = Object.keys(keysSection); - modules.forEach((module) => { - let keys = keysSection[module]; - let messages = messageSection[module]; - if (!messages || keys.length !== messages.length) { - emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`); - return; - } - let messageMap = Object.create(null); - defaultMessages[module] = messageMap; - keys.map((key, i) => { - if (typeof key === 'string') { - messageMap[key] = messages[i]; - } - else { - messageMap[key.key] = messages[i]; - } - }); - }); - let languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); - if (!fs.existsSync(languageDirectory)) { - log(`No VS Code localization repository found. Looking at ${languageDirectory}`); - log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); - } - let sortedLanguages = sortLanguages(languages); - sortedLanguages.forEach((language) => { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`Generating nls bundles for: ${language.id}`); - } - statistics[language.id] = 0; - let localizedModules = Object.create(null); - let languageFolderName = language.translationId || language.id; - let i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); - let allMessages; - if (fs.existsSync(i18nFile)) { - let content = stripComments(fs.readFileSync(i18nFile, 'utf8')); - allMessages = JSON.parse(content); - } - modules.forEach((module) => { - let order = keysSection[module]; - let moduleMessage; - if (allMessages) { - moduleMessage = allMessages.contents[module]; - } - if (!moduleMessage) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized messages found for module ${module}. Using default messages.`); - } - moduleMessage = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; - } - let localizedMessages = []; - order.forEach((keyInfo) => { - let key = null; - if (typeof keyInfo === 'string') { - key = keyInfo; - } - else { - key = keyInfo.key; - } - let message = moduleMessage[key]; - if (!message) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized message found for key ${key} in module ${module}. Using default message.`); - } - message = defaultMessages[module][key]; - statistics[language.id] = statistics[language.id] + 1; - } - localizedMessages.push(message); - }); - localizedModules[module] = localizedMessages; - }); - Object.keys(bundleSection).forEach((bundle) => { - let modules = bundleSection[bundle]; - let contents = [ - fileHeader, - `define("${bundle}.nls.${language.id}", {` - ]; - modules.forEach((module, index) => { - contents.push(`\t"${module}": [`); - let messages = localizedModules[module]; - if (!messages) { - emitter.emit('error', `Didn't find messages for module ${module}.`); - return; - } - messages.forEach((message, index) => { - contents.push(`\t\t"${escapeCharacters(message)}${index < messages.length ? '",' : '"'}`); - }); - contents.push(index < modules.length - 1 ? '\t],' : '\t]'); - }); - contents.push('});'); - emitter.queue(new File({ path: bundle + '.nls.' + language.id + '.js', contents: Buffer.from(contents.join('\n'), 'utf-8') })); - }); - }); - Object.keys(statistics).forEach(key => { - let value = statistics[key]; - log(`${key} has ${value} untranslated strings.`); - }); - sortedLanguages.forEach(language => { - let stats = statistics[language.id]; - if (Is.undef(stats)) { - log(`\tNo translations found for language ${language.id}. Using default language instead.`); - } - }); + let keysSection = json.keys; + let messageSection = json.messages; + let bundleSection = json.bundles; + let statistics = Object.create(null); + let defaultMessages = Object.create(null); + let modules = Object.keys(keysSection); + modules.forEach((module) => { + let keys = keysSection[module]; + let messages = messageSection[module]; + if (!messages || keys.length !== messages.length) { + emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`); + return; + } + let messageMap = Object.create(null); + defaultMessages[module] = messageMap; + keys.map((key, i) => { + if (typeof key === 'string') { + messageMap[key] = messages[i]; + } + else { + messageMap[key.key] = messages[i]; + } + }); + }); + let languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); + if (!fs.existsSync(languageDirectory)) { + log(`No VS Code localization repository found. Looking at ${languageDirectory}`); + log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); + } + let sortedLanguages = sortLanguages(languages); + sortedLanguages.forEach((language) => { + if (process.env['VSCODE_BUILD_VERBOSE']) { + log(`Generating nls bundles for: ${language.id}`); + } + statistics[language.id] = 0; + let localizedModules = Object.create(null); + let languageFolderName = language.translationId || language.id; + let i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); + let allMessages; + if (fs.existsSync(i18nFile)) { + let content = stripComments(fs.readFileSync(i18nFile, 'utf8')); + allMessages = JSON.parse(content); + } + modules.forEach((module) => { + let order = keysSection[module]; + let moduleMessage; + if (allMessages) { + moduleMessage = allMessages.contents[module]; + } + if (!moduleMessage) { + if (process.env['VSCODE_BUILD_VERBOSE']) { + log(`No localized messages found for module ${module}. Using default messages.`); + } + moduleMessage = defaultMessages[module]; + statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; + } + let localizedMessages = []; + order.forEach((keyInfo) => { + let key = null; + if (typeof keyInfo === 'string') { + key = keyInfo; + } + else { + key = keyInfo.key; + } + let message = moduleMessage[key]; + if (!message) { + if (process.env['VSCODE_BUILD_VERBOSE']) { + log(`No localized message found for key ${key} in module ${module}. Using default message.`); + } + message = defaultMessages[module][key]; + statistics[language.id] = statistics[language.id] + 1; + } + localizedMessages.push(message); + }); + localizedModules[module] = localizedMessages; + }); + Object.keys(bundleSection).forEach((bundle) => { + let modules = bundleSection[bundle]; + let contents = [ + fileHeader, + `define("${bundle}.nls.${language.id}", {` + ]; + modules.forEach((module, index) => { + contents.push(`\t"${module}": [`); + let messages = localizedModules[module]; + if (!messages) { + emitter.emit('error', `Didn't find messages for module ${module}.`); + return; + } + messages.forEach((message, index) => { + contents.push(`\t\t"${escapeCharacters(message)}${index < messages.length ? '",' : '"'}`); + }); + contents.push(index < modules.length - 1 ? '\t],' : '\t]'); + }); + contents.push('});'); + emitter.queue(new File({ path: bundle + '.nls.' + language.id + '.js', contents: Buffer.from(contents.join('\n'), 'utf-8') })); + }); + }); + Object.keys(statistics).forEach(key => { + let value = statistics[key]; + log(`${key} has ${value} untranslated strings.`); + }); + sortedLanguages.forEach(language => { + let stats = statistics[language.id]; + if (Is.undef(stats)) { + log(`\tNo translations found for language ${language.id}. Using default language instead.`); + } + }); } function processNlsFiles(opts) { - return event_stream_1.through(function (file) { - let fileName = path.basename(file.path); - if (fileName === 'nls.metadata.json') { - let json = null; - if (file.isBuffer()) { - json = JSON.parse(file.contents.toString('utf8')); - } - else { - this.emit('error', `Failed to read component file: ${file.relative}`); - return; - } - if (BundledFormat.is(json)) { - processCoreBundleFormat(opts.fileHeader, opts.languages, json, this); - } - } - this.queue(file); - }); + return event_stream_1.through(function (file) { + let fileName = path.basename(file.path); + if (fileName === 'nls.metadata.json') { + let json = null; + if (file.isBuffer()) { + json = JSON.parse(file.contents.toString('utf8')); + } + else { + this.emit('error', `Failed to read component file: ${file.relative}`); + return; + } + if (BundledFormat.is(json)) { + processCoreBundleFormat(opts.fileHeader, opts.languages, json, this); + } + } + this.queue(file); + }); } exports.processNlsFiles = processNlsFiles; const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench', extensionsProject = 'vscode-extensions', setupProject = 'vscode-setup'; function getResource(sourceFile) { - let resource; - if (/^vs\/platform/.test(sourceFile)) { - return { name: 'vs/platform', project: editorProject }; - } - else if (/^vs\/editor\/contrib/.test(sourceFile)) { - return { name: 'vs/editor/contrib', project: editorProject }; - } - else if (/^vs\/editor/.test(sourceFile)) { - return { name: 'vs/editor', project: editorProject }; - } - else if (/^vs\/base/.test(sourceFile)) { - return { name: 'vs/base', project: editorProject }; - } - else if (/^vs\/code/.test(sourceFile)) { - return { name: 'vs/code', project: workbenchProject }; - } - else if (/^vs\/workbench\/contrib/.test(sourceFile)) { - resource = sourceFile.split('/', 4).join('/'); - return { name: resource, project: workbenchProject }; - } - else if (/^vs\/workbench\/services/.test(sourceFile)) { - resource = sourceFile.split('/', 4).join('/'); - return { name: resource, project: workbenchProject }; - } - else if (/^vs\/workbench/.test(sourceFile)) { - return { name: 'vs/workbench', project: workbenchProject }; - } - throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); + let resource; + if (/^vs\/platform/.test(sourceFile)) { + return { name: 'vs/platform', project: editorProject }; + } + else if (/^vs\/editor\/contrib/.test(sourceFile)) { + return { name: 'vs/editor/contrib', project: editorProject }; + } + else if (/^vs\/editor/.test(sourceFile)) { + return { name: 'vs/editor', project: editorProject }; + } + else if (/^vs\/base/.test(sourceFile)) { + return { name: 'vs/base', project: editorProject }; + } + else if (/^vs\/code/.test(sourceFile)) { + return { name: 'vs/code', project: workbenchProject }; + } + else if (/^vs\/workbench\/contrib/.test(sourceFile)) { + resource = sourceFile.split('/', 4).join('/'); + return { name: resource, project: workbenchProject }; + } + else if (/^vs\/workbench\/services/.test(sourceFile)) { + resource = sourceFile.split('/', 4).join('/'); + return { name: resource, project: workbenchProject }; + } + else if (/^vs\/workbench/.test(sourceFile)) { + return { name: 'vs/workbench', project: workbenchProject }; + } + throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); } exports.getResource = getResource; function createXlfFilesForCoreBundle() { - return event_stream_1.through(function (file) { - const basename = path.basename(file.path); - if (basename === 'nls.metadata.json') { - if (file.isBuffer()) { - const xlfs = Object.create(null); - const json = JSON.parse(file.contents.toString('utf8')); - for (let coreModule in json.keys) { - const projectResource = getResource(coreModule); - const resource = projectResource.name; - const project = projectResource.project; - const keys = json.keys[coreModule]; - const messages = json.messages[coreModule]; - if (keys.length !== messages.length) { - this.emit('error', `There is a mismatch between keys and messages in ${file.relative} for module ${coreModule}`); - return; - } - else { - let xlf = xlfs[resource]; - if (!xlf) { - xlf = new XLF(project); - xlfs[resource] = xlf; - } - xlf.addFile(`src/${coreModule}`, keys, messages); - } - } - for (let resource in xlfs) { - const xlf = xlfs[resource]; - const filePath = `${xlf.project}/${resource.replace(/\//g, '_')}.xlf`; - const xlfFile = new File({ - path: filePath, - contents: Buffer.from(xlf.toString(), 'utf8') - }); - this.queue(xlfFile); - } - } - else { - this.emit('error', new Error(`File ${file.relative} is not using a buffer content`)); - return; - } - } - else { - this.emit('error', new Error(`File ${file.relative} is not a core meta data file.`)); - return; - } - }); + return event_stream_1.through(function (file) { + const basename = path.basename(file.path); + if (basename === 'nls.metadata.json') { + if (file.isBuffer()) { + const xlfs = Object.create(null); + const json = JSON.parse(file.contents.toString('utf8')); + for (let coreModule in json.keys) { + const projectResource = getResource(coreModule); + const resource = projectResource.name; + const project = projectResource.project; + const keys = json.keys[coreModule]; + const messages = json.messages[coreModule]; + if (keys.length !== messages.length) { + this.emit('error', `There is a mismatch between keys and messages in ${file.relative} for module ${coreModule}`); + return; + } + else { + let xlf = xlfs[resource]; + if (!xlf) { + xlf = new XLF(project); + xlfs[resource] = xlf; + } + xlf.addFile(`src/${coreModule}`, keys, messages); + } + } + for (let resource in xlfs) { + const xlf = xlfs[resource]; + const filePath = `${xlf.project}/${resource.replace(/\//g, '_')}.xlf`; + const xlfFile = new File({ + path: filePath, + contents: Buffer.from(xlf.toString(), 'utf8') + }); + this.queue(xlfFile); + } + } + else { + this.emit('error', new Error(`File ${file.relative} is not using a buffer content`)); + return; + } + } + else { + this.emit('error', new Error(`File ${file.relative} is not a core meta data file.`)); + return; + } + }); } exports.createXlfFilesForCoreBundle = createXlfFilesForCoreBundle; function createXlfFilesForExtensions() { - let counter = 0; - let folderStreamEnded = false; - let folderStreamEndEmitted = false; - return event_stream_1.through(function (extensionFolder) { - const folderStream = this; - const stat = fs.statSync(extensionFolder.path); - if (!stat.isDirectory()) { - return; - } - let extensionName = path.basename(extensionFolder.path); - if (extensionName === 'node_modules') { - return; - } - counter++; - let _xlf; - function getXlf() { - if (!_xlf) { - _xlf = new XLF(extensionsProject); - } - return _xlf; - } - gulp.src([`.build/extensions/${extensionName}/package.nls.json`, `.build/extensions/${extensionName}/**/nls.metadata.json`], { allowEmpty: true }).pipe(event_stream_1.through(function (file) { - if (file.isBuffer()) { - const buffer = file.contents; - const basename = path.basename(file.path); - if (basename === 'package.nls.json') { - const json = JSON.parse(buffer.toString('utf8')); - const keys = Object.keys(json); - const messages = keys.map((key) => { - const value = json[key]; - if (Is.string(value)) { - return value; - } - else if (value) { - return value.message; - } - else { - return `Unknown message for key: ${key}`; - } - }); - getXlf().addFile(`extensions/${extensionName}/package`, keys, messages); - } - else if (basename === 'nls.metadata.json') { - const json = JSON.parse(buffer.toString('utf8')); - const relPath = path.relative(`.build/extensions/${extensionName}`, path.dirname(file.path)); - for (let file in json) { - const fileContent = json[file]; - getXlf().addFile(`extensions/${extensionName}/${relPath}/${file}`, fileContent.keys, fileContent.messages); - } - } - else { - this.emit('error', new Error(`${file.path} is not a valid extension nls file`)); - return; - } - } - }, function () { - if (_xlf) { - let xlfFile = new File({ - path: path.join(extensionsProject, extensionName + '.xlf'), - contents: Buffer.from(_xlf.toString(), 'utf8') - }); - folderStream.queue(xlfFile); - } - this.queue(null); - counter--; - if (counter === 0 && folderStreamEnded && !folderStreamEndEmitted) { - folderStreamEndEmitted = true; - folderStream.queue(null); - } - })); - }, function () { - folderStreamEnded = true; - if (counter === 0) { - folderStreamEndEmitted = true; - this.queue(null); - } - }); + let counter = 0; + let folderStreamEnded = false; + let folderStreamEndEmitted = false; + return event_stream_1.through(function (extensionFolder) { + const folderStream = this; + const stat = fs.statSync(extensionFolder.path); + if (!stat.isDirectory()) { + return; + } + let extensionName = path.basename(extensionFolder.path); + if (extensionName === 'node_modules') { + return; + } + counter++; + let _xlf; + function getXlf() { + if (!_xlf) { + _xlf = new XLF(extensionsProject); + } + return _xlf; + } + gulp.src([`.build/extensions/${extensionName}/package.nls.json`, `.build/extensions/${extensionName}/**/nls.metadata.json`], { allowEmpty: true }).pipe(event_stream_1.through(function (file) { + if (file.isBuffer()) { + const buffer = file.contents; + const basename = path.basename(file.path); + if (basename === 'package.nls.json') { + const json = JSON.parse(buffer.toString('utf8')); + const keys = Object.keys(json); + const messages = keys.map((key) => { + const value = json[key]; + if (Is.string(value)) { + return value; + } + else if (value) { + return value.message; + } + else { + return `Unknown message for key: ${key}`; + } + }); + getXlf().addFile(`extensions/${extensionName}/package`, keys, messages); + } + else if (basename === 'nls.metadata.json') { + const json = JSON.parse(buffer.toString('utf8')); + const relPath = path.relative(`.build/extensions/${extensionName}`, path.dirname(file.path)); + for (let file in json) { + const fileContent = json[file]; + getXlf().addFile(`extensions/${extensionName}/${relPath}/${file}`, fileContent.keys, fileContent.messages); + } + } + else { + this.emit('error', new Error(`${file.path} is not a valid extension nls file`)); + return; + } + } + }, function () { + if (_xlf) { + let xlfFile = new File({ + path: path.join(extensionsProject, extensionName + '.xlf'), + contents: Buffer.from(_xlf.toString(), 'utf8') + }); + folderStream.queue(xlfFile); + } + this.queue(null); + counter--; + if (counter === 0 && folderStreamEnded && !folderStreamEndEmitted) { + folderStreamEndEmitted = true; + folderStream.queue(null); + } + })); + }, function () { + folderStreamEnded = true; + if (counter === 0) { + folderStreamEndEmitted = true; + this.queue(null); + } + }); } exports.createXlfFilesForExtensions = createXlfFilesForExtensions; function createXlfFilesForIsl() { - return event_stream_1.through(function (file) { - let projectName, resourceFile; - if (path.basename(file.path) === 'Default.isl') { - projectName = setupProject; - resourceFile = 'setup_default.xlf'; - } - else { - projectName = workbenchProject; - resourceFile = 'setup_messages.xlf'; - } - let xlf = new XLF(projectName), keys = [], messages = []; - let model = new TextModel(file.contents.toString()); - let inMessageSection = false; - model.lines.forEach(line => { - if (line.length === 0) { - return; - } - let firstChar = line.charAt(0); - switch (firstChar) { - case ';': - // Comment line; - return; - case '[': - inMessageSection = '[Messages]' === line || '[CustomMessages]' === line; - return; - } - if (!inMessageSection) { - return; - } - let sections = line.split('='); - if (sections.length !== 2) { - throw new Error(`Badly formatted message found: ${line}`); - } - else { - let key = sections[0]; - let value = sections[1]; - if (key.length > 0 && value.length > 0) { - keys.push(key); - messages.push(value); - } - } - }); - const originalPath = file.path.substring(file.cwd.length + 1, file.path.split('.')[0].length).replace(/\\/g, '/'); - xlf.addFile(originalPath, keys, messages); - // Emit only upon all ISL files combined into single XLF instance - const newFilePath = path.join(projectName, resourceFile); - const xlfFile = new File({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') }); - this.queue(xlfFile); - }); + return event_stream_1.through(function (file) { + let projectName, resourceFile; + if (path.basename(file.path) === 'messages.en.isl') { + projectName = setupProject; + resourceFile = 'messages.xlf'; + } + else { + throw new Error(`Unknown input file ${file.path}`); + } + let xlf = new XLF(projectName), keys = [], messages = []; + let model = new TextModel(file.contents.toString()); + let inMessageSection = false; + model.lines.forEach(line => { + if (line.length === 0) { + return; + } + let firstChar = line.charAt(0); + switch (firstChar) { + case ';': + // Comment line; + return; + case '[': + inMessageSection = '[Messages]' === line || '[CustomMessages]' === line; + return; + } + if (!inMessageSection) { + return; + } + let sections = line.split('='); + if (sections.length !== 2) { + throw new Error(`Badly formatted message found: ${line}`); + } + else { + let key = sections[0]; + let value = sections[1]; + if (key.length > 0 && value.length > 0) { + keys.push(key); + messages.push(value); + } + } + }); + const originalPath = file.path.substring(file.cwd.length + 1, file.path.split('.')[0].length).replace(/\\/g, '/'); + xlf.addFile(originalPath, keys, messages); + // Emit only upon all ISL files combined into single XLF instance + const newFilePath = path.join(projectName, resourceFile); + const xlfFile = new File({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') }); + this.queue(xlfFile); + }); } exports.createXlfFilesForIsl = createXlfFilesForIsl; function pushXlfFiles(apiHostname, username, password) { - let tryGetPromises = []; - let updateCreatePromises = []; - return event_stream_1.through(function (file) { - const project = path.dirname(file.relative); - const fileName = path.basename(file.path); - const slug = fileName.substr(0, fileName.length - '.xlf'.length); - const credentials = `${username}:${password}`; - // Check if resource already exists, if not, then create it. - let promise = tryGetResource(project, slug, apiHostname, credentials); - tryGetPromises.push(promise); - promise.then(exists => { - if (exists) { - promise = updateResource(project, slug, file, apiHostname, credentials); - } - else { - promise = createResource(project, slug, file, apiHostname, credentials); - } - updateCreatePromises.push(promise); - }); - }, function () { - // End the pipe only after all the communication with Transifex API happened - Promise.all(tryGetPromises).then(() => { - Promise.all(updateCreatePromises).then(() => { - this.queue(null); - }).catch((reason) => { throw new Error(reason); }); - }).catch((reason) => { throw new Error(reason); }); - }); + let tryGetPromises = []; + let updateCreatePromises = []; + return event_stream_1.through(function (file) { + const project = path.dirname(file.relative); + const fileName = path.basename(file.path); + const slug = fileName.substr(0, fileName.length - '.xlf'.length); + const credentials = `${username}:${password}`; + // Check if resource already exists, if not, then create it. + let promise = tryGetResource(project, slug, apiHostname, credentials); + tryGetPromises.push(promise); + promise.then(exists => { + if (exists) { + promise = updateResource(project, slug, file, apiHostname, credentials); + } + else { + promise = createResource(project, slug, file, apiHostname, credentials); + } + updateCreatePromises.push(promise); + }); + }, function () { + // End the pipe only after all the communication with Transifex API happened + Promise.all(tryGetPromises).then(() => { + Promise.all(updateCreatePromises).then(() => { + this.queue(null); + }).catch((reason) => { throw new Error(reason); }); + }).catch((reason) => { throw new Error(reason); }); + }); } exports.pushXlfFiles = pushXlfFiles; function getAllResources(project, apiHostname, username, password) { - return new Promise((resolve, reject) => { - const credentials = `${username}:${password}`; - const options = { - hostname: apiHostname, - path: `/api/2/project/${project}/resources`, - auth: credentials, - method: 'GET' - }; - const request = https.request(options, (res) => { - let buffer = []; - res.on('data', (chunk) => buffer.push(chunk)); - res.on('end', () => { - if (res.statusCode === 200) { - let json = JSON.parse(Buffer.concat(buffer).toString()); - if (Array.isArray(json)) { - resolve(json.map(o => o.slug)); - return; - } - reject(`Unexpected data format. Response code: ${res.statusCode}.`); - } - else { - reject(`No resources in ${project} returned no data. Response code: ${res.statusCode}.`); - } - }); - }); - request.on('error', (err) => { - reject(`Failed to query resources in ${project} with the following error: ${err}. ${options.path}`); - }); - request.end(); - }); + return new Promise((resolve, reject) => { + const credentials = `${username}:${password}`; + const options = { + hostname: apiHostname, + path: `/api/2/project/${project}/resources`, + auth: credentials, + method: 'GET' + }; + const request = https.request(options, (res) => { + let buffer = []; + res.on('data', (chunk) => buffer.push(chunk)); + res.on('end', () => { + if (res.statusCode === 200) { + let json = JSON.parse(Buffer.concat(buffer).toString()); + if (Array.isArray(json)) { + resolve(json.map(o => o.slug)); + return; + } + reject(`Unexpected data format. Response code: ${res.statusCode}.`); + } + else { + reject(`No resources in ${project} returned no data. Response code: ${res.statusCode}.`); + } + }); + }); + request.on('error', (err) => { + reject(`Failed to query resources in ${project} with the following error: ${err}. ${options.path}`); + }); + request.end(); + }); } function findObsoleteResources(apiHostname, username, password) { - let resourcesByProject = Object.create(null); - resourcesByProject[extensionsProject] = [].concat(exports.externalExtensionsWithTranslations); // clone - return event_stream_1.through(function (file) { - const project = path.dirname(file.relative); - const fileName = path.basename(file.path); - const slug = fileName.substr(0, fileName.length - '.xlf'.length); - let slugs = resourcesByProject[project]; - if (!slugs) { - resourcesByProject[project] = slugs = []; - } - slugs.push(slug); - this.push(file); - }, function () { - const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - let i18Resources = [...json.editor, ...json.workbench].map((r) => r.project + '/' + r.name.replace(/\//g, '_')); - let extractedResources = []; - for (let project of [workbenchProject, editorProject]) { - for (let resource of resourcesByProject[project]) { - if (resource !== 'setup_messages') { - extractedResources.push(project + '/' + resource); - } - } - } - if (i18Resources.length !== extractedResources.length) { - console.log(`[i18n] Obsolete resources in file 'build/lib/i18n.resources.json': JSON.stringify(${i18Resources.filter(p => extractedResources.indexOf(p) === -1)})`); - console.log(`[i18n] Missing resources in file 'build/lib/i18n.resources.json': JSON.stringify(${extractedResources.filter(p => i18Resources.indexOf(p) === -1)})`); - } - let promises = []; - for (let project in resourcesByProject) { - promises.push(getAllResources(project, apiHostname, username, password).then(resources => { - let expectedResources = resourcesByProject[project]; - let unusedResources = resources.filter(resource => resource && expectedResources.indexOf(resource) === -1); - if (unusedResources.length) { - console.log(`[transifex] Obsolete resources in project '${project}': ${unusedResources.join(', ')}`); - } - })); - } - return Promise.all(promises).then(_ => { - this.push(null); - }).catch((reason) => { throw new Error(reason); }); - }); + let resourcesByProject = Object.create(null); + resourcesByProject[extensionsProject] = [].concat(exports.externalExtensionsWithTranslations); // clone + return event_stream_1.through(function (file) { + const project = path.dirname(file.relative); + const fileName = path.basename(file.path); + const slug = fileName.substr(0, fileName.length - '.xlf'.length); + let slugs = resourcesByProject[project]; + if (!slugs) { + resourcesByProject[project] = slugs = []; + } + slugs.push(slug); + this.push(file); + }, function () { + const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); + let i18Resources = [...json.editor, ...json.workbench].map((r) => r.project + '/' + r.name.replace(/\//g, '_')); + let extractedResources = []; + for (let project of [workbenchProject, editorProject]) { + for (let resource of resourcesByProject[project]) { + if (resource !== 'setup_messages') { + extractedResources.push(project + '/' + resource); + } + } + } + if (i18Resources.length !== extractedResources.length) { + console.log(`[i18n] Obsolete resources in file 'build/lib/i18n.resources.json': JSON.stringify(${i18Resources.filter(p => extractedResources.indexOf(p) === -1)})`); + console.log(`[i18n] Missing resources in file 'build/lib/i18n.resources.json': JSON.stringify(${extractedResources.filter(p => i18Resources.indexOf(p) === -1)})`); + } + let promises = []; + for (let project in resourcesByProject) { + promises.push(getAllResources(project, apiHostname, username, password).then(resources => { + let expectedResources = resourcesByProject[project]; + let unusedResources = resources.filter(resource => resource && expectedResources.indexOf(resource) === -1); + if (unusedResources.length) { + console.log(`[transifex] Obsolete resources in project '${project}': ${unusedResources.join(', ')}`); + } + })); + } + return Promise.all(promises).then(_ => { + this.push(null); + }).catch((reason) => { throw new Error(reason); }); + }); } exports.findObsoleteResources = findObsoleteResources; function tryGetResource(project, slug, apiHostname, credentials) { - return new Promise((resolve, reject) => { - const options = { - hostname: apiHostname, - path: `/api/2/project/${project}/resource/${slug}/?details`, - auth: credentials, - method: 'GET' - }; - const request = https.request(options, (response) => { - if (response.statusCode === 404) { - resolve(false); - } - else if (response.statusCode === 200) { - resolve(true); - } - else { - reject(`Failed to query resource ${project}/${slug}. Response: ${response.statusCode} ${response.statusMessage}`); - } - }); - request.on('error', (err) => { - reject(`Failed to get ${project}/${slug} on Transifex: ${err}`); - }); - request.end(); - }); + return new Promise((resolve, reject) => { + const options = { + hostname: apiHostname, + path: `/api/2/project/${project}/resource/${slug}/?details`, + auth: credentials, + method: 'GET' + }; + const request = https.request(options, (response) => { + if (response.statusCode === 404) { + resolve(false); + } + else if (response.statusCode === 200) { + resolve(true); + } + else { + reject(`Failed to query resource ${project}/${slug}. Response: ${response.statusCode} ${response.statusMessage}`); + } + }); + request.on('error', (err) => { + reject(`Failed to get ${project}/${slug} on Transifex: ${err}`); + }); + request.end(); + }); } function createResource(project, slug, xlfFile, apiHostname, credentials) { - return new Promise((_resolve, reject) => { - const data = JSON.stringify({ - 'content': xlfFile.contents.toString(), - 'name': slug, - 'slug': slug, - 'i18n_type': 'XLIFF' - }); - const options = { - hostname: apiHostname, - path: `/api/2/project/${project}/resources`, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(data) - }, - auth: credentials, - method: 'POST' - }; - let request = https.request(options, (res) => { - if (res.statusCode === 201) { - log(`Resource ${project}/${slug} successfully created on Transifex.`); - } - else { - reject(`Something went wrong in the request creating ${slug} in ${project}. ${res.statusCode}`); - } - }); - request.on('error', (err) => { - reject(`Failed to create ${project}/${slug} on Transifex: ${err}`); - }); - request.write(data); - request.end(); - }); + return new Promise((_resolve, reject) => { + const data = JSON.stringify({ + 'content': xlfFile.contents.toString(), + 'name': slug, + 'slug': slug, + 'i18n_type': 'XLIFF' + }); + const options = { + hostname: apiHostname, + path: `/api/2/project/${project}/resources`, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + }, + auth: credentials, + method: 'POST' + }; + let request = https.request(options, (res) => { + if (res.statusCode === 201) { + log(`Resource ${project}/${slug} successfully created on Transifex.`); + } + else { + reject(`Something went wrong in the request creating ${slug} in ${project}. ${res.statusCode}`); + } + }); + request.on('error', (err) => { + reject(`Failed to create ${project}/${slug} on Transifex: ${err}`); + }); + request.write(data); + request.end(); + }); } /** * The following link provides information about how Transifex handles updates of a resource file: * https://dev.befoolish.co/tx-docs/public/projects/updating-content#what-happens-when-you-update-files */ function updateResource(project, slug, xlfFile, apiHostname, credentials) { - return new Promise((resolve, reject) => { - const data = JSON.stringify({ content: xlfFile.contents.toString() }); - const options = { - hostname: apiHostname, - path: `/api/2/project/${project}/resource/${slug}/content`, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(data) - }, - auth: credentials, - method: 'PUT' - }; - let request = https.request(options, (res) => { - if (res.statusCode === 200) { - res.setEncoding('utf8'); - let responseBuffer = ''; - res.on('data', function (chunk) { - responseBuffer += chunk; - }); - res.on('end', () => { - const response = JSON.parse(responseBuffer); - log(`Resource ${project}/${slug} successfully updated on Transifex. Strings added: ${response.strings_added}, updated: ${response.strings_added}, deleted: ${response.strings_added}`); - resolve(); - }); - } - else { - reject(`Something went wrong in the request updating ${slug} in ${project}. ${res.statusCode}`); - } - }); - request.on('error', (err) => { - reject(`Failed to update ${project}/${slug} on Transifex: ${err}`); - }); - request.write(data); - request.end(); - }); + return new Promise((resolve, reject) => { + const data = JSON.stringify({ content: xlfFile.contents.toString() }); + const options = { + hostname: apiHostname, + path: `/api/2/project/${project}/resource/${slug}/content`, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + }, + auth: credentials, + method: 'PUT' + }; + let request = https.request(options, (res) => { + if (res.statusCode === 200) { + res.setEncoding('utf8'); + let responseBuffer = ''; + res.on('data', function (chunk) { + responseBuffer += chunk; + }); + res.on('end', () => { + const response = JSON.parse(responseBuffer); + log(`Resource ${project}/${slug} successfully updated on Transifex. Strings added: ${response.strings_added}, updated: ${response.strings_added}, deleted: ${response.strings_added}`); + resolve(); + }); + } + else { + reject(`Something went wrong in the request updating ${slug} in ${project}. ${res.statusCode}`); + } + }); + request.on('error', (err) => { + reject(`Failed to update ${project}/${slug} on Transifex: ${err}`); + }); + request.write(data); + request.end(); + }); } -// cache resources -let _coreAndExtensionResources; -function pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, externalExtensions) { - if (!_coreAndExtensionResources) { - _coreAndExtensionResources = []; - // editor and workbench - const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - _coreAndExtensionResources.push(...json.editor); - _coreAndExtensionResources.push(...json.workbench); - // extensions - let extensionsToLocalize = Object.create(null); - glob.sync('.build/extensions/**/*.nls.json').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - glob.sync('.build/extensions/*/node_modules/vscode-nls').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - Object.keys(extensionsToLocalize).forEach(extension => { - _coreAndExtensionResources.push({ name: extension, project: extensionsProject }); - }); - if (externalExtensions) { - for (let resourceName in externalExtensions) { - _coreAndExtensionResources.push({ name: resourceName, project: extensionsProject }); - } - } - } - return pullXlfFiles(apiHostname, username, password, language, _coreAndExtensionResources); -} -exports.pullCoreAndExtensionsXlfFiles = pullCoreAndExtensionsXlfFiles; function pullSetupXlfFiles(apiHostname, username, password, language, includeDefault) { - let setupResources = [{ name: 'setup_messages', project: workbenchProject }]; - if (includeDefault) { - setupResources.push({ name: 'setup_default', project: setupProject }); - } - return pullXlfFiles(apiHostname, username, password, language, setupResources); + let setupResources = [{ name: 'setup_messages', project: workbenchProject }]; + if (includeDefault) { + setupResources.push({ name: 'setup_default', project: setupProject }); + } + return pullXlfFiles(apiHostname, username, password, language, setupResources); } exports.pullSetupXlfFiles = pullSetupXlfFiles; function pullXlfFiles(apiHostname, username, password, language, resources) { - const credentials = `${username}:${password}`; - let expectedTranslationsCount = resources.length; - let translationsRetrieved = 0, called = false; - return event_stream_1.readable(function (_count, callback) { - // Mark end of stream when all resources were retrieved - if (translationsRetrieved === expectedTranslationsCount) { - return this.emit('end'); - } - if (!called) { - called = true; - const stream = this; - resources.map(function (resource) { - retrieveResource(language, resource, apiHostname, credentials).then((file) => { - if (file) { - stream.emit('data', file); - } - translationsRetrieved++; - }).catch(error => { throw new Error(error); }); - }); - } - callback(); - }); + const credentials = `${username}:${password}`; + let expectedTranslationsCount = resources.length; + let translationsRetrieved = 0, called = false; + return event_stream_1.readable(function (_count, callback) { + // Mark end of stream when all resources were retrieved + if (translationsRetrieved === expectedTranslationsCount) { + return this.emit('end'); + } + if (!called) { + called = true; + const stream = this; + resources.map(function (resource) { + retrieveResource(language, resource, apiHostname, credentials).then((file) => { + if (file) { + stream.emit('data', file); + } + translationsRetrieved++; + }).catch(error => { throw new Error(error); }); + }); + } + callback(); + }); } const limiter = new Limiter(NUMBER_OF_CONCURRENT_DOWNLOADS); function retrieveResource(language, resource, apiHostname, credentials) { - return limiter.queue(() => new Promise((resolve, reject) => { - const slug = resource.name.replace(/\//g, '_'); - const project = resource.project; - let transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id; - const options = { - hostname: apiHostname, - path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`, - auth: credentials, - port: 443, - method: 'GET' - }; - console.log('[transifex] Fetching ' + options.path); - let request = https.request(options, (res) => { - let xlfBuffer = []; - res.on('data', (chunk) => xlfBuffer.push(chunk)); - res.on('end', () => { - if (res.statusCode === 200) { - resolve(new File({ contents: Buffer.concat(xlfBuffer), path: `${project}/${slug}.xlf` })); - } - else if (res.statusCode === 404) { - console.log(`[transifex] ${slug} in ${project} returned no data.`); - resolve(null); - } - else { - reject(`${slug} in ${project} returned no data. Response code: ${res.statusCode}.`); - } - }); - }); - request.on('error', (err) => { - reject(`Failed to query resource ${slug} with the following error: ${err}. ${options.path}`); - }); - request.end(); - })); + return limiter.queue(() => new Promise((resolve, reject) => { + const slug = resource.name.replace(/\//g, '_'); + const project = resource.project; + let transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id; + const options = { + hostname: apiHostname, + path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`, + auth: credentials, + port: 443, + method: 'GET' + }; + console.log('[transifex] Fetching ' + options.path); + let request = https.request(options, (res) => { + let xlfBuffer = []; + res.on('data', (chunk) => xlfBuffer.push(chunk)); + res.on('end', () => { + if (res.statusCode === 200) { + resolve(new File({ contents: Buffer.concat(xlfBuffer), path: `${project}/${slug}.xlf` })); + } + else if (res.statusCode === 404) { + console.log(`[transifex] ${slug} in ${project} returned no data.`); + resolve(null); + } + else { + reject(`${slug} in ${project} returned no data. Response code: ${res.statusCode}.`); + } + }); + }); + request.on('error', (err) => { + reject(`Failed to query resource ${slug} with the following error: ${err}. ${options.path}`); + }); + request.end(); + })); } function prepareI18nFiles() { - let parsePromises = []; - return event_stream_1.through(function (xlf) { - let stream = this; - let parsePromise = XLF.parse(xlf.contents.toString()); - parsePromises.push(parsePromise); - parsePromise.then(resolvedFiles => { - resolvedFiles.forEach(file => { - let translatedFile = createI18nFile(file.originalFilePath, file.messages); - stream.queue(translatedFile); - }); - }); - }, function () { - Promise.all(parsePromises) - .then(() => { this.queue(null); }) - .catch(reason => { throw new Error(reason); }); - }); + let parsePromises = []; + return event_stream_1.through(function (xlf) { + let stream = this; + let parsePromise = XLF.parse(xlf.contents.toString()); + parsePromises.push(parsePromise); + parsePromise.then(resolvedFiles => { + resolvedFiles.forEach(file => { + let translatedFile = createI18nFile(file.originalFilePath, file.messages); + stream.queue(translatedFile); + }); + }); + }, function () { + Promise.all(parsePromises) + .then(() => { this.queue(null); }) + .catch(reason => { throw new Error(reason); }); + }); } exports.prepareI18nFiles = prepareI18nFiles; function createI18nFile(originalFilePath, messages) { - let result = Object.create(null); - result[''] = [ - '--------------------------------------------------------------------------------------------', - 'Copyright (c) Microsoft Corporation. All rights reserved.', - 'Licensed under the MIT License. See License.txt in the project root for license information.', - '--------------------------------------------------------------------------------------------', - 'Do not edit this file. It is machine generated.' - ]; - for (let key of Object.keys(messages)) { - result[key] = messages[key]; - } - let content = JSON.stringify(result, null, '\t'); - if (process.platform === 'win32') { - content = content.replace(/\n/g, '\r\n'); - } - return new File({ - path: path.join(originalFilePath + '.i18n.json'), - contents: Buffer.from(content, 'utf8') - }); + let result = Object.create(null); + result[''] = [ + '--------------------------------------------------------------------------------------------', + 'Copyright (c) Microsoft Corporation. All rights reserved.', + 'Licensed under the MIT License. See License.txt in the project root for license information.', + '--------------------------------------------------------------------------------------------', + 'Do not edit this file. It is machine generated.' + ]; + for (let key of Object.keys(messages)) { + result[key] = messages[key]; + } + let content = JSON.stringify(result, null, '\t'); + if (process.platform === 'win32') { + content = content.replace(/\n/g, '\r\n'); + } + return new File({ + path: path.join(originalFilePath + '.i18n.json'), + contents: Buffer.from(content, 'utf8') + }); } const i18nPackVersion = '1.0.0'; -function pullI18nPackFiles(apiHostname, username, password, language, resultingTranslationPaths) { - return pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, exports.externalExtensionsWithTranslations) - .pipe(prepareI18nPackFiles(exports.externalExtensionsWithTranslations, resultingTranslationPaths, language.id === 'ps')); -} -exports.pullI18nPackFiles = pullI18nPackFiles; function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pseudo = false) { - let parsePromises = []; - let mainPack = { version: i18nPackVersion, contents: {} }; - let extensionsPacks = {}; - let errors = []; - return event_stream_1.through(function (xlf) { - let project = path.basename(path.dirname(xlf.relative)); - let resource = path.basename(xlf.relative, '.xlf'); - let contents = xlf.contents.toString(); - let parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents); - parsePromises.push(parsePromise); - parsePromise.then(resolvedFiles => { - resolvedFiles.forEach(file => { - const path = file.originalFilePath; - const firstSlash = path.indexOf('/'); - if (project === extensionsProject) { - let extPack = extensionsPacks[resource]; - if (!extPack) { - extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} }; - } - const externalId = externalExtensions[resource]; - if (!externalId) { // internal extension: remove 'extensions/extensionId/' segnent - const secondSlash = path.indexOf('/', firstSlash + 1); - extPack.contents[path.substr(secondSlash + 1)] = file.messages; - } - else { - extPack.contents[path] = file.messages; - } - } - else { - mainPack.contents[path.substr(firstSlash + 1)] = file.messages; - } - }); - }).catch(reason => { - errors.push(reason); - }); - }, function () { - Promise.all(parsePromises) - .then(() => { - if (errors.length > 0) { - throw errors; - } - const translatedMainFile = createI18nFile('./main', mainPack); - resultingTranslationPaths.push({ id: 'vscode', resourceName: 'main.i18n.json' }); - this.queue(translatedMainFile); - for (let extension in extensionsPacks) { - const translatedExtFile = createI18nFile(`extensions/${extension}`, extensionsPacks[extension]); - this.queue(translatedExtFile); - const externalExtensionId = externalExtensions[extension]; - if (externalExtensionId) { - resultingTranslationPaths.push({ id: externalExtensionId, resourceName: `extensions/${extension}.i18n.json` }); - } - else { - resultingTranslationPaths.push({ id: `vscode.${extension}`, resourceName: `extensions/${extension}.i18n.json` }); - } - } - this.queue(null); - }) - .catch((reason) => { - this.emit('error', reason); - }); - }); + let parsePromises = []; + let mainPack = { version: i18nPackVersion, contents: {} }; + let extensionsPacks = {}; + let errors = []; + return event_stream_1.through(function (xlf) { + let project = path.basename(path.dirname(path.dirname(xlf.relative))); + let resource = path.basename(xlf.relative, '.xlf'); + let contents = xlf.contents.toString(); + log(`Found ${project}: ${resource}`); + let parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents); + parsePromises.push(parsePromise); + parsePromise.then(resolvedFiles => { + resolvedFiles.forEach(file => { + const path = file.originalFilePath; + const firstSlash = path.indexOf('/'); + if (project === extensionsProject) { + let extPack = extensionsPacks[resource]; + if (!extPack) { + extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} }; + } + const externalId = externalExtensions[resource]; + if (!externalId) { // internal extension: remove 'extensions/extensionId/' segnent + const secondSlash = path.indexOf('/', firstSlash + 1); + extPack.contents[path.substr(secondSlash + 1)] = file.messages; + } + else { + extPack.contents[path] = file.messages; + } + } + else { + mainPack.contents[path.substr(firstSlash + 1)] = file.messages; + } + }); + }).catch(reason => { + errors.push(reason); + }); + }, function () { + Promise.all(parsePromises) + .then(() => { + if (errors.length > 0) { + throw errors; + } + const translatedMainFile = createI18nFile('./main', mainPack); + resultingTranslationPaths.push({ id: 'vscode', resourceName: 'main.i18n.json' }); + this.queue(translatedMainFile); + for (let extension in extensionsPacks) { + const translatedExtFile = createI18nFile(`extensions/${extension}`, extensionsPacks[extension]); + this.queue(translatedExtFile); + const externalExtensionId = externalExtensions[extension]; + if (externalExtensionId) { + resultingTranslationPaths.push({ id: externalExtensionId, resourceName: `extensions/${extension}.i18n.json` }); + } + else { + resultingTranslationPaths.push({ id: `vscode.${extension}`, resourceName: `extensions/${extension}.i18n.json` }); + } + } + this.queue(null); + }) + .catch((reason) => { + this.emit('error', reason); + }); + }); } exports.prepareI18nPackFiles = prepareI18nPackFiles; function prepareIslFiles(language, innoSetupConfig) { - let parsePromises = []; - return event_stream_1.through(function (xlf) { - let stream = this; - let parsePromise = XLF.parse(xlf.contents.toString()); - parsePromises.push(parsePromise); - parsePromise.then(resolvedFiles => { - resolvedFiles.forEach(file => { - if (path.basename(file.originalFilePath) === 'Default' && !innoSetupConfig.defaultInfo) { - return; - } - let translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig); - stream.queue(translatedFile); - }); - }).catch(reason => { - this.emit('error', reason); - }); - }, function () { - Promise.all(parsePromises) - .then(() => { this.queue(null); }) - .catch(reason => { - this.emit('error', reason); - }); - }); + let parsePromises = []; + return event_stream_1.through(function (xlf) { + let stream = this; + let parsePromise = XLF.parse(xlf.contents.toString()); + parsePromises.push(parsePromise); + parsePromise.then(resolvedFiles => { + resolvedFiles.forEach(file => { + let translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig); + stream.queue(translatedFile); + }); + }).catch(reason => { + this.emit('error', reason); + }); + }, function () { + Promise.all(parsePromises) + .then(() => { this.queue(null); }) + .catch(reason => { + this.emit('error', reason); + }); + }); } exports.prepareIslFiles = prepareIslFiles; function createIslFile(originalFilePath, messages, language, innoSetup) { - let content = []; - let originalContent; - if (path.basename(originalFilePath) === 'Default') { - originalContent = new TextModel(fs.readFileSync(originalFilePath + '.isl', 'utf8')); - } - else { - originalContent = new TextModel(fs.readFileSync(originalFilePath + '.en.isl', 'utf8')); - } - originalContent.lines.forEach(line => { - if (line.length > 0) { - let firstChar = line.charAt(0); - if (firstChar === '[' || firstChar === ';') { - content.push(line); - } - else { - let sections = line.split('='); - let key = sections[0]; - let translated = line; - if (key) { - if (key === 'LanguageName') { - translated = `${key}=${innoSetup.defaultInfo.name}`; - } - else if (key === 'LanguageID') { - translated = `${key}=${innoSetup.defaultInfo.id}`; - } - else if (key === 'LanguageCodePage') { - translated = `${key}=${innoSetup.codePage.substr(2)}`; - } - else { - let translatedMessage = messages[key]; - if (translatedMessage) { - translated = `${key}=${translatedMessage}`; - } - } - } - content.push(translated); - } - } - }); - const basename = path.basename(originalFilePath); - const filePath = `${basename}.${language.id}.isl`; - const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); - return new File({ - path: filePath, - contents: Buffer.from(encoded), - }); + let content = []; + let originalContent; + if (path.basename(originalFilePath) === 'Default') { + originalContent = new TextModel(fs.readFileSync(originalFilePath + '.isl', 'utf8')); + } + else { + originalContent = new TextModel(fs.readFileSync(originalFilePath + '.en.isl', 'utf8')); + } + originalContent.lines.forEach(line => { + if (line.length > 0) { + let firstChar = line.charAt(0); + if (firstChar === '[' || firstChar === ';') { + content.push(line); + } + else { + let sections = line.split('='); + let key = sections[0]; + let translated = line; + if (key) { + let translatedMessage = messages[key]; + if (translatedMessage) { + translated = `${key}=${translatedMessage}`; + } + } + content.push(translated); + } + } + }); + const basename = path.basename(originalFilePath); + const filePath = `${basename}.${language.id}.isl`; + const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); + return new File({ + path: filePath, + contents: Buffer.from(encoded), + }); } function encodeEntities(value) { - let result = []; - for (let i = 0; i < value.length; i++) { - let ch = value[i]; - switch (ch) { - case '<': - result.push('<'); - break; - case '>': - result.push('>'); - break; - case '&': - result.push('&'); - break; - default: - result.push(ch); - } - } - return result.join(''); + let result = []; + for (let i = 0; i < value.length; i++) { + let ch = value[i]; + switch (ch) { + case '<': + result.push('<'); + break; + case '>': + result.push('>'); + break; + case '&': + result.push('&'); + break; + default: + result.push(ch); + } + } + return result.join(''); } function decodeEntities(value) { - return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); + return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); } function pseudify(message) { - return '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; + return '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; } diff --git a/lib/vscode/build/lib/i18n.ts b/lib/vscode/build/lib/i18n.ts index 746c481b262d..5e00e9d6c6e6 100644 --- a/lib/vscode/build/lib/i18n.ts +++ b/lib/vscode/build/lib/i18n.ts @@ -10,7 +10,6 @@ import { through, readable, ThroughStream } from 'event-stream'; import * as File from 'vinyl'; import * as Is from 'is'; import * as xml2js from 'xml2js'; -import * as glob from 'glob'; import * as https from 'https'; import * as gulp from 'gulp'; import * as fancyLog from 'fancy-log'; @@ -31,10 +30,6 @@ export interface Language { export interface InnoSetup { codePage: string; //code page for encoding (http://www.jrsoftware.org/ishelp/index.php?topic=langoptionssection) - defaultInfo?: { - name: string; // inno setup language name - id: string; // locale identifier (https://msdn.microsoft.com/en-us/library/dd318693.aspx) - }; } export const defaultLanguages: Language[] = [ @@ -198,14 +193,17 @@ export class XLF { public toString(): string { this.appendHeader(); - for (let file in this.files) { + const files = Object.keys(this.files).sort(); + for (const file of files) { this.appendNewLine(``, 2); - for (let item of this.files[file]) { + const items = this.files[file].sort((a: Item, b: Item) => { + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; + }); + for (const item of items) { this.addStringItem(file, item); } - this.appendNewLine('', 2); + this.appendNewLine(''); } - this.appendFooter(); return this.buffer.join('\r\n'); } @@ -763,12 +761,11 @@ export function createXlfFilesForIsl(): ThroughStream { return through(function (this: ThroughStream, file: File) { let projectName: string, resourceFile: string; - if (path.basename(file.path) === 'Default.isl') { + if (path.basename(file.path) === 'messages.en.isl') { projectName = setupProject; - resourceFile = 'setup_default.xlf'; + resourceFile = 'messages.xlf'; } else { - projectName = workbenchProject; - resourceFile = 'setup_messages.xlf'; + throw new Error(`Unknown input file ${file.path}`); } let xlf = new XLF(projectName), @@ -1036,35 +1033,6 @@ function updateResource(project: string, slug: string, xlfFile: File, apiHostnam }); } -// cache resources -let _coreAndExtensionResources: Resource[]; - -export function pullCoreAndExtensionsXlfFiles(apiHostname: string, username: string, password: string, language: Language, externalExtensions?: Map): NodeJS.ReadableStream { - if (!_coreAndExtensionResources) { - _coreAndExtensionResources = []; - // editor and workbench - const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - _coreAndExtensionResources.push(...json.editor); - _coreAndExtensionResources.push(...json.workbench); - - // extensions - let extensionsToLocalize = Object.create(null); - glob.sync('.build/extensions/**/*.nls.json').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - glob.sync('.build/extensions/*/node_modules/vscode-nls').forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); - - Object.keys(extensionsToLocalize).forEach(extension => { - _coreAndExtensionResources.push({ name: extension, project: extensionsProject }); - }); - - if (externalExtensions) { - for (let resourceName in externalExtensions) { - _coreAndExtensionResources.push({ name: resourceName, project: extensionsProject }); - } - } - } - return pullXlfFiles(apiHostname, username, password, language, _coreAndExtensionResources); -} - export function pullSetupXlfFiles(apiHostname: string, username: string, password: string, language: Language, includeDefault: boolean): NodeJS.ReadableStream { let setupResources = [{ name: 'setup_messages', project: workbenchProject }]; if (includeDefault) { @@ -1196,20 +1164,16 @@ export interface TranslationPath { resourceName: string; } -export function pullI18nPackFiles(apiHostname: string, username: string, password: string, language: Language, resultingTranslationPaths: TranslationPath[]): NodeJS.ReadableStream { - return pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, externalExtensionsWithTranslations) - .pipe(prepareI18nPackFiles(externalExtensionsWithTranslations, resultingTranslationPaths, language.id === 'ps')); -} - export function prepareI18nPackFiles(externalExtensions: Map, resultingTranslationPaths: TranslationPath[], pseudo = false): NodeJS.ReadWriteStream { let parsePromises: Promise[] = []; let mainPack: I18nPack = { version: i18nPackVersion, contents: {} }; let extensionsPacks: Map = {}; let errors: any[] = []; return through(function (this: ThroughStream, xlf: File) { - let project = path.basename(path.dirname(xlf.relative)); + let project = path.basename(path.dirname(path.dirname(xlf.relative))); let resource = path.basename(xlf.relative, '.xlf'); let contents = xlf.contents.toString(); + log(`Found ${project}: ${resource}`); let parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents); parsePromises.push(parsePromise); parsePromise.then( @@ -1278,9 +1242,6 @@ export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup): parsePromise.then( resolvedFiles => { resolvedFiles.forEach(file => { - if (path.basename(file.originalFilePath) === 'Default' && !innoSetupConfig.defaultInfo) { - return; - } let translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig); stream.queue(translatedFile); }); @@ -1315,17 +1276,9 @@ function createIslFile(originalFilePath: string, messages: Map, language let key = sections[0]; let translated = line; if (key) { - if (key === 'LanguageName') { - translated = `${key}=${innoSetup.defaultInfo!.name}`; - } else if (key === 'LanguageID') { - translated = `${key}=${innoSetup.defaultInfo!.id}`; - } else if (key === 'LanguageCodePage') { - translated = `${key}=${innoSetup.codePage.substr(2)}`; - } else { - let translatedMessage = messages[key]; - if (translatedMessage) { - translated = `${key}=${translatedMessage}`; - } + let translatedMessage = messages[key]; + if (translatedMessage) { + translated = `${key}=${translatedMessage}`; } } diff --git a/lib/vscode/build/lib/node.js b/lib/vscode/build/lib/node.js index e727aff83239..6e735f2cdb29 100644 --- a/lib/vscode/build/lib/node.js +++ b/lib/vscode/build/lib/node.js @@ -8,8 +8,8 @@ const path = require("path"); const fs = require("fs"); const root = path.dirname(path.dirname(__dirname)); const yarnrcPath = path.join(root, 'remote', '.yarnrc'); -const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); -const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1]; +// NOTE@coder: Fix version due to .yarnrc removal. +const version = process.versions.node; const platform = process.platform; const arch = platform === 'darwin' ? 'x64' : process.arch; const node = platform === 'win32' ? 'node.exe' : 'node'; diff --git a/lib/vscode/build/lib/node.ts b/lib/vscode/build/lib/node.ts index 20389c69445b..96fa624ad30a 100644 --- a/lib/vscode/build/lib/node.ts +++ b/lib/vscode/build/lib/node.ts @@ -6,9 +6,10 @@ import * as path from 'path'; const root = path.dirname(path.dirname(__dirname)); + +// NOTE@coder: Fix version due to .yarnrc removal. const version = process.versions.node; const platform = process.platform; - const arch = platform === 'darwin' ? 'x64' : process.arch; const node = platform === 'win32' ? 'node.exe' : 'node'; diff --git a/lib/vscode/build/lib/optimize.js b/lib/vscode/build/lib/optimize.js index 1cfca357f7c8..762a1ac2b4f1 100644 --- a/lib/vscode/build/lib/optimize.js +++ b/lib/vscode/build/lib/optimize.js @@ -177,7 +177,7 @@ function minifyTask(src, sourceMapBaseUrl) { sourcemap: 'external', outdir: '.', platform: 'node', - target: ['node12.18'], + target: ['node14.16'], write: false }).then(res => { const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path)); diff --git a/lib/vscode/build/lib/optimize.ts b/lib/vscode/build/lib/optimize.ts index c3c29e4a7004..b613416cab2f 100644 --- a/lib/vscode/build/lib/optimize.ts +++ b/lib/vscode/build/lib/optimize.ts @@ -254,7 +254,7 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => sourcemap: 'external', outdir: '.', platform: 'node', - target: ['node12.18'], + target: ['node14.16'], write: false }).then(res => { const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path))!; diff --git a/lib/vscode/build/lib/treeshaking.js b/lib/vscode/build/lib/treeshaking.js index 3abe020882bc..41cb33809b04 100644 --- a/lib/vscode/build/lib/treeshaking.js +++ b/lib/vscode/build/lib/treeshaking.js @@ -241,6 +241,9 @@ function nodeOrChildIsBlack(node) { } return false; } +function isSymbolWithDeclarations(symbol) { + return !!(symbol && symbol.declarations); +} function markNodes(ts, languageService, options) { const program = languageService.getProgram(); if (!program) { @@ -413,7 +416,7 @@ function markNodes(ts, languageService, options) { if (symbolImportNode) { setColor(symbolImportNode, 2 /* Black */); } - if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { for (let i = 0, len = symbol.declarations.length; i < len; i++) { const declaration = symbol.declarations[i]; if (ts.isSourceFile(declaration)) { @@ -686,7 +689,7 @@ function getRealNodeSymbol(ts, checker, node) { // get the aliased symbol instead. This allows for goto def on an import e.g. // import {A, B} from "mod"; // to jump to the implementation directly. - if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + if (symbol && symbol.flags & ts.SymbolFlags.Alias && symbol.declarations && shouldSkipAlias(node, symbol.declarations[0])) { const aliased = checker.getAliasedSymbol(symbol); if (aliased.declarations) { // We should mark the import as visited diff --git a/lib/vscode/build/lib/treeshaking.ts b/lib/vscode/build/lib/treeshaking.ts index f941e9791a76..f24b31e26ac9 100644 --- a/lib/vscode/build/lib/treeshaking.ts +++ b/lib/vscode/build/lib/treeshaking.ts @@ -323,6 +323,10 @@ function nodeOrChildIsBlack(node: ts.Node): boolean { return false; } +function isSymbolWithDeclarations(symbol: ts.Symbol | undefined | null): symbol is ts.Symbol & { declarations: ts.Declaration[] } { + return !!(symbol && symbol.declarations); +} + function markNodes(ts: typeof import('typescript'), languageService: ts.LanguageService, options: ITreeShakingOptions) { const program = languageService.getProgram(); if (!program) { @@ -530,7 +534,7 @@ function markNodes(ts: typeof import('typescript'), languageService: ts.Language setColor(symbolImportNode, NodeColor.Black); } - if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { for (let i = 0, len = symbol.declarations.length; i < len; i++) { const declaration = symbol.declarations[i]; if (ts.isSourceFile(declaration)) { @@ -595,7 +599,7 @@ function markNodes(ts: typeof import('typescript'), languageService: ts.Language } } -function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol): boolean { +function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol & { declarations: ts.Declaration[] }): boolean { for (let i = 0, len = symbol.declarations.length; i < len; i++) { const declaration = symbol.declarations[i]; const declarationSourceFile = declaration.getSourceFile(); @@ -838,7 +842,7 @@ function getRealNodeSymbol(ts: typeof import('typescript'), checker: ts.TypeChec // get the aliased symbol instead. This allows for goto def on an import e.g. // import {A, B} from "mod"; // to jump to the implementation directly. - if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + if (symbol && symbol.flags & ts.SymbolFlags.Alias && symbol.declarations && shouldSkipAlias(node, symbol.declarations[0])) { const aliased = checker.getAliasedSymbol(symbol); if (aliased.declarations) { // We should mark the import as visited diff --git a/lib/vscode/build/lib/util.js b/lib/vscode/build/lib/util.js index 8d0294e4cebc..b58fb754d2bb 100644 --- a/lib/vscode/build/lib/util.js +++ b/lib/vscode/build/lib/util.js @@ -269,6 +269,8 @@ function streamToPromise(stream) { } exports.streamToPromise = streamToPromise; function getElectronVersion() { + // NOTE@coder: Fix version due to .yarnrc removal. + return process.versions.node; const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; diff --git a/lib/vscode/build/lib/util.ts b/lib/vscode/build/lib/util.ts index 48853bc6201a..2c64a4bfe64b 100644 --- a/lib/vscode/build/lib/util.ts +++ b/lib/vscode/build/lib/util.ts @@ -336,8 +336,6 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { } export function getElectronVersion(): string { + // NOTE@coder: Fix version due to .yarnrc removal. return process.versions.node; - const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); - const target = /^target "(.*)"$/m.exec(yarnrc)![1]; - return target; } diff --git a/lib/vscode/build/npm/update-localization-extension.js b/lib/vscode/build/npm/update-localization-extension.js index b1656dc88f44..7c6a73aa9d84 100644 --- a/lib/vscode/build/npm/update-localization-extension.js +++ b/lib/vscode/build/npm/update-localization-extension.js @@ -20,19 +20,12 @@ function update(options) { if (!idOrPath) { throw new Error('Argument must be the location of the localization extension.'); } - let transifex = options.transifex; let location = options.location; - if (transifex === true && location !== undefined) { - throw new Error('Either --transifex or --location can be specified, but not both.'); - } - if (!transifex && !location) { - transifex = true; - } if (location !== undefined && !fs.existsSync(location)) { throw new Error(`${location} doesn't exist.`); } let locExtFolder = idOrPath; - if (/^\w{2}(-\w+)?$/.test(idOrPath)) { + if (/^\w{2,3}(-\w+)?$/.test(idOrPath)) { locExtFolder = path.join('..', 'vscode-loc', 'i18n', `vscode-language-pack-${idOrPath}`); } let locExtStat = fs.statSync(locExtFolder); @@ -53,79 +46,55 @@ function update(options) { if (!localization.languageId || !localization.languageName || !localization.localizedLanguageName) { throw new Error('Each localization contribution must define "languageId", "languageName" and "localizedLanguageName" properties.'); } - let server = localization.server || 'www.transifex.com'; - let userName = localization.userName || 'api'; - let apiToken = process.env.TRANSIFEX_API_TOKEN; - let languageId = localization.transifexId || localization.languageId; + let languageId = localization.languageId; let translationDataFolder = path.join(locExtFolder, 'translations'); - if (languageId === "zh-cn") { - languageId = "zh-hans"; - } - if (languageId === "zh-tw") { - languageId = "zh-hant"; + + switch (languageId) { + case 'zh-cn': + languageId = 'zh-Hans'; + break; + case 'zh-tw': + languageId = 'zh-Hant'; + break; + case 'pt-br': + languageId = 'pt-BR'; + break; } + if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { console.log('Clearing \'' + translationDataFolder + '\'...'); rimraf.sync(translationDataFolder); } - if (transifex) { - console.log(`Downloading translations for ${languageId} to '${translationDataFolder}' ...`); - let translationPaths = []; - i18n.pullI18nPackFiles(server, userName, apiToken, { id: languageId }, translationPaths) - .on('error', (error) => { - console.log(`Error occurred while importing translations:`); - translationPaths = undefined; - if (Array.isArray(error)) { - error.forEach(console.log); - } else if (error) { - console.log(error); - } else { - console.log('Unknown error'); + console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`); + let translationPaths = []; + gulp.src(path.join(location, '**', languageId, '*.xlf'), { silent: false }) + .pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths, languageId === 'ps')) + .on('error', (error) => { + console.log(`Error occurred while importing translations:`); + translationPaths = undefined; + if (Array.isArray(error)) { + error.forEach(console.log); + } else if (error) { + console.log(error); + } else { + console.log('Unknown error'); + } + }) + .pipe(vfs.dest(translationDataFolder)) + .on('end', function () { + if (translationPaths !== undefined) { + localization.translations = []; + for (let tp of translationPaths) { + localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}` }); } - }) - .pipe(vfs.dest(translationDataFolder)) - .on('end', function () { - if (translationPaths !== undefined) { - localization.translations = []; - for (let tp of translationPaths) { - localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}`}); - } - fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t')); - } - }); - } else { - console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`); - let translationPaths = []; - gulp.src(path.join(location, languageId, '**', '*.xlf')) - .pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths, languageId === 'ps')) - .on('error', (error) => { - console.log(`Error occurred while importing translations:`); - translationPaths = undefined; - if (Array.isArray(error)) { - error.forEach(console.log); - } else if (error) { - console.log(error); - } else { - console.log('Unknown error'); - } - }) - .pipe(vfs.dest(translationDataFolder)) - .on('end', function () { - if (translationPaths !== undefined) { - localization.translations = []; - for (let tp of translationPaths) { - localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}`}); - } - fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t')); - } - }); - } + fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t')); + } + }); }); } if (path.basename(process.argv[1]) === 'update-localization-extension.js') { var options = minimist(process.argv.slice(2), { - boolean: 'transifex', string: 'location' }); update(options); diff --git a/lib/vscode/build/package.json b/lib/vscode/build/package.json index 9c1e9e3823ab..69a210e5e57b 100644 --- a/lib/vscode/build/package.json +++ b/lib/vscode/build/package.json @@ -22,7 +22,7 @@ "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^8.2.0", - "@types/node": "^14.14.37", + "@types/node": "14.x", "@types/p-limit": "^2.2.0", "@types/plist": "^3.0.2", "@types/pump": "^1.0.1", @@ -38,7 +38,7 @@ "byline": "^5.0.0", "colors": "^1.4.0", "commander": "^7.0.0", - "esbuild": "^0.8.30", + "esbuild": "^0.12.6", "fs-extra": "^9.1.0", "got": "11.8.1", "iconv-lite-umd": "0.6.8", @@ -47,7 +47,7 @@ "mkdirp": "^1.0.4", "p-limit": "^3.1.0", "source-map": "0.6.1", - "typescript": "^4.3.0-dev.20210426", + "typescript": "^4.4.0-dev.20210528", "vsce": "1.88.0", "vscode-universal": "deepak1556/universal#61454d96223b774c53cda10f72c2098c0ce02d58" }, diff --git a/lib/vscode/build/yarn.lock b/lib/vscode/build/yarn.lock index bdefb0d7c030..9d50c2f27df4 100644 --- a/lib/vscode/build/yarn.lock +++ b/lib/vscode/build/yarn.lock @@ -235,10 +235,20 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@*", "@types/node@^14.14.21", "@types/node@^14.14.37": - version "14.14.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" - integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== +"@types/node@*": + version "8.0.51" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" + integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== + +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + +"@types/node@^14.14.21": + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== "@types/p-limit@^2.2.0": version "2.2.0" @@ -683,7 +693,12 @@ dom-serializer@0, dom-serializer@~0.1.0: domelementtype "~1.1.1" entities "~1.1.1" -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.2.1.tgz#578558ef23befac043a1abb0db07635509393479" + integrity sha512-SQVCLFS2E7G5CRCMdn6K9bIhRj1bS6QBWZfF0TUPh4V/BbqrQ619IdSS3/izn0FZ+9l+uODzaZjb08fjOfablA== + +domelementtype@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" integrity sha1-sXrtguirWeUt2cGbF1bg/BhyBMI= @@ -733,10 +748,10 @@ entities@~2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== -esbuild@^0.8.30: - version "0.8.30" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.30.tgz#3d057ff9ffe6d5d30bccb0afe8cc92a2e69622d3" - integrity sha512-gCJQYUMO9QNrfpNOIiCnFoX41nWiPFCvURBQF+qWckyJ7gmw2xCShdKCXvS+RZcQ5krcxEOLIkzujqclePKhfw== +esbuild@^0.12.6: + version "0.12.6" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.6.tgz#85bc755c7cf3005d4f34b4f10f98049ce0ee67ce" + integrity sha512-RDvVLvAjsq/kIZJoneMiUOH7EE7t2QaW7T3Q7EdQij14+bZbDq5sndb0tTanmHIFSqZVMBMMyqzVHkS3dJobeA== escape-string-regexp@^1.0.5: version "1.0.5" @@ -818,7 +833,19 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -glob@^7.0.6, glob@^7.1.6: +glob@^7.0.6: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -912,11 +939,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3: +inherits@2, inherits@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1010,7 +1042,12 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -mime@^1.3.4, mime@^1.4.1: +mime@^1.3.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + +mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -1060,9 +1097,9 @@ nth-check@~1.0.1: boolbase "~1.0.0" object-inspect@^1.9.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" - integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -1283,10 +1320,10 @@ typescript@^4.1.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== -typescript@^4.3.0-dev.20210426: - version "4.3.0-dev.20210426" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210426.tgz#00198cb8828f6a04b4e0ae32554a486bf7137a53" - integrity sha512-8YTqlzf3w8O8XwnnRlwRV2rswu7V7WEPUnAnH1BPPMrr06thNByMjIadA5SDW3tUJc1MG8Uj3NgZYocU5fWTVg== +typescript@^4.4.0-dev.20210528: + version "4.4.0-dev.20210528" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.0-dev.20210528.tgz#42453bc42e9d9df8ad0741c207c24d56407c0347" + integrity sha512-ACV+mYKC+PhWUXIDUL6qmFClIdrKc20KRxDePt8bniCgkKQD4XRYKl7m02paxJM3nTMRdlfjs0ncaslA5BA1GA== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" diff --git a/lib/vscode/cglicenses.json b/lib/vscode/cglicenses.json index 621c771c4c2a..7c42f531d52a 100644 --- a/lib/vscode/cglicenses.json +++ b/lib/vscode/cglicenses.json @@ -54,35 +54,6 @@ "Copyright (c) Microsoft Corporation. All rights reserved." ] }, - { - // Reason: The npm package lacks a repoURL field - // So the license at https://github.com/floatdrop/pinkie/blob/master/license - // cannot be found by the OSS tool automatically. - "name": "pinkie", - "fullLicenseText": [ - "The MIT License (MIT)", - "", - "Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop)", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in", - "all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN", - "THE SOFTWARE." - ] - }, { "name": "big-integer", "prependLicenseText": [ @@ -134,6 +105,7 @@ { // Reason: Repository lacks license text. // https://github.com/LinusU/load-yaml-file/blob/master/package.json declares MIT. + // https://github.com/LinusU/load-yaml-file/issues/2 "name": "load-yaml-file", "fullLicenseText": [ "MIT License", @@ -149,6 +121,7 @@ { // Reason: Repository lacks license text. // https://github.com/othiym23/emitter-listener/blob/master/package.json declares BSD-2-Clause. + // https://github.com/othiym23/emitter-listener/issues/3 "name": "emitter-listener", "fullLicenseText": [ "BSD 2-Clause \"Simplified\" License", @@ -174,19 +147,5 @@ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS", "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ] - }, - { - // Reason: Repository has been deleted (package.json declares MIT). - "name": "vscode-js-debug-cdp-proxy-api", - "fullLicenseText": [ - "MIT License", - "Copyright (c) Manuel Alabor", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] } ] diff --git a/lib/vscode/cgmanifest.json b/lib/vscode/cgmanifest.json index aa54e3243ab4..474d6191ba53 100644 --- a/lib/vscode/cgmanifest.json +++ b/lib/vscode/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "5342041f85833c038dcbc5632d62fc10f7592323" + "commitHash": "cd7a46bf02a768a1aabf9443f6ee469bc6e28e7c" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "89.0.4389.114" + "version": "89.0.4389.128" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "9ce7c512475aa6aa91417a3b08e19f85a8587a30" + "commitHash": "8d55658bfa8b5983e1a90ad079c2e2ac91ee7af0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "12.0.4" + "version": "12.0.7" }, { "component": { diff --git a/lib/vscode/coder.js b/lib/vscode/coder.js index d3ed513329b1..9498585c88f7 100644 --- a/lib/vscode/coder.js +++ b/lib/vscode/coder.js @@ -30,13 +30,13 @@ const vscodeResources = [ "out-build/bootstrap-fork.js", "out-build/bootstrap-amd.js", 'out-build/bootstrap-node.js', - "out-build/paths.js", 'out-build/vs/**/*.{svg,png,html,ttf,jpg}', "!out-build/vs/code/browser/workbench/*.html", '!out-build/vs/code/electron-browser/**', "out-build/vs/base/common/performance.js", "out-build/vs/base/node/languagePacks.js", 'out-build/vs/base/browser/ui/codicons/codicon/**', + 'out-build/vs/base/node/userDataPath.js', "out-build/vs/workbench/browser/media/*-theme.css", "out-build/vs/workbench/contrib/debug/**/*.json", "out-build/vs/workbench/contrib/externalTerminal/**/*.scpt", diff --git a/lib/vscode/extensions/bat/cgmanifest.json b/lib/vscode/extensions/bat/cgmanifest.json index 5bc3e285f0c1..7e0426c1387f 100644 --- a/lib/vscode/extensions/bat/cgmanifest.json +++ b/lib/vscode/extensions/bat/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "mmims/language-batchfile", "repositoryUrl": "https://github.com/mmims/language-batchfile", - "commitHash": "95ea8c699f7a8296b15767069868532d52631c46" + "commitHash": "6154ae25a24e01ac9329e7bcf958e093cd8733a9" } }, "license": "MIT", - "version": "0.7.5" + "version": "0.7.6" } ], "version": 1 diff --git a/lib/vscode/extensions/bat/syntaxes/batchfile.tmLanguage.json b/lib/vscode/extensions/bat/syntaxes/batchfile.tmLanguage.json index 9eff3c9de2ba..85e03dbc85b0 100644 --- a/lib/vscode/extensions/bat/syntaxes/batchfile.tmLanguage.json +++ b/lib/vscode/extensions/bat/syntaxes/batchfile.tmLanguage.json @@ -4,9 +4,18 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/mmims/language-batchfile/commit/95ea8c699f7a8296b15767069868532d52631c46", + "version": "https://github.com/mmims/language-batchfile/commit/6154ae25a24e01ac9329e7bcf958e093cd8733a9", "name": "Batch File", "scopeName": "source.batchfile", + "injections": { + "L:meta.block.repeat.batchfile": { + "patterns": [ + { + "include": "#repeatParameter" + } + ] + } + }, "patterns": [ { "include": "#commands" @@ -46,7 +55,7 @@ "commands": { "patterns": [ { - "match": "(?<=^|[\\s@])(?i:adprep|append|arp|assoc|at|atmadm|attrib|auditpol|autochk|autoconv|autofmt|bcdboot|bcdedit|bdehdcfg|bitsadmin|bootcfg|brea|cacls|cd|certreq|certutil|change|chcp|chdir|chglogon|chgport|chgusr|chkdsk|chkntfs|choice|cipher|clip|cls|clscluadmin|cluster|cmd|cmdkey|cmstp|color|comp|compact|convert|copy|cprofile|cscript|csvde|date|dcdiag|dcgpofix|dcpromo|defra|del|dfscmd|dfsdiag|dfsrmig|diantz|dir|dirquota|diskcomp|diskcopy|diskpart|diskperf|diskraid|diskshadow|dispdiag|doin|dnscmd|doskey|driverquery|dsacls|dsadd|dsamain|dsdbutil|dsget|dsmgmt|dsmod|dsmove|dsquery|dsrm|edit|endlocal|eraseesentutl|eventcreate|eventquery|eventtriggers|evntcmd|expand|extract|fc|filescrn|find|findstr|finger|flattemp|fonde|forfiles|format|freedisk|fsutil|ftp|ftype|fveupdate|getmac|gettype|gpfixup|gpresult|gpupdate|graftabl|hashgen|hep|helpctr|hostname|icacls|iisreset|inuse|ipconfig|ipxroute|irftp|ismserv|jetpack|klist|ksetup|ktmutil|ktpass|label|ldifd|ldp|lodctr|logman|logoff|lpq|lpr|macfile|makecab|manage-bde|mapadmin|md|mkdir|mklink|mmc|mode|more|mount|mountvol|move|mqbup|mqsvc|mqtgsvc|msdt|msg|msiexec|msinfo32|mstsc|nbtstat|net computer|net group|net localgroup|net print|net session|net share|net start|net stop|net use|net user|net view|net|netcfg|netdiag|netdom|netsh|netstat|nfsadmin|nfsshare|nfsstat|nlb|nlbmgr|nltest|nslookup|ntackup|ntcmdprompt|ntdsutil|ntfrsutl|openfiles|pagefileconfig|path|pathping|pause|pbadmin|pentnt|perfmon|ping|pnpunatten|pnputil|popd|powercfg|powershell|powershell_ise|print|prncnfg|prndrvr|prnjobs|prnmngr|prnport|prnqctl|prompt|pubprn|pushd|pushprinterconnections|pwlauncher|qappsrv|qprocess|query|quser|qwinsta|rasdial|rcp|rd|rdpsign|regentc|recover|redircmp|redirusr|reg|regini|regsvr32|relog|ren|rename|rendom|repadmin|repair-bde|replace|reset session|rxec|risetup|rmdir|robocopy|route|rpcinfo|rpcping|rsh|runas|rundll32|rwinsta|scp|sc|schtasks|scwcmd|secedit|serverceipoptin|servrmanagercmd|serverweroptin|setspn|setx|sfc|shadow|shift|showmount|shutdown|sort|ssh|start|storrept|subst|sxstrace|ysocmgr|systeminfo|takeown|tapicfg|taskkill|tasklist|tcmsetup|telnet|tftp|time|timeout|title|tlntadmn|tpmvscmgr|tpmvscmgr|tacerpt|tracert|tree|tscon|tsdiscon|tsecimp|tskill|tsprof|type|typeperf|tzutil|uddiconfig|umount|unlodctr|ver|verifier|verif|vol|vssadmin|w32tm|waitfor|wbadmin|wdsutil|wecutil|wevtutil|where|whoami|winnt|winnt32|winpop|winrm|winrs|winsat|wlbs|mic|wscript|xcopy)(?=$|\\s)", + "match": "(?<=^|[\\s@])(?i:adprep|append|arp|assoc|at|atmadm|attrib|auditpol|autochk|autoconv|autofmt|bcdboot|bcdedit|bdehdcfg|bitsadmin|bootcfg|brea|cacls|cd|certreq|certutil|change|chcp|chdir|chglogon|chgport|chgusr|chkdsk|chkntfs|choice|cipher|clip|cls|clscluadmin|cluster|cmd|cmdkey|cmstp|color|comp|compact|convert|copy|cprofile|cscript|csvde|date|dcdiag|dcgpofix|dcpromo|defra|del|dfscmd|dfsdiag|dfsrmig|diantz|dir|dirquota|diskcomp|diskcopy|diskpart|diskperf|diskraid|diskshadow|dispdiag|doin|dnscmd|doskey|driverquery|dsacls|dsadd|dsamain|dsdbutil|dsget|dsmgmt|dsmod|dsmove|dsquery|dsrm|edit|endlocal|eraseesentutl|eventcreate|eventquery|eventtriggers|evntcmd|expand|extract|fc|filescrn|find|findstr|finger|flattemp|fonde|forfiles|format|freedisk|fsutil|ftp|ftype|fveupdate|getmac|gettype|gpfixup|gpresult|gpupdate|graftabl|hashgen|hep|helpctr|hostname|icacls|iisreset|inuse|ipconfig|ipxroute|irftp|ismserv|jetpack|klist|ksetup|ktmutil|ktpass|label|ldifd|ldp|lodctr|logman|logoff|lpq|lpr|macfile|makecab|manage-bde|mapadmin|md|mkdir|mklink|mmc|mode|more|mount|mountvol|move|mqbup|mqsvc|mqtgsvc|msdt|msg|msiexec|msinfo32|mstsc|nbtstat|net computer|net group|net localgroup|net print|net session|net share|net start|net stop|net use|net user|net view|net|netcfg|netdiag|netdom|netsh|netstat|nfsadmin|nfsshare|nfsstat|nlb|nlbmgr|nltest|nslookup|ntackup|ntcmdprompt|ntdsutil|ntfrsutl|openfiles|pagefileconfig|path|pathping|pause|pbadmin|pentnt|perfmon|ping|pnpunatten|pnputil|popd|powercfg|powershell|powershell_ise|print|prncnfg|prndrvr|prnjobs|prnmngr|prnport|prnqctl|prompt|pubprn|pushd|pushprinterconnections|pwlauncher|qappsrv|qprocess|query|quser|qwinsta|rasdial|rcp|rd|rdpsign|regentc|recover|redircmp|redirusr|reg|regini|regsvr32|relog|ren|rename|rendom|repadmin|repair-bde|replace|reset session|rxec|risetup|rmdir|robocopy|route|rpcinfo|rpcping|rsh|runas|rundll32|rwinsta|sc|schtasks|scp|scwcmd|secedit|serverceipoptin|servrmanagercmd|serverweroptin|setspn|setx|sfc|sftp|shadow|shift|showmount|shutdown|sort|ssh|ssh-add|ssh-agent|ssh-keygen|ssh-keyscan|start|storrept|subst|sxstrace|ysocmgr|systeminfo|takeown|tapicfg|taskkill|tasklist|tcmsetup|telnet|tftp|time|timeout|title|tlntadmn|tpmvscmgr|tpmvscmgr|tacerpt|tracert|tree|tscon|tsdiscon|tsecimp|tskill|tsprof|type|typeperf|tzutil|uddiconfig|umount|unlodctr|ver|verifier|verif|vol|vssadmin|w32tm|waitfor|wbadmin|wdsutil|wecutil|wevtutil|where|whoami|winnt|winnt32|winpop|winrm|winrs|winsat|wlbs|wmic|wscript|wsl|xcopy)(?=$|\\s)", "name": "keyword.command.batchfile" }, { @@ -285,7 +294,7 @@ "name": "keyword.operator.logical.batchfile" }, { - "match": "([^ ][^=]*)(=)", + "match": "([^ =]*)(=)", "captures": { "1": { "name": "variable.other.readwrite.batchfile" @@ -420,8 +429,38 @@ "name": "keyword.control.conditional.batchfile" }, { - "match": "(?<=^|\\s)(?i)for(?=\\s)", - "name": "keyword.control.repeat.batchfile" + "begin": "(?<=^|[\\s(&^])(?i)for(?=\\s)", + "beginCaptures": { + "0": { + "name": "keyword.control.repeat.batchfile" + } + }, + "name": "meta.block.repeat.batchfile", + "end": "\\n", + "patterns": [ + { + "begin": "(?<=[\\s^])(?i)in(?=\\s)", + "beginCaptures": { + "0": { + "name": "keyword.control.repeat.in.batchfile" + } + }, + "end": "(?<=[\\s)^])(?i)do(?=\\s)|\\n", + "endCaptures": { + "0": { + "name": "keyword.control.repeat.do.batchfile" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + }, + { + "include": "$self" + } + ] } ] }, @@ -436,7 +475,7 @@ "labels": { "patterns": [ { - "match": "(?i)(?:^\\s*|(?<=goto)\\s*)(:)([^+=,;:\\s].*)$", + "match": "(?i)(?:^\\s*|(?<=call|goto)\\s*)(:)([^+=,;:\\s]\\S*)", "captures": { "1": { "name": "punctuation.separator.batchfile" @@ -512,6 +551,19 @@ } ] }, + "repeatParameter": { + "patterns": [ + { + "match": "(%%)(?:(?i:~[fdpnxsatz]*(?:\\$PATH:)?)?[a-zA-Z])", + "captures": { + "1": { + "name": "punctuation.definition.variable.batchfile" + } + }, + "name": "variable.parameter.repeat.batchfile" + } + ] + }, "strings": { "patterns": [ { @@ -546,15 +598,13 @@ "variables": { "patterns": [ { - "match": "(%)((~([fdpnxsatz]|\\$PATH:)*)?\\d|\\*)", + "match": "(%)(?:(?i:~[fdpnxsatz]*(?:\\$PATH:)?)?\\d|\\*)", "captures": { "1": { "name": "punctuation.definition.variable.batchfile" - }, - "2": { - "name": "variable.parameter.batchfile" } - } + }, + "name": "variable.parameter.batchfile" }, { "include": "#variable" diff --git a/lib/vscode/extensions/configuration-editing/package.json b/lib/vscode/extensions/configuration-editing/package.json index 324e0c80607d..80e627e5f939 100644 --- a/lib/vscode/extensions/configuration-editing/package.json +++ b/lib/vscode/extensions/configuration-editing/package.json @@ -139,7 +139,7 @@ ] }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/configuration-editing/schemas/attachContainer.schema.json b/lib/vscode/extensions/configuration-editing/schemas/attachContainer.schema.json index ec765b8fc24b..9cdd3a5f6d36 100644 --- a/lib/vscode/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/lib/vscode/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -54,6 +54,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -80,7 +93,13 @@ "properties": { "onAutoForward": { "type": "string", - "enum": ["notify", "openBrowser", "openPreview", "silent", "ignore"], + "enum": [ + "notify", + "openBrowser", + "openPreview", + "silent", + "ignore" + ], "enumDescriptions": [ "Shows a notification when a port is automatically forwarded.", "Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.", @@ -100,9 +119,28 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, - "defaultSnippets": [{ "body": { "onAutoForward": "ignore" } }], + "defaultSnippets": [ + { + "body": { + "onAutoForward": "ignore" + } + } + ], "markdownDescription": "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```", "additionalProperties": false }, diff --git a/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.generated.json b/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.generated.json index 90e70c0125ac..112b4e482795 100644 --- a/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.generated.json +++ b/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.generated.json @@ -162,6 +162,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -215,6 +228,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -246,7 +272,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -256,7 +302,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -266,7 +312,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -276,11 +322,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -299,6 +357,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -461,6 +541,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -514,6 +607,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -545,7 +651,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -555,7 +681,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -565,7 +691,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -575,11 +701,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -598,6 +736,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -736,6 +896,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -789,6 +962,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -820,7 +1006,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -830,7 +1036,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -840,7 +1046,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -850,11 +1056,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -873,6 +1091,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -977,6 +1217,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -1030,6 +1283,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1061,7 +1327,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1071,7 +1357,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1081,7 +1367,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1091,11 +1377,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -1114,6 +1412,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "required": [ @@ -1187,6 +1507,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -1240,6 +1573,19 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1271,7 +1617,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1281,7 +1647,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1291,7 +1657,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -1301,11 +1667,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -1324,6 +1702,28 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.src.json b/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.src.json index 2987aad16c5a..14c13a958b3b 100644 --- a/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.src.json +++ b/lib/vscode/extensions/configuration-editing/schemas/devContainer.schema.src.json @@ -68,6 +68,16 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": ["http", "https"], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -120,6 +130,16 @@ "type": "string", "description": "Label that will be shown in the UI for this port.", "default": "Application" + }, + "requireLocalPort": { + "type": "boolean", + "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", + "default": false + }, + "protocol": { + "type": "string", + "enum": ["http", "https"], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -151,7 +171,27 @@ "string", "array" ], - "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run locally before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "onCreateCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, + "updateContentCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -161,7 +201,7 @@ "string", "array" ], - "description": "A command to run after creating the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -171,7 +211,7 @@ "string", "array" ], - "description": "A command to run after starting the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } @@ -181,11 +221,23 @@ "string", "array" ], - "description": "A command to run after attaching to the container. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", "items": { "type": "string" } }, + "waitFor": { + "type": "string", + "enum": [ + "initializeCommand", + "onCreateCommand", + "updateContentCommand", + "postCreateCommand", + "postStartCommand", + "postAttachCommand" + ], + "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"." + }, "devPort": { "type": "integer", "description": "The port VS Code can use to connect to its backend." @@ -204,6 +256,32 @@ "type": "object", "additionalProperties": true, "description": "Codespaces-specific configuration." + }, + "hostRequirements": { + "type": "object", + "description": "Host hardware requirements.", + "allOf": [ + { + "type": "object", + "properties": { + "cpus": { + "type": "integer", + "minimum": 1, + "description": "Number of required CPUs." + }, + "memory": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb." + }, + "storage": { + "type": "string", + "pattern": "^\\d+([tgmk]b)?$", + "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb." + } + } + } + ] } } }, diff --git a/lib/vscode/extensions/configuration-editing/src/settingsDocumentHelper.ts b/lib/vscode/extensions/configuration-editing/src/settingsDocumentHelper.ts index a21ec18e3103..3a469148da61 100644 --- a/lib/vscode/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/lib/vscode/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { getLocation, Location, parse } from 'jsonc-parser'; import * as nls from 'vscode-nls'; -import { provideInstalledExtensionProposals, provideWorkspaceTrustExtensionProposals } from './extensionsProposals'; +import { provideInstalledExtensionProposals } from './extensionsProposals'; const localize = nls.loadMessageBundle(); @@ -60,13 +60,9 @@ export class SettingsDocument { return provideInstalledExtensionProposals(alreadyConfigured, `: [\n\t"ui"\n]`, range, true); } - // extensions.supportUntrustedWorkspaces - if (location.path[0] === 'extensions.supportUntrustedWorkspaces' && location.path.length === 2 && location.isAtPropertyKey) { - let alreadyConfigured: string[] = []; - try { - alreadyConfigured = Object.keys(parse(this.document.getText())['extensions.supportUntrustedWorkspaces']); - } catch (e) {/* ignore error */ } - return provideWorkspaceTrustExtensionProposals(alreadyConfigured, range); + // remote.portsAttributes + if (location.path[0] === 'remote.portsAttributes' && location.path.length === 2 && location.isAtPropertyKey) { + return this.providePortsAttributesCompletionItem(range); } return this.provideLanguageOverridesCompletionItems(location, position); @@ -247,6 +243,31 @@ export class SettingsDocument { return Promise.resolve([]); } + private providePortsAttributesCompletionItem(range: vscode.Range): vscode.CompletionItem[] { + return [this.newSnippetCompletionItem( + { + label: '\"3000\"', + documentation: 'Single Port Attribute', + range, + snippet: '\n \"${1:3000}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n' + }), + this.newSnippetCompletionItem( + { + label: '\"5000-6000\"', + documentation: 'Ranged Port Attribute', + range, + snippet: '\n \"${1:40000-55000}\": {\n \"onAutoForward\": \"${2:ignore}\"\n }\n' + }), + this.newSnippetCompletionItem( + { + label: '\".+\\\\/server.js\"', + documentation: 'Command Match Port Attribute', + range, + snippet: '\n \"${1:.+\\\\/server.js\}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n' + }) + ]; + } + private newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem { const item = new vscode.CompletionItem(text); item.kind = vscode.CompletionItemKind.Value; diff --git a/lib/vscode/extensions/configuration-editing/yarn.lock b/lib/vscode/extensions/configuration-editing/yarn.lock index ebfaa046da41..d7215349a938 100644 --- a/lib/vscode/extensions/configuration-editing/yarn.lock +++ b/lib/vscode/extensions/configuration-editing/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== jsonc-parser@^2.2.1: version "2.2.1" diff --git a/lib/vscode/extensions/csharp/cgmanifest.json b/lib/vscode/extensions/csharp/cgmanifest.json index 0b192d30ec47..6156bbb50828 100644 --- a/lib/vscode/extensions/csharp/cgmanifest.json +++ b/lib/vscode/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "4d14854c9bfc9d84cce625d2bfebaac9741b9db8" + "commitHash": "572697a2c2267430010b3534281f73337144e94f" } }, "license": "MIT", diff --git a/lib/vscode/extensions/csharp/syntaxes/csharp.tmLanguage.json b/lib/vscode/extensions/csharp/syntaxes/csharp.tmLanguage.json index f67a94ba101d..8e26aaefa775 100644 --- a/lib/vscode/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/lib/vscode/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/4d14854c9bfc9d84cce625d2bfebaac9741b9db8", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/572697a2c2267430010b3534281f73337144e94f", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -286,9 +286,6 @@ { "include": "#this-or-base-expression" }, - { - "include": "#switch-expression" - }, { "include": "#conditional-operator" }, @@ -619,7 +616,7 @@ ] }, "delegate-declaration": { - "begin": "(?x)\n(?:\\b(delegate)\\b)\\s+\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\s*\n(<([^<>]+)>)?\\s*\n(?=\\()", + "begin": "(?x)\n(?:\\b(delegate)\\b)\\s+\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s+\n(\\g)\\s*\n(<([^<>]+)>)?\\s*\n(?=\\()", "beginCaptures": { "1": { "name": "keyword.other.delegate.cs" @@ -970,7 +967,7 @@ ] }, "field-declaration": { - "begin": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\s* # first field name\n(?!=>|==)(?=,|;|=|$)", + "begin": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s+\n(\\g)\\s* # first field name\n(?!=>|==)(?=,|;|=|$)", "beginCaptures": { "1": { "patterns": [ @@ -1004,7 +1001,7 @@ ] }, "property-declaration": { - "begin": "(?x)\n\n# The negative lookahead below ensures that we don't match nested types\n# or other declarations as properties.\n(?![[:word:][:space:]]*\\b(?:class|interface|struct|enum|event)\\b)\n\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g)\\s*\n(?=\\{|=>|$)", + "begin": "(?x)\n\n# The negative lookahead below ensures that we don't match nested types\n# or other declarations as properties.\n(?![[:word:][:space:]]*\\b(?:class|interface|struct|enum|event)\\b)\n\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g)\\s*\n(?=\\{|=>|$)", "beginCaptures": { "1": { "patterns": [ @@ -1047,7 +1044,7 @@ ] }, "indexer-declaration": { - "begin": "(?x)\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?this)\\s*\n(?=\\[)", + "begin": "(?x)\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?this)\\s*\n(?=\\[)", "beginCaptures": { "1": { "patterns": [ @@ -1090,7 +1087,7 @@ ] }, "event-declaration": { - "begin": "(?x)\n\\b(event)\\b\\s*\n(?\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g(?:\\s*,\\s*\\g)*)\\s*\n(?=\\{|;|$)", + "begin": "(?x)\n\\b(event)\\b\\s*\n(?\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(?\\g(?:\\s*,\\s*\\g)*)\\s*\n(?=\\{|;|$)", "beginCaptures": { "1": { "name": "keyword.other.event.cs" @@ -1220,7 +1217,7 @@ ] }, "method-declaration": { - "begin": "(?x)\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(\\g)\\s*\n(<([^<>]+)>)?\\s*\n(?=\\()", + "begin": "(?x)\n(?\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\\s+\n)\n(?\\g\\s*\\.\\s*)?\n(\\g)\\s*\n(<([^<>]+)>)?\\s*\n(?=\\()", "beginCaptures": { "1": { "patterns": [ @@ -1356,7 +1353,7 @@ ] }, "operator-declaration": { - "begin": "(?x)\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?(?:\\b(?:operator)))\\s*\n(?(?:\\+|-|\\*|/|%|&|\\||\\^|\\<\\<|\\>\\>|==|!=|\\>|\\<|\\>=|\\<=|!|~|\\+\\+|--|true|false))\\s*\n(?=\\()", + "begin": "(?x)\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s*\n(?(?:\\b(?:operator)))\\s*\n(?(?:\\+|-|\\*|/|%|&|\\||\\^|\\<\\<|\\>\\>|==|!=|\\>|\\<|\\>=|\\<=|!|~|\\+\\+|--|true|false))\\s*\n(?=\\()", "beginCaptures": { "1": { "patterns": [ @@ -1389,7 +1386,7 @@ ] }, "conversion-operator-declaration": { - "begin": "(?x)\n(?(?:\\b(?:explicit|implicit)))\\s*\n(?(?:\\b(?:operator)))\\s*\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?=\\()", + "begin": "(?x)\n(?(?:\\b(?:explicit|implicit)))\\s*\n(?(?:\\b(?:operator)))\\s*\n(?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s*\n(?=\\()", "beginCaptures": { "1": { "patterns": [ @@ -1618,7 +1615,7 @@ "end": "(?=;)", "patterns": [ { - "include": "#statement" + "include": "#expression" } ] }, @@ -1721,178 +1718,6 @@ } ] }, - "switch-expression": { - "begin": "(?x) (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\b\\s*", - "beginCaptures": { - "1": { - "patterns": [ - { - "include": "#type" - } - ] - }, - "2": { - "name": "entity.name.variable.local.cs" - } - }, - "end": "(?==>)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#switch-when-clause" - } - ] - }, - "switch-property-expression": { - "begin": "(?x) # e.g. int x OR var x\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\\s*\n(\\{)", - "beginCaptures": { - "1": { - "patterns": [ - { - "include": "#type" - } - ] - }, - "6": { - "name": "punctuation.curlybrace.open.cs" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.curlybrace.close.cs" - } - }, - "patterns": [ - { - "include": "#expression" - }, - { - "include": "#punctuation-comma" - } - ] - }, - "switch-var-pattern": { - "begin": "(?x) # match foreach (var (x, y) in ...)\n(?:\\b(var)\\b\\s*)\n(?\\((?:[^\\(\\)]|\\g)+\\))\\s*", - "beginCaptures": { - "1": { - "name": "keyword.other.var.cs" - }, - "2": { - "patterns": [ - { - "include": "#tuple-declaration-deconstruction-element-list" - } - ] - } - }, - "end": "(?==>)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#switch-when-clause" - } - ] - }, - "switch-when-clause": { - "begin": "(?)", - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#expression" - }, - { - "include": "#punctuation-comma" - }, - { - "match": "\\(", - "captures": { - "0": { - "name": "punctuation.parenthesis.open.cs" - } - } - }, - { - "match": "\\)", - "captures": { - "0": { - "name": "punctuation.parenthesis.close.cs" - } - } - } - ] - }, "switch-label": { "patterns": [ { @@ -2040,7 +1865,7 @@ }, "patterns": [ { - "match": "(?x)\n(?:\n (\\bvar\\b)|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s+\n\\b(in)\\b", + "match": "(?x)\n(?:\n (\\bvar\\b)|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\n)\\s+\n(\\g)\\s+\n\\b(in)\\b", "captures": { "1": { "name": "keyword.other.var.cs" @@ -2159,7 +1984,7 @@ }, "patterns": [ { - "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?:(\\g)\\b)?", + "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s*\n(?:(\\g)\\b)?", "captures": { "1": { "patterns": [ @@ -2319,37 +2144,31 @@ { "include": "#local-variable-declaration" }, - { - "include": "#local-function-declaration" - }, { "include": "#local-tuple-var-deconstruction" } ] }, "local-variable-declaration": { - "begin": "(?x)\n(?:\n (?:(\\busing)\\s+)?\n (?:(\\bref)\\s+(?:(\\breadonly)\\s+)?)?(\\bvar\\b)| # ref local\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref local\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s*\n(?!=>)\n(?=,|;|=|\\))", + "begin": "(?x)\n(?:\n (?:(\\bref)\\s+(?:(\\breadonly)\\s+)?)?(\\bvar\\b)| # ref local\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref local\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\n)\\s+\n(\\g)\\s*\n(?!=>)\n(?=,|;|=|\\))", "beginCaptures": { "1": { - "name": "keyword.other.using.cs" + "name": "storage.modifier.cs" }, "2": { "name": "storage.modifier.cs" }, "3": { - "name": "storage.modifier.cs" - }, - "4": { "name": "keyword.other.var.cs" }, - "5": { + "4": { "patterns": [ { "include": "#type" } ] }, - "10": { + "9": { "name": "entity.name.variable.local.cs" } }, @@ -2371,7 +2190,7 @@ ] }, "local-constant-declaration": { - "begin": "(?x)\n(?\\b(?:const)\\b)\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\s*\n(?=,|;|=)", + "begin": "(?x)\n(?\\b(?:const)\\b)\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s+\n(\\g)\\s*\n(?=,|;|=)", "beginCaptures": { "1": { "name": "storage.modifier.cs" @@ -2404,13 +2223,6 @@ } ] }, - "local-function-declaration": { - "patterns": [ - { - "include": "#method-declaration" - } - ] - }, "local-tuple-var-deconstruction": { "begin": "(?x) # e.g. var (x, y) = GetPoint();\n(?:\\b(var)\\b\\s*)\n(?\\((?:[^\\(\\)]|\\g)+\\))\\s*\n(?=;|=|\\))", "beginCaptures": { @@ -2520,7 +2332,7 @@ ] }, "declaration-expression-local": { - "match": "(?x) # e.g. int x OR var x\n(?:\n \\b(var)\\b|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\b\\s*\n(?=[,)\\]])", + "match": "(?x) # e.g. int x OR var x\n(?:\n \\b(var)\\b|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\n)\\s+\n(\\g)\\b\\s*\n(?=[,)\\]])", "captures": { "1": { "name": "keyword.other.var.cs" @@ -2538,7 +2350,7 @@ } }, "declaration-expression-tuple": { - "match": "(?x) # e.g. int x OR var x\n(?:\n \\b(var)\\b|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\b\\s*\n(?=[,)])", + "match": "(?x) # e.g. int x OR var x\n(?:\n \\b(var)\\b|\n (?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n )\n)\\s+\n(\\g)\\b\\s*\n(?=[,)])", "captures": { "1": { "name": "keyword.other.var.cs" @@ -2663,7 +2475,7 @@ }, "verbatim-interpolated-string": { "name": "string.quoted.double.cs", - "begin": "(?:\\$@|@\\$)\"", + "begin": "\\$@\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.cs" @@ -2900,7 +2712,7 @@ "patterns": [ { "name": "keyword.operator.assignment.compound.cs", - "match": "\\*=|/=|%=|\\+=|-=|\\?\\?=" + "match": "\\*=|/=|%=|\\+=|-=" }, { "name": "keyword.operator.assignment.compound.bitwise.cs", @@ -2948,26 +2760,6 @@ } ] }, - "switch-literal": { - "name": "constant.language.null.cs", - "match": "(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(\\))(?=\\s*@?[_[:alnum:]\\(])", + "match": "(?x)\n(\\()\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s*\n(\\))(?=\\s*@?[_[:alnum:]\\(])", "captures": { "1": { "name": "punctuation.parenthesis.open.cs" @@ -3055,7 +2847,7 @@ } }, "as-expression": { - "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?", + "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)?", "captures": { "1": { "name": "keyword.other.as.cs" @@ -3070,7 +2862,7 @@ } }, "is-expression": { - "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?", + "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)?", "captures": { "1": { "name": "keyword.other.is.cs" @@ -3096,7 +2888,7 @@ } }, "invocation-expression": { - "begin": "(?x)\n(?:(\\?)\\s*)? # preceding null-conditional operator?\n(?:(\\.)\\s*)? # preceding dot?\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # method name\n(?\\s*<([^<>]|\\g)+>\\s*)?\\s* # type arguments\n(?=\\() # open paren of argument list", + "begin": "(?x)\n(?:(\\?)\\s*)? # preceding null-conditional operator?\n(?:(\\.)\\s*)? # preceding dot?\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # method name\n(?\\s*<([^<>]|\\g)+>\\s*)?\\s* # type arguments\n(?=\\() # open paren of argument list", "beginCaptures": { "1": { "name": "keyword.operator.null-conditional.cs" @@ -3162,7 +2954,7 @@ } }, { - "match": "(?x)\n(\\.)?\\s*\n(@?[_[:alpha:]][_[:alnum:]]*)\n(?\\s*<([^<>]|\\g)+>\\s*)\n(?=\n (\\s*\\?)?\n \\s*\\.\\s*@?[_[:alpha:]][_[:alnum:]]*\n)", + "match": "(?x)\n(\\.)?\\s*\n(@?[_[:alpha:]][_[:alnum:]]*)\n(?\\s*<([^<>]|\\g)+>\\s*)\n(?=\n (\\s*\\?)?\n \\s*\\.\\s*@?[_[:alpha:]][_[:alnum:]]*\n)", "captures": { "1": { "name": "punctuation.accessor.cs" @@ -3200,7 +2992,7 @@ ] }, "object-creation-expression-with-parameters": { - "begin": "(?x)\n(new)\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?=\\()", + "begin": "(?x)\n(new)\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s*\n(?=\\()", "beginCaptures": { "1": { "name": "keyword.other.new.cs" @@ -3221,7 +3013,7 @@ ] }, "object-creation-expression-with-no-parameters": { - "match": "(?x)\n(new)\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s*\n(?=\\{|$)", + "match": "(?x)\n(new)\\s+\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s*\n(?=\\{|$)", "captures": { "1": { "name": "keyword.other.new.cs" @@ -3236,7 +3028,7 @@ } }, "array-creation-expression": { - "begin": "(?x)\n\\b(new|stackalloc)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\\s*\n(?=\\[)", + "begin": "(?x)\n\\b(new|stackalloc)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)?\\s*\n(?=\\[)", "beginCaptures": { "1": { "name": "keyword.other.new.cs" @@ -3339,7 +3131,7 @@ ] }, "parameter": { - "match": "(?x)\n(?:(?:\\b(ref|params|out|in|this)\\b)\\s+)?\n(?\n (?:\n (?:ref\\s+)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)", + "match": "(?x)\n(?:(?:\\b(ref|params|out|in|this)\\b)\\s+)?\n(?\n (?:\n (?:ref\\s+)? # ref return\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s+\n(\\g)", "captures": { "1": { "name": "storage.modifier.cs" @@ -3438,7 +3230,7 @@ ] }, "query-expression": { - "begin": "(?x)\n\\b(from)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\n\\s+(\\g)\\b\\s*\n\\b(in)\\b\\s*", + "begin": "(?x)\n\\b(from)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)?\n\\s+(\\g)\\b\\s*\n\\b(in)\\b\\s*", "beginCaptures": { "1": { "name": "keyword.query.from.cs" @@ -3530,7 +3322,7 @@ ] }, "join-clause": { - "begin": "(?x)\n\\b(join)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\n\\s+(\\g)\\b\\s*\n\\b(in)\\b\\s*", + "begin": "(?x)\n\\b(join)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)?\n\\s+(\\g)\\b\\s*\n\\b(in)\\b\\s*", "beginCaptures": { "1": { "name": "keyword.query.join.cs" @@ -3800,7 +3592,7 @@ ] }, "lambda-parameter": { - "match": "(?x)\n(?:\\b(ref|out|in)\\b)?\\s*\n(?:(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+)?\n(\\g)\\b\\s*\n(?=[,)])", + "match": "(?x)\n(?:\\b(ref|out|in)\\b)?\\s*\n(?:(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\\s+)?\n(\\g)\\b\\s*\n(?=[,)])", "captures": { "1": { "name": "storage.modifier.cs" @@ -3880,7 +3672,7 @@ ] }, "tuple-element": { - "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\n(?:(?\\g)\\b)?", + "match": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s*\\[(?:\\s*,\\s*)*\\]\\s*)* # array suffix?\n )\n)\n(?:(?\\g)\\b)?", "captures": { "1": { "patterns": [ diff --git a/lib/vscode/extensions/css-language-features/package.json b/lib/vscode/extensions/css-language-features/package.json index 7d80d7512237..207041a60943 100644 --- a/lib/vscode/extensions/css-language-features/package.json +++ b/lib/vscode/extensions/css-language-features/package.json @@ -853,7 +853,7 @@ "vscode-uri": "^3.0.2" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/css-language-features/server/package.json b/lib/vscode/extensions/css-language-features/server/package.json index ce3020c21a4e..62238cf5c40c 100644 --- a/lib/vscode/extensions/css-language-features/server/package.json +++ b/lib/vscode/extensions/css-language-features/server/package.json @@ -10,13 +10,13 @@ "main": "./out/node/cssServerMain", "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^5.1.1", + "vscode-css-languageservice": "^5.1.3", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.2" }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/lib/vscode/extensions/css-language-features/server/src/test/completion.test.ts b/lib/vscode/extensions/css-language-features/server/src/test/completion.test.ts index 55eb64d9789c..ec336448a504 100644 --- a/lib/vscode/extensions/css-language-features/server/src/test/completion.test.ts +++ b/lib/vscode/extensions/css-language-features/server/src/test/completion.test.ts @@ -24,10 +24,10 @@ suite('Completions', () => { return completion.label === expected.label; }); - assert.equal(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`); + assert.strictEqual(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`); let match = matches[0]; if (expected.resultText && TextEdit.is(match.textEdit)) { - assert.equal(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText); + assert.strictEqual(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText); } }; @@ -50,7 +50,7 @@ suite('Completions', () => { let list = await cssLanguageService.doComplete2(document, position, stylesheet, context); if (expected.count) { - assert.equal(list.items.length, expected.count); + assert.strictEqual(list.items.length, expected.count); } if (expected.items) { for (let item of expected.items) { diff --git a/lib/vscode/extensions/css-language-features/server/src/test/links.test.ts b/lib/vscode/extensions/css-language-features/server/src/test/links.test.ts index f67b5e92f25c..bc4c6db6477a 100644 --- a/lib/vscode/extensions/css-language-features/server/src/test/links.test.ts +++ b/lib/vscode/extensions/css-language-features/server/src/test/links.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'mocha'; import * as assert from 'assert'; -import { URI } from 'vscode-uri'; +import { URI } from 'vscode-uri'; import { resolve } from 'path'; import { TextDocument, DocumentLink } from 'vscode-languageserver-types'; import { WorkspaceFolder } from 'vscode-languageserver-protocol'; @@ -26,10 +26,10 @@ suite('Links', () => { return document.offsetAt(link.range.start) === expected.offset; }); - assert.equal(matches.length, 1, `${expected.offset} should only existing once: Actual: ${links.map(l => document.offsetAt(l.range.start)).join(', ')}`); + assert.strictEqual(matches.length, 1, `${expected.offset} should only existing once: Actual: ${links.map(l => document.offsetAt(l.range.start)).join(', ')}`); let match = matches[0]; - assert.equal(document.getText(match.range), expected.value); - assert.equal(match.target, expected.target); + assert.strictEqual(document.getText(match.range), expected.value); + assert.strictEqual(match.target, expected.target); }; async function assertLinks(value: string, expected: ItemDescription[], testUri: string, workspaceFolders?: WorkspaceFolder[], lang: string = 'css'): Promise { @@ -47,7 +47,7 @@ suite('Links', () => { const stylesheet = cssLanguageService.parseStylesheet(document); let links = await cssLanguageService.findDocumentLinks2(document, stylesheet, context)!; - assert.equal(links.length, expected.length); + assert.strictEqual(links.length, expected.length); for (let item of expected) { assertLink(links, item, document); diff --git a/lib/vscode/extensions/css-language-features/server/yarn.lock b/lib/vscode/extensions/css-language-features/server/yarn.lock index 3ff39d282f66..c734be9d5baa 100644 --- a/lib/vscode/extensions/css-language-features/server/yarn.lock +++ b/lib/vscode/extensions/css-language-features/server/yarn.lock @@ -7,15 +7,15 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== - -vscode-css-languageservice@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.1.tgz#d68a22ea0b34a8356c169cafc7d32564c2ff6e87" - integrity sha512-QW0oe/g2y5E2AbVqY7FJNr2Q8uHiAHNSFpptI6xB8Y0KgzVKppOcIVrgmBNzXhFp9IswAwptkdqr8ExSJbqPkQ== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + +vscode-css-languageservice@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.3.tgz#a7b2f21ed48842af5d9a98223bcae09e33d707d5" + integrity sha512-c8xiUhrDBNG6iS92FEE+K3IWOHAqVvzsqjjLSaXHyF5Qdn/6VhUweGNjtZ2CBSfs+Vkmz7pJzLQ7Io1x5deumA== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.16.0" diff --git a/lib/vscode/extensions/css-language-features/yarn.lock b/lib/vscode/extensions/css-language-features/yarn.lock index eec3195c3f72..2e524319229d 100644 --- a/lib/vscode/extensions/css-language-features/yarn.lock +++ b/lib/vscode/extensions/css-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== balanced-match@^1.0.0: version "1.0.0" diff --git a/lib/vscode/extensions/dart/.vscodeignore b/lib/vscode/extensions/dart/.vscodeignore new file mode 100644 index 000000000000..d9011becfb64 --- /dev/null +++ b/lib/vscode/extensions/dart/.vscodeignore @@ -0,0 +1,2 @@ +build/** +cgmanifest.json diff --git a/lib/vscode/extensions/dart/cgmanifest.json b/lib/vscode/extensions/dart/cgmanifest.json new file mode 100644 index 000000000000..52c4a994d19d --- /dev/null +++ b/lib/vscode/extensions/dart/cgmanifest.json @@ -0,0 +1,46 @@ +{ + "registrations": [ + { + "component": { + "type": "git", + "git": { + "name": "dart-lang/dart-syntax-highlight", + "repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight", + "commitHash": "65f211722c85e9fdf0aa658d5663e6ccaf2ebe25" + } + }, + "licenseDetail": [ + "Copyright 2020, the Dart project authors.", + "", + "Redistribution and use in source and binary forms, with or without", + "modification, are permitted provided that the following conditions are", + "met:", + "", + " * Redistributions of source code must retain the above copyright", + " notice, this list of conditions and the following disclaimer.", + " * Redistributions in binary form must reproduce the above", + " copyright notice, this list of conditions and the following", + " disclaimer in the documentation and/or other materials provided", + " with the distribution.", + " * Neither the name of Google LLC nor the names of its", + " contributors may be used to endorse or promote products derived", + " from this software without specific prior written permission.", + "", + "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS", + "\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT", + "LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR", + "A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT", + "OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,", + "SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT", + "LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,", + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY", + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT", + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE", + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + ], + "license": "BSD", + "version": "0.0.0" + } + ], + "version": 1 +} diff --git a/lib/vscode/extensions/dart/language-configuration.json b/lib/vscode/extensions/dart/language-configuration.json new file mode 100644 index 000000000000..9d44a40ee84b --- /dev/null +++ b/lib/vscode/extensions/dart/language-configuration.json @@ -0,0 +1,29 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": [ "/*", "*/" ] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "`", "close": "`", "notIn": ["string", "comment"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["<", ">"], + ["'", "'"], + ["\"", "\""], + ["`", "`"] + ] +} diff --git a/lib/vscode/extensions/dart/package.json b/lib/vscode/extensions/dart/package.json new file mode 100644 index 000000000000..92863ffb2b7f --- /dev/null +++ b/lib/vscode/extensions/dart/package.json @@ -0,0 +1,35 @@ +{ + "name": "dart", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "0.10.x" + }, + "scripts": { + "update-grammar": "node ../node_modules/vscode-grammar-updater/bin dart-lang/dart-syntax-highlight grammars/dart.json ./syntaxes/dart.tmLanguage.json" + }, + "contributes": { + "languages": [ + { + "id": "dart", + "extensions": [ + ".dart" + ], + "aliases": [ + "Dart" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "dart", + "scopeName": "source.dart", + "path": "./syntaxes/dart.tmLanguage.json" + } + ] + } +} diff --git a/lib/vscode/extensions/dart/package.nls.json b/lib/vscode/extensions/dart/package.nls.json new file mode 100644 index 000000000000..71e6b91e93ae --- /dev/null +++ b/lib/vscode/extensions/dart/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": "Dart Language Basics", + "description": "Provides syntax highlighting & bracket matching in Dart files." +} diff --git a/lib/vscode/extensions/dart/syntaxes/dart.tmLanguage.json b/lib/vscode/extensions/dart/syntaxes/dart.tmLanguage.json new file mode 100644 index 000000000000..fc2fae5bc4c3 --- /dev/null +++ b/lib/vscode/extensions/dart/syntaxes/dart.tmLanguage.json @@ -0,0 +1,439 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/dart-lang/dart-syntax-highlight/blob/master/grammars/dart.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/65f211722c85e9fdf0aa658d5663e6ccaf2ebe25", + "name": "Dart", + "scopeName": "source.dart", + "patterns": [ + { + "name": "meta.preprocessor.script.dart", + "match": "^(#!.*)$" + }, + { + "name": "meta.declaration.dart", + "begin": "^\\w*\\b(library|import|part of|part|export)\\b", + "beginCaptures": { + "0": { + "name": "keyword.other.import.dart" + } + }, + "end": ";", + "endCaptures": { + "0": { + "name": "punctuation.terminator.dart" + } + }, + "patterns": [ + { + "include": "#strings" + }, + { + "include": "#comments" + }, + { + "name": "keyword.other.import.dart", + "match": "\\b(as|show|hide)\\b" + } + ] + }, + { + "include": "#comments" + }, + { + "include": "#punctuation" + }, + { + "include": "#annotations" + }, + { + "include": "#keywords" + }, + { + "include": "#constants-and-special-vars" + }, + { + "include": "#strings" + } + ], + "repository": { + "dartdoc": { + "patterns": [ + { + "match": "(\\[.*?\\])", + "captures": { + "0": { + "name": "variable.name.source.dart" + } + } + }, + { + "match": "^ {4,}(?![ \\*]).*", + "captures": { + "0": { + "name": "variable.name.source.dart" + } + } + }, + { + "contentName": "variable.other.source.dart", + "begin": "```.*?$", + "end": "```" + }, + { + "match": "(`.*?`)", + "captures": { + "0": { + "name": "variable.other.source.dart" + } + } + }, + { + "match": "(`.*?`)", + "captures": { + "0": { + "name": "variable.other.source.dart" + } + } + }, + { + "match": "(\\* (( ).*))$", + "captures": { + "2": { + "name": "variable.other.source.dart" + } + } + }, + { + "match": "(\\* .*)$" + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.block.empty.dart", + "match": "/\\*\\*/", + "captures": { + "0": { + "name": "punctuation.definition.comment.dart" + } + } + }, + { + "include": "#comments-doc-oldschool" + }, + { + "include": "#comments-doc" + }, + { + "include": "#comments-inline" + } + ] + }, + "comments-doc-oldschool": { + "patterns": [ + { + "name": "comment.block.documentation.dart", + "begin": "/\\*\\*", + "end": "\\*/", + "patterns": [ + { + "include": "#comments-doc-oldschool" + }, + { + "include": "#comments-block" + }, + { + "include": "#dartdoc" + } + ] + } + ] + }, + "comments-doc": { + "patterns": [ + { + "name": "comment.block.documentation.dart", + "begin": "///", + "while": "^\\s*///", + "patterns": [ + { + "include": "#dartdoc" + } + ] + } + ] + }, + "comments-inline": { + "patterns": [ + { + "include": "#comments-block" + }, + { + "match": "((//).*)$", + "captures": { + "1": { + "name": "comment.line.double-slash.dart" + } + } + } + ] + }, + "comments-block": { + "patterns": [ + { + "name": "comment.block.dart", + "begin": "/\\*", + "end": "\\*/", + "patterns": [ + { + "include": "#comments-block" + } + ] + } + ] + }, + "annotations": { + "patterns": [ + { + "name": "storage.type.annotation.dart", + "match": "@[a-zA-Z]+" + } + ] + }, + "constants-and-special-vars": { + "patterns": [ + { + "name": "constant.language.dart", + "match": "(?)", + "captures": { + "1": { + "name": "entity.name.function.dart" + } + } + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.cast.dart", + "match": "(?>>?|~|\\^|\\||&)" + }, + { + "name": "keyword.operator.assignment.bitwise.dart", + "match": "((&|\\^|\\||<<|>>>?)=)" + }, + { + "name": "keyword.operator.closure.dart", + "match": "(=>)" + }, + { + "name": "keyword.operator.comparison.dart", + "match": "(==|!=|<=?|>=?)" + }, + { + "name": "keyword.operator.assignment.arithmetic.dart", + "match": "(([+*/%-]|\\~)=)" + }, + { + "name": "keyword.operator.assignment.dart", + "match": "(=)" + }, + { + "name": "keyword.operator.increment-decrement.dart", + "match": "(\\-\\-|\\+\\+)" + }, + { + "name": "keyword.operator.arithmetic.dart", + "match": "(\\-|\\+|\\*|\\/|\\~\\/|%)" + }, + { + "name": "keyword.operator.logical.dart", + "match": "(!|&&|\\|\\|)" + }, + { + "name": "storage.modifier.dart", + "match": "(? { let { start, end } = rangeToReplace; const startOffset = document.offsetAt(start); - const startNode = getFlatNode(rootNode, startOffset, true); + const documentText = document.getText(); + const startNode = getHtmlFlatNode(documentText, rootNode, startOffset, true); if (startNode && isOffsetInsideOpenOrCloseTag(startNode, startOffset)) { start = document.positionAt(startNode.start); const nodeEndPosition = document.positionAt(startNode.end); @@ -64,7 +65,7 @@ export async function wrapWithAbbreviation(args: any): Promise { } const endOffset = document.offsetAt(end); - const endNode = getFlatNode(rootNode, endOffset, true); + const endNode = getHtmlFlatNode(documentText, rootNode, endOffset, true); if (endNode && isOffsetInsideOpenOrCloseTag(endNode, endOffset)) { const nodeStartPosition = document.positionAt(endNode.start); start = nodeStartPosition.isBefore(start) ? nodeStartPosition : start; @@ -665,6 +666,8 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined { const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter); if (input.textToWrap) { + // escape ${ sections, fixes #122231 + input.textToWrap = input.textToWrap.map(e => e.replace(/\$\{/g, '\\\$\{')); if (input.filter && input.filter.includes('t')) { input.textToWrap = input.textToWrap.map(line => { return line.replace(trimRegex, '').trim(); diff --git a/lib/vscode/extensions/emmet/src/test/wrapWithAbbreviation.test.ts b/lib/vscode/extensions/emmet/src/test/wrapWithAbbreviation.test.ts index 020f01912913..978d154ae97f 100644 --- a/lib/vscode/extensions/emmet/src/test/wrapWithAbbreviation.test.ts +++ b/lib/vscode/extensions/emmet/src/test/wrapWithAbbreviation.test.ts @@ -13,6 +13,7 @@ const htmlContentsForBlockWrapTests = ` `; @@ -20,6 +21,7 @@ const htmlContentsForInlineWrapTests = ` `; @@ -31,6 +33,9 @@ const wrapBlockElementExpected = `
  • $hithere
  • +
    +
  • \${hithere}
  • +
    `; @@ -38,6 +43,7 @@ const wrapInlineElementExpected = ` `; @@ -49,6 +55,9 @@ const wrapSnippetExpected = `
  • $hithere
  • + +
  • \${hithere}
  • +
    `; @@ -64,6 +73,11 @@ const wrapMultiLineAbbrExpected = `
  • $hithere
  • +
      +
    • +
    • \${hithere}
    • + +
    `; @@ -77,15 +91,18 @@ const wrapInlineElementExpectedFormatFalse = `

  • $hithere
  • +

    +
  • \${hithere}
  • +

    `; suite('Tests for Wrap with Abbreviations', () => { teardown(closeAllEditors); - const multiCursors = [new Selection(2, 6, 2, 6), new Selection(3, 6, 3, 6)]; - const multiCursorsWithSelection = [new Selection(2, 2, 2, 28), new Selection(3, 2, 3, 33)]; - const multiCursorsWithFullLineSelection = [new Selection(2, 0, 2, 28), new Selection(3, 0, 4, 0)]; + const multiCursors = [new Selection(2, 6, 2, 6), new Selection(3, 6, 3, 6), new Selection(4, 6, 4, 6)]; + const multiCursorsWithSelection = [new Selection(2, 2, 2, 28), new Selection(3, 2, 3, 33), new Selection(4, 6, 4, 36)]; + const multiCursorsWithFullLineSelection = [new Selection(2, 0, 2, 28), new Selection(3, 0, 3, 33), new Selection(4, 0, 4, 36)]; const oldValueForSyntaxProfiles = workspace.getConfiguration('emmet').inspect('syntaxProfile'); @@ -202,6 +219,79 @@ suite('Tests for Wrap with Abbreviations', () => { return testWrapWithAbbreviation([new Selection(3, 2, 3, 2)], 'div', expectedContents, contents); }); + test('Wrap with abbreviation inner node in cdata', () => { + const contents = ` + +

    Test 2

    + ]]> + hello + + `; + const expectedContents = ` + +
    +

    Test 2

    +
    + ]]> + hello + + `; + return testWrapWithAbbreviation([new Selection(6, 5, 6, 5)], 'div', expectedContents, contents); + }); + + test('Wrap with abbreviation inner node in script in cdata', () => { + const contents = ` + + `; + const expectedContents = ` + + `; + return testWrapWithAbbreviation([new Selection(4, 10, 4, 10)], 'div', expectedContents, contents); + }); + + test('Wrap with abbreviation inner node in cdata one-liner', () => { + const contents = ` + + `; + // this result occurs because no selection on the open/close p tag was given + const expectedContents = ` + + `; + return testWrapWithAbbreviation([new Selection(2, 15, 2, 15)], 'div', expectedContents, contents); + }); + test('Wrap with multiline abbreviation doesnt add extra spaces', () => { // Issue #29898 const contents = ` @@ -343,11 +433,12 @@ suite('Tests for Wrap with Abbreviations', () => {
  • img
  • $hithere
  • +
  • \${hithere}
  • `; - return testWrapWithAbbreviation([new Selection(2, 2, 3, 33)], '.hello', wrapMultiLineJsxExpected, htmlContentsForBlockWrapTests, 'jsx'); + return testWrapWithAbbreviation([new Selection(2, 2, 4, 36)], '.hello', wrapMultiLineJsxExpected, htmlContentsForBlockWrapTests, 'jsx'); }); test('Wrap individual line with abbreviation uses className for jsx files', () => { @@ -359,10 +450,13 @@ suite('Tests for Wrap with Abbreviations', () => {
  • $hithere
  • +
    +
  • \${hithere}
  • +
    `; - return testWrapIndividualLinesWithAbbreviation([new Selection(2, 2, 3, 33)], '.hello$*', wrapIndividualLinesJsxExpected, htmlContentsForBlockWrapTests, 'jsx'); + return testWrapIndividualLinesWithAbbreviation([new Selection(2, 2, 4, 36)], '.hello$*', wrapIndividualLinesJsxExpected, htmlContentsForBlockWrapTests, 'jsx'); }); test('Wrap with abbreviation merge overlapping computed ranges', () => { diff --git a/lib/vscode/extensions/emmet/src/util.ts b/lib/vscode/extensions/emmet/src/util.ts index cb63f504d5d0..cb4a876bdcb3 100644 --- a/lib/vscode/extensions/emmet/src/util.ts +++ b/lib/vscode/extensions/emmet/src/util.ts @@ -381,13 +381,19 @@ export function getHtmlFlatNode(documentText: string, root: FlatNode | undefined // If the currentNode is a script one, first set up its subtree and then find HTML node. if (currentNode.name === 'script' && currentNode.children.length === 0) { - setUpScriptNodeSubtree(documentText, currentNode); - currentNode = getFlatNode(currentNode, offset, includeNodeBoundary) ?? currentNode; + const scriptNodeBody = setupScriptNodeSubtree(documentText, currentNode); + if (scriptNodeBody) { + currentNode = getHtmlFlatNode(scriptNodeBody, currentNode, offset, includeNodeBoundary) ?? currentNode; + } + } + else if (currentNode.type === 'cdata') { + const cdataBody = setupCdataNodeSubtree(documentText, currentNode); + currentNode = getHtmlFlatNode(cdataBody, currentNode, offset, includeNodeBoundary) ?? currentNode; } return currentNode; } -export function setUpScriptNodeSubtree(documentText: string, scriptNode: HtmlFlatNode): void { +export function setupScriptNodeSubtree(documentText: string, scriptNode: HtmlFlatNode): string { const isTemplateScript = scriptNode.name === 'script' && (scriptNode.attributes && scriptNode.attributes.some(x => x.name.toString() === 'type' @@ -403,7 +409,25 @@ export function setUpScriptNodeSubtree(documentText: string, scriptNode: HtmlFla scriptNode.children.push(child); child.parent = scriptNode; }); + return scriptBodyText; } + return ''; +} + +export function setupCdataNodeSubtree(documentText: string, cdataNode: HtmlFlatNode): string { + // blank out the rest of the document and generate the subtree. + const cdataStart = ''; + const startToUse = cdataNode.start + cdataStart.length; + const endToUse = cdataNode.end - cdataEnd.length; + const beforePadding = ' '.repeat(startToUse); + const cdataBody = beforePadding + documentText.substring(startToUse, endToUse); + const innerRoot: HtmlFlatNode = parse(cdataBody); + innerRoot.children.forEach(child => { + cdataNode.children.push(child); + child.parent = cdataNode; + }); + return cdataBody; } export function isOffsetInsideOpenOrCloseTag(node: FlatNode, offset: number): boolean { diff --git a/lib/vscode/extensions/emmet/yarn.lock b/lib/vscode/extensions/emmet/yarn.lock index 3578363bdc5b..2d35a2c78aba 100644 --- a/lib/vscode/extensions/emmet/yarn.lock +++ b/lib/vscode/extensions/emmet/yarn.lock @@ -53,10 +53,10 @@ resolved "https://registry.yarnpkg.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz#46cffea119a0a003312a21c2d9b5628cb5fcd442" integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= -"@types/node@^12.19.9": - version "12.20.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.7.tgz#1cb61fd0c85cb87e728c43107b5fd82b69bc9ef8" - integrity sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA== +"@types/node@14.x": + version "14.17.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.1.tgz#5e07e0cb2ff793aa7a1b41deae76221e6166049f" + integrity sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw== emmet@^2.3.0: version "2.3.4" @@ -77,9 +77,9 @@ jsonc-parser@^2.3.0: integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== vscode-emmet-helper@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.6.2.tgz#777b471a7851ba0ca8e4151533be7f92511f39b0" - integrity sha512-SkL1WjZZsA+bfTo52QH4PgqXCQAJSqzOmJtAY3rOl17MKbY6iJhVv2T26PshjmUnHoXnXMNa7PcLMCS75RsQDQ== + version "2.6.4" + resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.6.4.tgz#bea47f17649bba26b412f3d1fac18aaee43eba25" + integrity sha512-fP0nunW1RUWEKGf4gqiYLOVNFFGXSRHjCl0pikxtwCFlty8WwimM+RBJ5o0aIiwerrYD30HqeaVyvDW027Sseg== dependencies: emmet "^2.3.0" jsonc-parser "^2.3.0" diff --git a/lib/vscode/extensions/extension-editing/package.json b/lib/vscode/extensions/extension-editing/package.json index d60c8420514f..710f3d7be706 100644 --- a/lib/vscode/extensions/extension-editing/package.json +++ b/lib/vscode/extensions/extension-editing/package.json @@ -69,7 +69,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/extension-editing/yarn.lock b/lib/vscode/extensions/extension-editing/yarn.lock index 3275b1f2ad6b..01c08624126a 100644 --- a/lib/vscode/extensions/extension-editing/yarn.lock +++ b/lib/vscode/extensions/extension-editing/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/node@^6.0.46": version "6.0.78" diff --git a/lib/vscode/extensions/git/package.json b/lib/vscode/extensions/git/package.json index 7d5e81b25510..55f83ea6a896 100644 --- a/lib/vscode/extensions/git/package.json +++ b/lib/vscode/extensions/git/package.json @@ -2385,7 +2385,7 @@ "@types/byline": "4.2.31", "@types/file-type": "^5.2.1", "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/which": "^1.0.28" }, "repository": { diff --git a/lib/vscode/extensions/git/src/test/git.test.ts b/lib/vscode/extensions/git/src/test/git.test.ts index 4c74e26321b9..c0d0d2003c96 100644 --- a/lib/vscode/extensions/git/src/test/git.test.ts +++ b/lib/vscode/extensions/git/src/test/git.test.ts @@ -12,19 +12,19 @@ suite('git', () => { suite('GitStatusParser', () => { test('empty parser', () => { const parser = new GitStatusParser(); - assert.deepEqual(parser.status, []); + assert.deepStrictEqual(parser.status, []); }); test('empty parser 2', () => { const parser = new GitStatusParser(); parser.update(''); - assert.deepEqual(parser.status, []); + assert.deepStrictEqual(parser.status, []); }); test('simple', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' } ]); }); @@ -34,7 +34,7 @@ suite('git', () => { parser.update('?? file.txt\0'); parser.update('?? file2.txt\0'); parser.update('?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -51,7 +51,7 @@ suite('git', () => { parser.update(''); parser.update('?? file3.txt\0'); parser.update(''); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -61,7 +61,7 @@ suite('git', () => { test('combined', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -72,7 +72,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0?? file2'); parser.update('.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -83,7 +83,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('?? file.txt'); parser.update('\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -94,7 +94,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('?? file.txt\0?? file2.txt\0?? file3.txt'); parser.update('\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: undefined, x: '?', y: '?' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -104,7 +104,7 @@ suite('git', () => { test('rename', () => { const parser = new GitStatusParser(); parser.update('R newfile.txt\0file.txt\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -115,7 +115,7 @@ suite('git', () => { const parser = new GitStatusParser(); parser.update('R newfile.txt\0fil'); parser.update('e.txt\0?? file2.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' }, { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -127,7 +127,7 @@ suite('git', () => { parser.update('?? file2.txt\0R new'); parser.update('file.txt\0fil'); parser.update('e.txt\0?? file3.txt\0'); - assert.deepEqual(parser.status, [ + assert.deepStrictEqual(parser.status, [ { path: 'file2.txt', rename: undefined, x: '?', y: '?' }, { path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' }, { path: 'file3.txt', rename: undefined, x: '?', y: '?' } @@ -137,7 +137,7 @@ suite('git', () => { suite('parseGitmodules', () => { test('empty', () => { - assert.deepEqual(parseGitmodules(''), []); + assert.deepStrictEqual(parseGitmodules(''), []); }); test('sample', () => { @@ -146,7 +146,7 @@ suite('git', () => { url = https://github.com/gabime/spdlog.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' } ]); }); @@ -166,7 +166,7 @@ suite('git', () => { url = https://github.com/gabime/spdlog4.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }, { name: 'deps/spdlog2', path: 'deps/spdlog2', url: 'https://github.com/gabime/spdlog.git' }, { name: 'deps/spdlog3', path: 'deps/spdlog3', url: 'https://github.com/gabime/spdlog.git' }, @@ -180,7 +180,7 @@ suite('git', () => { url = https://github.com/gabime/spdlog.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' } ]); }); @@ -191,7 +191,7 @@ suite('git', () => { url=https://github.com/gabime/spdlog.git `; - assert.deepEqual(parseGitmodules(sample), [ + assert.deepStrictEqual(parseGitmodules(sample), [ { name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' } ]); }); @@ -207,7 +207,7 @@ john.doe@mail.com 8e5a374372b8393906c7e380dbb09349c5385554 This is a commit message.\x00`; - assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ + assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: ['8e5a374372b8393906c7e380dbb09349c5385554'], @@ -227,7 +227,7 @@ john.doe@mail.com 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 This is a commit message.\x00`; - assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ + assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'], @@ -247,7 +247,7 @@ john.doe@mail.com This is a commit message.\x00`; - assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ + assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: [], @@ -275,7 +275,7 @@ This is a commit message.\x00`; const output = parseLsTree(input); - assert.deepEqual(output, [ + assert.deepStrictEqual(output, [ { mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', size: '-', file: '.vscode' }, { mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', size: '491570', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, { mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', size: '764420', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, @@ -307,7 +307,7 @@ This is a commit message.\x00`; const output = parseLsFiles(input); - assert.deepEqual(output, [ + assert.deepStrictEqual(output, [ { mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' }, { mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, { mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, @@ -325,72 +325,72 @@ This is a commit message.\x00`; suite('splitInChunks', () => { test('unit tests', function () { - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 6)], [['hello'], ['there'], ['cool'], ['stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 10)], [['hello', 'there'], ['cool', 'stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 12)], [['hello', 'there'], ['cool', 'stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 14)], [['hello', 'there', 'cool'], ['stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['hello', 'there', 'cool', 'stuff'], 2000)], [['hello', 'there', 'cool', 'stuff']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 1)], [['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 2)], [['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 3)], [['0', '01'], ['012'], ['0', '01'], ['012'], ['0', '01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 4)], [['0', '01'], ['012', '0'], ['01'], ['012', '0'], ['01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 5)], [['0', '01'], ['012', '0'], ['01', '012'], ['0', '01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 6)], [['0', '01', '012'], ['0', '01', '012'], ['0', '01', '012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 7)], [['0', '01', '012', '0'], ['01', '012', '0'], ['01', '012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 8)], [['0', '01', '012', '0'], ['01', '012', '0', '01'], ['012']] ); - assert.deepEqual( + assert.deepStrictEqual( [...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 9)], [['0', '01', '012', '0', '01'], ['012', '0', '01', '012']] ); diff --git a/lib/vscode/extensions/git/src/test/smoke.test.ts b/lib/vscode/extensions/git/src/test/smoke.test.ts index ed6a5d504a6f..2ba3da99aa67 100644 --- a/lib/vscode/extensions/git/src/test/smoke.test.ts +++ b/lib/vscode/extensions/git/src/test/smoke.test.ts @@ -59,8 +59,8 @@ suite('git smoke test', function () { await eventToPromise(git.onDidOpenRepository); } - assert.equal(git.repositories.length, 1); - assert.equal(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd); + assert.strictEqual(git.repositories.length, 1); + assert.strictEqual(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd); repository = git.repositories[0]; }); @@ -72,7 +72,7 @@ suite('git smoke test', function () { await type(appjs, ' world'); await appjs.save(); await repository.status(); - assert.equal(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges.length, 1); repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED); fs.writeFileSync(file('newfile.txt'), ''); @@ -80,7 +80,7 @@ suite('git smoke test', function () { await type(newfile, 'hey there'); await newfile.save(); await repository.status(); - assert.equal(repository.state.workingTreeChanges.length, 2); + assert.strictEqual(repository.state.workingTreeChanges.length, 2); repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.uri.path && r.status === Status.UNTRACKED); }); @@ -90,7 +90,7 @@ suite('git smoke test', function () { await commands.executeCommand('git.openChange', appjs); assert(window.activeTextEditor); - assert.equal(window.activeTextEditor!.document.uri.path, appjs.path); + assert.strictEqual(window.activeTextEditor!.document.uri.path, appjs.path); // TODO: how do we really know this is a diff editor? }); @@ -100,13 +100,13 @@ suite('git smoke test', function () { const newfile = uri('newfile.txt'); await commands.executeCommand('git.stage', appjs); - assert.equal(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges.length, 1); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); - assert.equal(repository.state.indexChanges.length, 1); + assert.strictEqual(repository.state.indexChanges.length, 1); repository.state.indexChanges.some(r => r.uri.path === appjs.path && r.status === Status.INDEX_MODIFIED); await commands.executeCommand('git.unstage', appjs); - assert.equal(repository.state.workingTreeChanges.length, 2); + assert.strictEqual(repository.state.workingTreeChanges.length, 2); repository.state.workingTreeChanges.some(r => r.uri.path === appjs.path && r.status === Status.MODIFIED); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); }); @@ -117,14 +117,14 @@ suite('git smoke test', function () { await commands.executeCommand('git.stage', appjs); await repository.commit('second commit'); - assert.equal(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges.length, 1); repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); - assert.equal(repository.state.indexChanges.length, 0); + assert.strictEqual(repository.state.indexChanges.length, 0); await commands.executeCommand('git.stageAll', appjs); await repository.commit('third commit'); - assert.equal(repository.state.workingTreeChanges.length, 0); - assert.equal(repository.state.indexChanges.length, 0); + assert.strictEqual(repository.state.workingTreeChanges.length, 0); + assert.strictEqual(repository.state.indexChanges.length, 0); }); test('rename/delete conflict', async function () { diff --git a/lib/vscode/extensions/git/yarn.lock b/lib/vscode/extensions/git/yarn.lock index c964f284a3af..3853a2d2c046 100644 --- a/lib/vscode/extensions/git/yarn.lock +++ b/lib/vscode/extensions/git/yarn.lock @@ -26,10 +26,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/which@^1.0.28": version "1.0.28" diff --git a/lib/vscode/extensions/github-authentication/package.json b/lib/vscode/extensions/github-authentication/package.json index 4a18a5bd0a85..2064e28dedc8 100644 --- a/lib/vscode/extensions/github-authentication/package.json +++ b/lib/vscode/extensions/github-authentication/package.json @@ -4,7 +4,7 @@ "description": "%description%", "publisher": "vscode", "license": "MIT", - "version": "0.0.1", + "version": "0.0.2", "engines": { "vscode": "^1.41.0" }, @@ -19,7 +19,8 @@ "web" ], "activationEvents": [ - "onAuthenticationRequest:github" + "onAuthenticationRequest:github", + "onAuthenticationRequest:github-enterprise" ], "capabilities": { "virtualWorkspaces": true, @@ -31,7 +32,14 @@ "commands": [ { "command": "github.provide-token", - "title": "Manually Provide Token" + "title": "Manually Provide Token", + "category": "GitHub" + }, + { + "command": "github-enterprise.provide-token", + "title": "Manually Provide Token", + "category": "GitHub Enterprise" + } ], "menus": { @@ -39,6 +47,10 @@ { "command": "github.provide-token", "when": "false" + }, + { + "command": "github-enterprise.provide-token", + "when": "false" } ] }, @@ -46,8 +58,21 @@ { "label": "GitHub", "id": "github" + }, + { + "label": "GitHub Enterprise", + "id": "github-enterprise" } - ] + ], + "configuration": { + "title": "GitHub Enterprise Authentication Provider", + "properties": { + "github-enterprise.uri" : { + "type": "string", + "description": "URI of your GitHub Enterprise Instanace" + } + } + } }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", @@ -67,7 +92,7 @@ "vscode-tas-client": "^0.1.22" }, "devDependencies": { - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/node-fetch": "^2.5.7", "@types/uuid": "8.0.0" }, diff --git a/lib/vscode/extensions/github-authentication/src/common/keychain.ts b/lib/vscode/extensions/github-authentication/src/common/keychain.ts index d1dc0a32a640..04d64682c989 100644 --- a/lib/vscode/extensions/github-authentication/src/common/keychain.ts +++ b/lib/vscode/extensions/github-authentication/src/common/keychain.ts @@ -28,13 +28,11 @@ export type Keytar = { deletePassword: typeof keytarType['deletePassword']; }; -const SERVICE_ID = `github.auth`; - export class Keychain { - constructor(private context: vscode.ExtensionContext) { } + constructor(private context: vscode.ExtensionContext, private serviceId: string) { } async setToken(token: string): Promise { try { - return await this.context.secrets.store(SERVICE_ID, token); + return await this.context.secrets.store(this.serviceId, token); } catch (e) { // Ignore Logger.error(`Setting token failed: ${e}`); @@ -48,7 +46,7 @@ export class Keychain { async getToken(): Promise { try { - return await this.context.secrets.get(SERVICE_ID); + return await this.context.secrets.get(this.serviceId); } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); @@ -58,7 +56,7 @@ export class Keychain { async deleteToken(): Promise { try { - return await this.context.secrets.delete(SERVICE_ID); + return await this.context.secrets.delete(this.serviceId); } catch (e) { // Ignore Logger.error(`Deleting token failed: ${e}`); diff --git a/lib/vscode/extensions/github-authentication/src/extension.ts b/lib/vscode/extensions/github-authentication/src/extension.ts index 253f3aec579e..bc7a086f6927 100644 --- a/lib/vscode/extensions/github-authentication/src/extension.ts +++ b/lib/vscode/extensions/github-authentication/src/extension.ts @@ -4,9 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { GitHubAuthenticationProvider, onDidChangeSessions } from './github'; -import { uriHandler } from './githubServer'; -import Logger from './common/logger'; +import { GitHubAuthenticationProvider, AuthProviderType } from './github'; import TelemetryReporter from 'vscode-extension-telemetry'; import { createExperimentationService, ExperimentationTelemetry } from './experimentationService'; @@ -17,74 +15,13 @@ export async function activate(context: vscode.ExtensionContext) { const experimentationService = await createExperimentationService(context, telemetryReporter); await experimentationService.initialFetch; - context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); - const loginService = new GitHubAuthenticationProvider(context, telemetryReporter); - - await loginService.initialize(context); - - context.subscriptions.push(vscode.commands.registerCommand('github.provide-token', () => { - return loginService.manuallyProvideToken(); - })); - - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('github', 'GitHub', { - onDidChangeSessions: onDidChangeSessions.event, - getSessions: (scopes?: string[]) => loginService.getSessions(scopes), - createSession: async (scopeList: string[]) => { - try { - /* __GDPR__ - "login" : { } - */ - telemetryReporter.sendTelemetryEvent('login'); - - const session = await loginService.createSession(scopeList.sort().join(' ')); - Logger.info('Login success!'); - onDidChangeSessions.fire({ added: [session], removed: [], changed: [] }); - return session; - } catch (e) { - // If login was cancelled, do not notify user. - if (e.message === 'Cancelled') { - /* __GDPR__ - "loginCancelled" : { } - */ - telemetryReporter.sendTelemetryEvent('loginCancelled'); - throw e; - } - - /* __GDPR__ - "loginFailed" : { } - */ - telemetryReporter.sendTelemetryEvent('loginFailed'); - - vscode.window.showErrorMessage(`Sign in failed: ${e}`); - Logger.error(e); - throw e; - } - }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logout" : { } - */ - telemetryReporter.sendTelemetryEvent('logout'); - - const session = await loginService.removeSession(id); - if (session) { - onDidChangeSessions.fire({ added: [], removed: [session], changed: [] }); - } - } catch (e) { - /* __GDPR__ - "logoutFailed" : { } - */ - telemetryReporter.sendTelemetryEvent('logoutFailed'); - - vscode.window.showErrorMessage(`Sign out failed: ${e}`); - Logger.error(e); - throw e; - } - } - }, { supportsMultipleAccounts: false })); - - return; + [ + AuthProviderType.github, + AuthProviderType['github-enterprise'] + ].forEach(async type => { + const loginService = new GitHubAuthenticationProvider(context, type, telemetryReporter); + await loginService.initialize(); + }); } // this method is called when your extension is deactivated diff --git a/lib/vscode/extensions/github-authentication/src/github.ts b/lib/vscode/extensions/github-authentication/src/github.ts index 4d3573a7e643..fa676744079c 100644 --- a/lib/vscode/extensions/github-authentication/src/github.ts +++ b/lib/vscode/extensions/github-authentication/src/github.ts @@ -6,13 +6,11 @@ import * as vscode from 'vscode'; import { v4 as uuid } from 'uuid'; import { Keychain } from './common/keychain'; -import { GitHubServer, NETWORK_ERROR } from './githubServer'; +import { GitHubServer, uriHandler, NETWORK_ERROR } from './githubServer'; import Logger from './common/logger'; import { arrayEquals } from './common/utils'; import { ExperimentationTelemetry } from './experimentationService'; -export const onDidChangeSessions = new vscode.EventEmitter(); - interface SessionData { id: string; account?: { @@ -24,18 +22,29 @@ interface SessionData { accessToken: string; } -export class GitHubAuthenticationProvider { +export enum AuthProviderType { + github = 'github', + 'github-enterprise' = 'github-enterprise' +} + + +export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider { private _sessions: vscode.AuthenticationSession[] = []; + private _sessionChangeEmitter = new vscode.EventEmitter(); private _githubServer: GitHubServer; private _keychain: Keychain; - constructor(context: vscode.ExtensionContext, telemetryReporter: ExperimentationTelemetry) { - this._keychain = new Keychain(context); - this._githubServer = new GitHubServer(telemetryReporter); + constructor(private context: vscode.ExtensionContext, private type: AuthProviderType, private telemetryReporter: ExperimentationTelemetry) { + this._keychain = new Keychain(context, `${type}.auth`); + this._githubServer = new GitHubServer(type, telemetryReporter); } - public async initialize(context: vscode.ExtensionContext): Promise { + get onDidChangeSessions() { + return this._sessionChangeEmitter.event; + } + + public async initialize(): Promise { try { this._sessions = await this.readSessions(); await this.verifySessions(); @@ -43,7 +52,17 @@ export class GitHubAuthenticationProvider { // Ignore, network request failed } - context.subscriptions.push(context.secrets.onDidChange(() => this.checkForUpdates())); + let friendlyName = 'GitHub'; + if (this.type === AuthProviderType.github) { + this.context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); + } + if (this.type === AuthProviderType['github-enterprise']) { + friendlyName = 'GitHub Enterprise'; + } + + this.context.subscriptions.push(vscode.commands.registerCommand(`${this.type}.provide-token`, () => this.manuallyProvideToken())); + this.context.subscriptions.push(vscode.authentication.registerAuthenticationProvider(this.type, friendlyName, this, { supportsMultipleAccounts: false })); + this.context.subscriptions.push(this.context.secrets.onDidChange(() => this.checkForUpdates())); } async getSessions(scopes?: string[]): Promise { @@ -52,12 +71,21 @@ export class GitHubAuthenticationProvider { : this._sessions; } + private async afterTokenLoad(token: string): Promise { + if (this.type === AuthProviderType.github) { + this._githubServer.checkIsEdu(token); + } + if (this.type === AuthProviderType['github-enterprise']) { + this._githubServer.checkEnterpriseVersion(token); + } + } + private async verifySessions(): Promise { const verifiedSessions: vscode.AuthenticationSession[] = []; const verificationPromises = this._sessions.map(async session => { try { await this._githubServer.getUserInfo(session.accessToken); - this._githubServer.checkIsEdu(session.accessToken); + this.afterTokenLoad(session.accessToken); verifiedSessions.push(session); } catch (e) { // Remove sessions that return unauthorized response @@ -112,7 +140,7 @@ export class GitHubAuthenticationProvider { }); if (added.length || removed.length) { - onDidChangeSessions.fire({ added, removed, changed: [] }); + this._sessionChangeEmitter.fire({ added, removed, changed: [] }); } } @@ -163,12 +191,41 @@ export class GitHubAuthenticationProvider { return this._sessions; } - public async createSession(scopes: string): Promise { - const token = await this._githubServer.login(scopes); - const session = await this.tokenToSession(token, scopes.split(' ')); - this._githubServer.checkIsEdu(token); - await this.setToken(session); - return session; + public async createSession(scopes: string[]): Promise { + try { + /* __GDPR__ + "login" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('login'); + + const token = await this._githubServer.login(scopes.join(' ')); + this.afterTokenLoad(token); + const session = await this.tokenToSession(token, scopes); + await this.setToken(session); + this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + + Logger.info('Login success!'); + + return session; + } catch (e) { + // If login was cancelled, do not notify user. + if (e.message === 'Cancelled') { + /* __GDPR__ + "loginCancelled" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('loginCancelled'); + throw e; + } + + /* __GDPR__ + "loginFailed" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('loginFailed'); + + vscode.window.showErrorMessage(`Sign in failed: ${e}`); + Logger.error(e); + throw e; + } } public async manuallyProvideToken(): Promise { @@ -196,18 +253,33 @@ export class GitHubAuthenticationProvider { await this.storeSessions(); } - public async removeSession(id: string): Promise { - Logger.info(`Logging out of ${id}`); - const sessionIndex = this._sessions.findIndex(session => session.id === id); - let session: vscode.AuthenticationSession | undefined; - if (sessionIndex > -1) { - session = this._sessions[sessionIndex]; - this._sessions.splice(sessionIndex, 1); - } else { - Logger.error('Session not found'); - } + public async removeSession(id: string) { + try { + /* __GDPR__ + "logout" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('logout'); - await this.storeSessions(); - return session; + Logger.info(`Logging out of ${id}`); + const sessionIndex = this._sessions.findIndex(session => session.id === id); + if (sessionIndex > -1) { + const session = this._sessions[sessionIndex]; + this._sessions.splice(sessionIndex, 1); + this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); + } else { + Logger.error('Session not found'); + } + + await this.storeSessions(); + } catch (e) { + /* __GDPR__ + "logoutFailed" : { } + */ + this.telemetryReporter?.sendTelemetryEvent('logoutFailed'); + + vscode.window.showErrorMessage(`Sign out failed: ${e}`); + Logger.error(e); + throw e; + } } } diff --git a/lib/vscode/extensions/github-authentication/src/githubServer.ts b/lib/vscode/extensions/github-authentication/src/githubServer.ts index 4fdafe0ccb12..33ee47e63162 100644 --- a/lib/vscode/extensions/github-authentication/src/githubServer.ts +++ b/lib/vscode/extensions/github-authentication/src/githubServer.ts @@ -10,6 +10,7 @@ import { v4 as uuid } from 'uuid'; import { PromiseAdapter, promiseFromEvent } from './common/utils'; import Logger from './common/logger'; import { ExperimentationTelemetry } from './experimentationService'; +import { AuthProviderType } from './github'; const localize = nls.loadMessageBundle(); @@ -26,10 +27,6 @@ class UriEventHandler extends vscode.EventEmitter implements vscode. export const uriHandler = new UriEventHandler; -const onDidManuallyProvideToken = new vscode.EventEmitter(); - - - function parseQuery(uri: vscode.Uri) { return uri.query.split('&').reduce((prev: any, current) => { const queryString = current.split('='); @@ -40,20 +37,21 @@ function parseQuery(uri: vscode.Uri) { export class GitHubServer { private _statusBarItem: vscode.StatusBarItem | undefined; + private _onDidManuallyProvideToken = new vscode.EventEmitter(); private _pendingStates = new Map(); private _codeExchangePromises = new Map, cancel: vscode.EventEmitter }>(); - constructor(private readonly telemetryReporter: ExperimentationTelemetry) { } + constructor(private type: AuthProviderType, private readonly telemetryReporter: ExperimentationTelemetry) { } private isTestEnvironment(url: vscode.Uri): boolean { - return /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:'); + return this.type === AuthProviderType['github-enterprise'] || /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:'); } // TODO@joaomoreno TODO@RMacfarlane private async isNoCorsEnvironment(): Promise { - const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - return uri.scheme === 'https' && /^vscode\./.test(uri.authority); + const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); + return (uri.scheme === 'https' && /^vscode\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); } public async login(scopes: string): Promise { @@ -105,7 +103,7 @@ export class GitHubServer { return Promise.race([ codeExchangePromise.promise, - promiseFromEvent(onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { + promiseFromEvent(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { if (!token) { reject('Cancelled'); } else { @@ -165,11 +163,31 @@ export class GitHubServer { } }; + private getServerUri(path?: string) { + const apiUri = this.type === AuthProviderType['github-enterprise'] + ? vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get('uri') || '', true) + : vscode.Uri.parse('https://api.github.com'); + + if (!path) { + path = ''; + } + if (this.type === AuthProviderType['github-enterprise']) { + path = '/api/v3' + path; + } + + return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`); + } + private updateStatusBarItem(isStart?: boolean) { if (isStart && !this._statusBarItem) { - this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - this._statusBarItem.text = localize('signingIn', "$(mark-github) Signing in to github.com..."); - this._statusBarItem.command = 'github.provide-token'; + this._statusBarItem = vscode.window.createStatusBarItem('status.git.signIn', vscode.StatusBarAlignment.Left); + this._statusBarItem.name = localize('status.git.signIn.name', "GitHub Sign-in"); + this._statusBarItem.text = this.type === AuthProviderType.github + ? localize('signingIn', "$(mark-github) Signing in to github.com...") + : localize('signingInEnterprise', "$(mark-github) Signing in to {0}...", this.getServerUri().authority); + this._statusBarItem.command = this.type === AuthProviderType.github + ? 'github.provide-token' + : 'github-enterprise.provide-token'; this._statusBarItem.show(); } @@ -182,7 +200,7 @@ export class GitHubServer { public async manuallyProvideToken() { const uriOrToken = await vscode.window.showInputBox({ prompt: 'Token', ignoreFocusOut: true }); if (!uriOrToken) { - onDidManuallyProvideToken.fire(undefined); + this._onDidManuallyProvideToken.fire(undefined); return; } @@ -193,14 +211,14 @@ export class GitHubServer { } catch (e) { // If it doesn't look like a URI, treat it as a token. Logger.info('Treating input as token'); - onDidManuallyProvideToken.fire(uriOrToken); + this._onDidManuallyProvideToken.fire(uriOrToken); } } private async getScopes(token: string): Promise { try { Logger.info('Getting token scopes...'); - const result = await fetch('https://api.github.com', { + const result = await fetch(this.getServerUri('/').toString(), { headers: { Authorization: `token ${token}`, 'User-Agent': 'Visual-Studio-Code' @@ -224,7 +242,7 @@ export class GitHubServer { let result: Response; try { Logger.info('Getting user info...'); - result = await fetch('https://api.github.com/user', { + result = await fetch(this.getServerUri('/user').toString(), { headers: { Authorization: `token ${token}`, 'User-Agent': 'Visual-Studio-Code' @@ -280,6 +298,34 @@ export class GitHubServer { } catch (e) { // No-op } + } + + public async checkEnterpriseVersion(token: string): Promise { + try { + + const result = await fetch(this.getServerUri('/meta').toString(), { + headers: { + Authorization: `token ${token}`, + 'User-Agent': 'Visual-Studio-Code' + } + }); + if (!result.ok) { + return; + } + + const json: { verifiable_password_authentication: boolean, installed_version: string } = await result.json(); + + /* __GDPR__ + "ghe-session" : { + "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryReporter.sendTelemetryEvent('ghe-session', { + version: json.installed_version + }); + } catch { + // No-op + } } } diff --git a/lib/vscode/extensions/github-authentication/src/typings/ref.d.ts b/lib/vscode/extensions/github-authentication/src/typings/ref.d.ts index dfd710c06cf3..c9849d48e083 100644 --- a/lib/vscode/extensions/github-authentication/src/typings/ref.d.ts +++ b/lib/vscode/extensions/github-authentication/src/typings/ref.d.ts @@ -5,6 +5,3 @@ /// /// -/// - -// NOTE@coder: add keytar typeref diff --git a/lib/vscode/extensions/github-authentication/yarn.lock b/lib/vscode/extensions/github-authentication/yarn.lock index 8095e5745237..090dc94dcb76 100644 --- a/lib/vscode/extensions/github-authentication/yarn.lock +++ b/lib/vscode/extensions/github-authentication/yarn.lock @@ -15,10 +15,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/uuid@8.0.0": version "8.0.0" diff --git a/lib/vscode/extensions/github/package.json b/lib/vscode/extensions/github/package.json index 8f67314b459d..f21080ab6f17 100644 --- a/lib/vscode/extensions/github/package.json +++ b/lib/vscode/extensions/github/package.json @@ -70,7 +70,7 @@ "vscode-nls": "^4.1.2" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/github/yarn.lock b/lib/vscode/extensions/github/yarn.lock index 05a0b4cf6f9f..94f7a26da9d9 100644 --- a/lib/vscode/extensions/github/yarn.lock +++ b/lib/vscode/extensions/github/yarn.lock @@ -99,16 +99,16 @@ dependencies: "@types/node" ">= 8" +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + "@types/node@>= 8": version "14.0.23" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== - before-after-hook@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" diff --git a/lib/vscode/extensions/grunt/package.json b/lib/vscode/extensions/grunt/package.json index d7fbd50e57ac..c784b1ed83a1 100644 --- a/lib/vscode/extensions/grunt/package.json +++ b/lib/vscode/extensions/grunt/package.json @@ -20,7 +20,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "main": "./out/main", "activationEvents": [ @@ -39,13 +39,13 @@ "title": "Grunt", "properties": { "grunt.autoDetect": { - "scope": "resource", + "scope": "application", "type": "string", "enum": [ "off", "on" ], - "default": "on", + "default": "off", "description": "%config.grunt.autoDetect%" } } diff --git a/lib/vscode/extensions/grunt/package.nls.json b/lib/vscode/extensions/grunt/package.nls.json index 873de038882f..789a579cc545 100644 --- a/lib/vscode/extensions/grunt/package.nls.json +++ b/lib/vscode/extensions/grunt/package.nls.json @@ -1,7 +1,7 @@ { "description": "Extension to add Grunt capabilities to VS Code.", "displayName": "Grunt support for VS Code", - "config.grunt.autoDetect": "Controls whether auto detection of Grunt tasks is on or off. Default is on.", + "config.grunt.autoDetect": "Controls enablement of Grunt task detection. Grunt task detection can cause files in any open workspace to be executed.", "grunt.taskDefinition.type.description": "The Grunt task to customize.", "grunt.taskDefinition.args.description": "Command line arguments to pass to the grunt task", "grunt.taskDefinition.file.description": "The Grunt file that provides the task. Can be omitted." diff --git a/lib/vscode/extensions/grunt/yarn.lock b/lib/vscode/extensions/grunt/yarn.lock index 687f15f481e5..f7a30098ef45 100644 --- a/lib/vscode/extensions/grunt/yarn.lock +++ b/lib/vscode/extensions/grunt/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== vscode-nls@^4.0.0: version "4.0.0" diff --git a/lib/vscode/extensions/gulp/package.json b/lib/vscode/extensions/gulp/package.json index 2fb8d5831234..21800e96cfb6 100644 --- a/lib/vscode/extensions/gulp/package.json +++ b/lib/vscode/extensions/gulp/package.json @@ -20,7 +20,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "main": "./out/main", "activationEvents": [ @@ -39,13 +39,13 @@ "title": "Gulp", "properties": { "gulp.autoDetect": { - "scope": "resource", + "scope": "application", "type": "string", "enum": [ "off", "on" ], - "default": "on", + "default": "off", "description": "%config.gulp.autoDetect%" } } diff --git a/lib/vscode/extensions/gulp/package.nls.json b/lib/vscode/extensions/gulp/package.nls.json index a87dbe1dc85b..222ad3a87ec3 100644 --- a/lib/vscode/extensions/gulp/package.nls.json +++ b/lib/vscode/extensions/gulp/package.nls.json @@ -1,7 +1,7 @@ { "description": "Extension to add Gulp capabilities to VSCode.", "displayName": "Gulp support for VSCode", - "config.gulp.autoDetect": "Controls whether auto detection of Gulp tasks is on or off. Default is on.", + "config.gulp.autoDetect": "Controls enablement of Gulp task detection. Gulp task detection can cause files in any open workspace to be executed.", "gulp.taskDefinition.type.description": "The Gulp task to customize.", "gulp.taskDefinition.file.description": "The Gulp file that provides the task. Can be omitted." } diff --git a/lib/vscode/extensions/gulp/yarn.lock b/lib/vscode/extensions/gulp/yarn.lock index 687f15f481e5..f7a30098ef45 100644 --- a/lib/vscode/extensions/gulp/yarn.lock +++ b/lib/vscode/extensions/gulp/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== vscode-nls@^4.0.0: version "4.0.0" diff --git a/lib/vscode/extensions/html-language-features/package.json b/lib/vscode/extensions/html-language-features/package.json index 9fa6bc314e52..3c6a539c0797 100644 --- a/lib/vscode/extensions/html-language-features/package.json +++ b/lib/vscode/extensions/html-language-features/package.json @@ -245,7 +245,7 @@ "vscode-nls": "^5.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/html-language-features/server/package.json b/lib/vscode/extensions/html-language-features/server/package.json index fdb292853138..ab5b93df5c76 100644 --- a/lib/vscode/extensions/html-language-features/server/package.json +++ b/lib/vscode/extensions/html-language-features/server/package.json @@ -9,8 +9,8 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^5.1.1", - "vscode-html-languageservice": "^4.0.3", + "vscode-css-languageservice": "^5.1.3", + "vscode-html-languageservice": "^4.0.4", "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-nls": "^5.0.0", @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", diff --git a/lib/vscode/extensions/html-language-features/server/src/htmlServer.ts b/lib/vscode/extensions/html-language-features/server/src/htmlServer.ts index 7bc6a444bea7..e153cf7fa91f 100644 --- a/lib/vscode/extensions/html-language-features/server/src/htmlServer.ts +++ b/lib/vscode/extensions/html-language-features/server/src/htmlServer.ts @@ -5,9 +5,9 @@ import { Connection, TextDocuments, InitializeParams, InitializeResult, RequestType, - DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities, + DocumentRangeFormattingRequest, Disposable, TextDocumentPositionParams, ServerCapabilities, ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, - DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType, RequestType0 + DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType, RequestType0, DocumentFormattingRequest, FormattingOptions, TextEdit } from 'vscode-languageserver'; import { getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation, @@ -152,7 +152,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, hoverProvider: true, documentHighlightProvider: true, - documentRangeFormattingProvider: initializationOptions?.provideFormatter === true, + documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, + documentFormattingProvider: params.initializationOptions?.provideFormatter === true, documentLinkProvider: { resolveProvider: false }, documentSymbolProvider: true, definitionProvider: true, @@ -188,7 +189,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } }); - let formatterRegistration: Thenable | null = null; + let formatterRegistrations: Thenable[] | null = null; // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration((change) => { @@ -200,13 +201,16 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) if (dynamicFormatterRegistration) { const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable; if (enableFormatter) { - if (!formatterRegistration) { - const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }]; - formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }); + if (!formatterRegistrations) { + const documentSelector = [{ language: 'html' }, { language: 'handlebars' }]; + formatterRegistrations = [ + connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }), + connection.client.register(DocumentFormattingRequest.type, { documentSelector }) + ]; } - } else if (formatterRegistration) { - formatterRegistration.then(r => r.dispose()); - formatterRegistration = null; + } else if (formatterRegistrations) { + formatterRegistrations.forEach(p => p.then(r => r.dispose())); + formatterRegistrations = null; } } }); @@ -381,21 +385,27 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token); }); - connection.onDocumentRangeFormatting(async (formatParams, token) => { - return runSafe(async () => { - const document = documents.get(formatParams.textDocument.uri); - if (document) { - let settings = await getDocumentSettings(document, () => true); - if (!settings) { - settings = globalSettings; - } - const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || ''; - const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) }; - - return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes); + async function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): Promise { + const document = documents.get(textDocument.uri); + if (document) { + let settings = await getDocumentSettings(document, () => true); + if (!settings) { + settings = globalSettings; } - return []; - }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || ''; + const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) }; + + return format(languageModes, document, range ?? getFullRange(document), options, settings, enabledModes); + } + return []; + } + + connection.onDocumentRangeFormatting((formatParams, token) => { + return runSafe(() => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + }); + + connection.onDocumentFormatting((formatParams, token) => { + return runSafe(() => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token); }); connection.onDocumentLinks((documentLinkParam, token) => { @@ -561,3 +571,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // Listen on the connection connection.listen(); } + +function getFullRange(document: TextDocument): Range { + return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); +} diff --git a/lib/vscode/extensions/html-language-features/server/src/modes/javascriptMode.ts b/lib/vscode/extensions/html-language-features/server/src/modes/javascriptMode.ts index 01e0b381d112..e6571c5bac0a 100644 --- a/lib/vscode/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/lib/vscode/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -127,7 +127,6 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); const jsLanguageService = await host.getLanguageService(jsDocument); - // @ts-expect-error until 4.3 protocol update let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined); if (details) { item.detail = ts.displayPartsToString(details.displayParts); diff --git a/lib/vscode/extensions/html-language-features/server/src/test/completions.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/completions.test.ts index 0371a2f74cd0..aaf3cb5ec6b8 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/completions.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/completions.test.ts @@ -23,24 +23,24 @@ export function assertCompletion(completions: CompletionList, expected: ItemDesc return completion.label === expected.label; }); if (expected.notAvailable) { - assert.equal(matches.length, 0, `${expected.label} should not existing is results`); + assert.strictEqual(matches.length, 0, `${expected.label} should not existing is results`); return; } - assert.equal(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`); + assert.strictEqual(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`); let match = matches[0]; if (expected.documentation) { - assert.equal(match.documentation, expected.documentation); + assert.strictEqual(match.documentation, expected.documentation); } if (expected.kind) { - assert.equal(match.kind, expected.kind); + assert.strictEqual(match.kind, expected.kind); } if (expected.resultText && match.textEdit) { const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText); - assert.equal(TextDocument.applyEdits(document, [edit]), expected.resultText); + assert.strictEqual(TextDocument.applyEdits(document, [edit]), expected.resultText); } if (expected.command) { - assert.deepEqual(match.command, expected.command); + assert.deepStrictEqual(match.command, expected.command); } } @@ -65,7 +65,7 @@ export async function testCompletionFor(value: string, expected: { count?: numbe let list = await mode.doComplete!(document, position, context); if (expected.count) { - assert.equal(list.items.length, expected.count); + assert.strictEqual(list.items.length, expected.count); } if (expected.items) { for (let item of expected.items) { diff --git a/lib/vscode/extensions/html-language-features/server/src/test/documentContext.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/documentContext.test.ts index 612c0063c058..29ba68809723 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/documentContext.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/documentContext.test.ts @@ -12,9 +12,9 @@ suite('HTML Document Context', () => { const rootFolders = [{ name: '', uri: 'file:///users/test/' }]; let context = getDocumentContext(docURI, rootFolders); - assert.equal(context.resolveReference('/', docURI), 'file:///users/test/'); - assert.equal(context.resolveReference('/message.html', docURI), 'file:///users/test/message.html'); - assert.equal(context.resolveReference('message.html', docURI), 'file:///users/test/folder/message.html'); - assert.equal(context.resolveReference('message.html', 'file:///users/test/'), 'file:///users/test/message.html'); + assert.strictEqual(context.resolveReference('/', docURI), 'file:///users/test/'); + assert.strictEqual(context.resolveReference('/message.html', docURI), 'file:///users/test/message.html'); + assert.strictEqual(context.resolveReference('message.html', docURI), 'file:///users/test/folder/message.html'); + assert.strictEqual(context.resolveReference('message.html', 'file:///users/test/'), 'file:///users/test/message.html'); }); -}); \ No newline at end of file +}); diff --git a/lib/vscode/extensions/html-language-features/server/src/test/embedded.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/embedded.test.ts index 525d5a59c112..005ecf0864c3 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/embedded.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/embedded.test.ts @@ -23,7 +23,7 @@ suite('HTML Embedded Support', () => { const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); const languageId = docRegions.getLanguageAtPosition(position); - assert.equal(languageId, expectedLanguageId); + assert.strictEqual(languageId, expectedLanguageId); } function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void { @@ -31,7 +31,7 @@ suite('HTML Embedded Support', () => { const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); const content = docRegions.getEmbeddedDocument(languageId); - assert.equal(content.getText(), expectedContent); + assert.strictEqual(content.getText(), expectedContent); } test('Styles', function (): any { diff --git a/lib/vscode/extensions/html-language-features/server/src/test/folding.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/folding.test.ts index 693a74420f08..7bf90bb10d54 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/folding.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/folding.test.ts @@ -30,7 +30,7 @@ async function assertRanges(lines: string[], expected: ExpectedIndentRange[], me actualRanges[i] = r(actual[i].startLine, actual[i].endLine, actual[i].kind); } actualRanges = actualRanges.sort((r1, r2) => r1.startLine - r2.startLine); - assert.deepEqual(actualRanges, expected, message); + assert.deepStrictEqual(actualRanges, expected, message); } function r(startLine: number, endLine: number, kind?: string): ExpectedIndentRange { diff --git a/lib/vscode/extensions/html-language-features/server/src/test/formatting.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/formatting.test.ts index d7c922c736a6..107001c7c007 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/formatting.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/formatting.test.ts @@ -41,7 +41,7 @@ suite('HTML Embedded Formatting', () => { let result = await format(languageModes, document, range, formatOptions, undefined, { css: true, javascript: true }); let actual = TextDocument.applyEdits(document, result); - assert.equal(actual, expected, message); + assert.strictEqual(actual, expected, message); } async function assertFormatWithFixture(fixtureName: string, expectedPath: string, options?: any, formatOptions?: FormattingOptions): Promise { diff --git a/lib/vscode/extensions/html-language-features/server/src/test/rename.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/rename.test.ts index 2705b56fdf87..edebe68d7b26 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/rename.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/rename.test.ts @@ -34,7 +34,7 @@ async function testRename(value: string, newName: string, expectedDocContent: st } const newDocContent = TextDocument.applyEdits(document, edits); - assert.equal(newDocContent, expectedDocContent, `Expected: ${expectedDocContent}\nActual: ${newDocContent}`); + assert.strictEqual(newDocContent, expectedDocContent, `Expected: ${expectedDocContent}\nActual: ${newDocContent}`); } else { assert.fail('should have javascriptMode but no') } diff --git a/lib/vscode/extensions/html-language-features/server/src/test/selectionRanges.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/selectionRanges.test.ts index 9f9937bcaf73..c5166c7bb453 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/selectionRanges.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/selectionRanges.test.ts @@ -5,7 +5,7 @@ import 'mocha'; import * as assert from 'assert'; -import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange} from '../modes/languageModes'; +import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange } from '../modes/languageModes'; import { getSelectionRanges } from '../modes/selectionRanges'; import { getNodeFSRequestService } from '../node/nodeFs'; @@ -23,7 +23,7 @@ async function assertRanges(content: string, expected: (number | string)[][]): P const document = TextDocument.create('test://foo.html', 'html', 1, content); const actualRanges = await getSelectionRanges(languageModes, document, [document.positionAt(offset)]); - assert.equal(actualRanges.length, 1); + assert.strictEqual(actualRanges.length, 1); const offsetPairs: [number, string][] = []; let curr: SelectionRange | undefined = actualRanges[0]; while (curr) { @@ -32,7 +32,7 @@ async function assertRanges(content: string, expected: (number | string)[][]): P } message += `${JSON.stringify(offsetPairs)}\n but should give:\n${JSON.stringify(expected)}\n`; - assert.deepEqual(offsetPairs, expected, message); + assert.deepStrictEqual(offsetPairs, expected, message); } suite('HTML SelectionRange', () => { diff --git a/lib/vscode/extensions/html-language-features/server/src/test/semanticTokens.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/semanticTokens.test.ts index 548ce7d84586..3cf242df18f1 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/semanticTokens.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/semanticTokens.test.ts @@ -40,7 +40,7 @@ async function assertTokens(lines: string[], expected: ExpectedToken[], ranges?: lastLine = line; lastCharacter = character; } - assert.deepEqual(actualRanges, expected, message); + assert.deepStrictEqual(actualRanges, expected, message); } function t(startLine: number, character: number, length: number, tokenClassifiction: string): ExpectedToken { diff --git a/lib/vscode/extensions/html-language-features/server/src/test/words.test.ts b/lib/vscode/extensions/html-language-features/server/src/test/words.test.ts index e094bfb4251b..f86ae969c666 100644 --- a/lib/vscode/extensions/html-language-features/server/src/test/words.test.ts +++ b/lib/vscode/extensions/html-language-features/server/src/test/words.test.ts @@ -16,7 +16,7 @@ suite('HTML Words', () => { let actualRange = words.getWordAtText(value, offset, wordRegex); assert(actualRange.start <= offset); assert(actualRange.start + actualRange.length >= offset); - assert.equal(value.substr(actualRange.start, actualRange.length), expected); + assert.strictEqual(value.substr(actualRange.start, actualRange.length), expected); } @@ -41,4 +41,4 @@ suite('HTML Words', () => { assertWord('console.log("hello");\n\r |var x1 = new F(a, b);', 'var'); }); -}); \ No newline at end of file +}); diff --git a/lib/vscode/extensions/html-language-features/server/yarn.lock b/lib/vscode/extensions/html-language-features/server/yarn.lock index ca035d1c79b3..5a578b7ea202 100644 --- a/lib/vscode/extensions/html-language-features/server/yarn.lock +++ b/lib/vscode/extensions/html-language-features/server/yarn.lock @@ -7,25 +7,25 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== -vscode-css-languageservice@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.1.tgz#d68a22ea0b34a8356c169cafc7d32564c2ff6e87" - integrity sha512-QW0oe/g2y5E2AbVqY7FJNr2Q8uHiAHNSFpptI6xB8Y0KgzVKppOcIVrgmBNzXhFp9IswAwptkdqr8ExSJbqPkQ== +vscode-css-languageservice@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.3.tgz#a7b2f21ed48842af5d9a98223bcae09e33d707d5" + integrity sha512-c8xiUhrDBNG6iS92FEE+K3IWOHAqVvzsqjjLSaXHyF5Qdn/6VhUweGNjtZ2CBSfs+Vkmz7pJzLQ7Io1x5deumA== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.16.0" vscode-nls "^5.0.0" vscode-uri "^3.0.2" -vscode-html-languageservice@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.0.3.tgz#3b7e7d3cfee75d47da0181dd638b5f459456a913" - integrity sha512-34KPIgRHVInT+TiFNmfiPFDrUAOOLuySNP2h0pMxBu1ObAbSixSqB3BMQFxIHz9hrGd3X0DEvi5YkobDxs4rWw== +vscode-html-languageservice@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.0.4.tgz#f171f663e83037c8a5c5f4552a6b4771c1a9f017" + integrity sha512-WHXpLfj5NlCAgppDa6n5gQjW1YTTt72MVs0lkkuGQwTb5Sfdq8UhMjLDT82MuzqwV0QvmSBWlUbreGodzXleLg== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.16.0" diff --git a/lib/vscode/extensions/html-language-features/yarn.lock b/lib/vscode/extensions/html-language-features/yarn.lock index f538d16e7d46..d14954d241fd 100644 --- a/lib/vscode/extensions/html-language-features/yarn.lock +++ b/lib/vscode/extensions/html-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== applicationinsights@1.7.4: version "1.7.4" diff --git a/lib/vscode/extensions/image-preview/src/binarySizeStatusBarEntry.ts b/lib/vscode/extensions/image-preview/src/binarySizeStatusBarEntry.ts index 0c983d37cd21..ce375fc19b80 100644 --- a/lib/vscode/extensions/image-preview/src/binarySizeStatusBarEntry.ts +++ b/lib/vscode/extensions/image-preview/src/binarySizeStatusBarEntry.ts @@ -39,12 +39,7 @@ class BinarySize { export class BinarySizeStatusBarEntry extends PreviewStatusBarEntry { constructor() { - super({ - id: 'imagePreview.binarySize', - name: localize('sizeStatusBar.name', "Image Binary Size"), - alignment: vscode.StatusBarAlignment.Right, - priority: 100, - }); + super('status.imagePreview.binarySize', localize('sizeStatusBar.name', "Image Binary Size"), vscode.StatusBarAlignment.Right, 100); } public show(owner: string, size: number | undefined) { diff --git a/lib/vscode/extensions/image-preview/src/ownedStatusBarEntry.ts b/lib/vscode/extensions/image-preview/src/ownedStatusBarEntry.ts index 51c9e25503cf..31165f67d69d 100644 --- a/lib/vscode/extensions/image-preview/src/ownedStatusBarEntry.ts +++ b/lib/vscode/extensions/image-preview/src/ownedStatusBarEntry.ts @@ -11,9 +11,10 @@ export abstract class PreviewStatusBarEntry extends Disposable { protected readonly entry: vscode.StatusBarItem; - constructor(options: vscode.StatusBarItemOptions) { + constructor(id: string, name: string, alignment: vscode.StatusBarAlignment, priority: number) { super(); - this.entry = this._register(vscode.window.createStatusBarItem(options)); + this.entry = this._register(vscode.window.createStatusBarItem(id, alignment, priority)); + this.entry.name = name; } protected showItem(owner: string, text: string) { diff --git a/lib/vscode/extensions/image-preview/src/sizeStatusBarEntry.ts b/lib/vscode/extensions/image-preview/src/sizeStatusBarEntry.ts index e74eea0fe603..68e5c34d2325 100644 --- a/lib/vscode/extensions/image-preview/src/sizeStatusBarEntry.ts +++ b/lib/vscode/extensions/image-preview/src/sizeStatusBarEntry.ts @@ -12,12 +12,7 @@ const localize = nls.loadMessageBundle(); export class SizeStatusBarEntry extends PreviewStatusBarEntry { constructor() { - super({ - id: 'imagePreview.size', - name: localize('sizeStatusBar.name', "Image Size"), - alignment: vscode.StatusBarAlignment.Right, - priority: 101 /* to the left of editor status (100) */, - }); + super('status.imagePreview.size', localize('sizeStatusBar.name', "Image Size"), vscode.StatusBarAlignment.Right, 101 /* to the left of editor status (100) */); } public show(owner: string, text: string) { diff --git a/lib/vscode/extensions/image-preview/src/zoomStatusBarEntry.ts b/lib/vscode/extensions/image-preview/src/zoomStatusBarEntry.ts index 18adc19d6d2b..a4fcdc2b604c 100644 --- a/lib/vscode/extensions/image-preview/src/zoomStatusBarEntry.ts +++ b/lib/vscode/extensions/image-preview/src/zoomStatusBarEntry.ts @@ -19,12 +19,7 @@ export class ZoomStatusBarEntry extends OwnedStatusBarEntry { public readonly onDidChangeScale = this._onDidChangeScale.event; constructor() { - super({ - id: 'imagePreview.zoom', - name: localize('zoomStatusBar.name', "Image Zoom"), - alignment: vscode.StatusBarAlignment.Right, - priority: 102 /* to the left of editor size entry (101) */, - }); + super('status.imagePreview.zoom', localize('zoomStatusBar.name', "Image Zoom"), vscode.StatusBarAlignment.Right, 102 /* to the left of editor size entry (101) */); this._register(vscode.commands.registerCommand(selectZoomLevelCommandId, async () => { type MyPickItem = vscode.QuickPickItem & { scale: Scale }; diff --git a/lib/vscode/extensions/jake/package.json b/lib/vscode/extensions/jake/package.json index ce19974e1057..244d12d96e56 100644 --- a/lib/vscode/extensions/jake/package.json +++ b/lib/vscode/extensions/jake/package.json @@ -20,7 +20,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "main": "./out/main", "activationEvents": [ @@ -39,13 +39,13 @@ "title": "Jake", "properties": { "jake.autoDetect": { - "scope": "resource", + "scope": "application", "type": "string", "enum": [ "off", "on" ], - "default": "on", + "default": "off", "description": "%config.jake.autoDetect%" } } diff --git a/lib/vscode/extensions/jake/package.nls.json b/lib/vscode/extensions/jake/package.nls.json index c2dfb3c3818b..e82030efcb81 100644 --- a/lib/vscode/extensions/jake/package.nls.json +++ b/lib/vscode/extensions/jake/package.nls.json @@ -3,5 +3,5 @@ "displayName": "Jake support for VS Code", "jake.taskDefinition.type.description": "The Jake task to customize.", "jake.taskDefinition.file.description": "The Jake file that provides the task. Can be omitted.", - "config.jake.autoDetect": "Controls whether auto detection of Jake tasks is on or off. Default is on." + "config.jake.autoDetect": "Controls enablement of Jake task detection. Jake task detection can cause files in any open workspace to be executed." } diff --git a/lib/vscode/extensions/jake/yarn.lock b/lib/vscode/extensions/jake/yarn.lock index 687f15f481e5..f7a30098ef45 100644 --- a/lib/vscode/extensions/jake/yarn.lock +++ b/lib/vscode/extensions/jake/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== vscode-nls@^4.0.0: version "4.0.0" diff --git a/lib/vscode/extensions/json-language-features/client/src/jsonClient.ts b/lib/vscode/extensions/json-language-features/client/src/jsonClient.ts index c925a9181f61..611327188efc 100644 --- a/lib/vscode/extensions/json-language-features/client/src/jsonClient.ts +++ b/lib/vscode/extensions/json-language-features/client/src/jsonClient.ts @@ -101,12 +101,8 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua const documentSelector = ['json', 'jsonc']; - const schemaResolutionErrorStatusBarItem = window.createStatusBarItem({ - id: 'status.json.resolveError', - name: localize('json.resolveError', "JSON: Schema Resolution Error"), - alignment: StatusBarAlignment.Right, - priority: 0, - }); + const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0); + schemaResolutionErrorStatusBarItem.name = localize('json.resolveError', "JSON: Schema Resolution Error"); schemaResolutionErrorStatusBarItem.text = '$(alert)'; toDispose.push(schemaResolutionErrorStatusBarItem); diff --git a/lib/vscode/extensions/json-language-features/package.json b/lib/vscode/extensions/json-language-features/package.json index e854c07569a7..4b832e4ebb6f 100644 --- a/lib/vscode/extensions/json-language-features/package.json +++ b/lib/vscode/extensions/json-language-features/package.json @@ -140,7 +140,7 @@ "vscode-nls": "^5.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/json-language-features/server/package.json b/lib/vscode/extensions/json-language-features/server/package.json index 2d841f89a75e..49926f97faed 100644 --- a/lib/vscode/extensions/json-language-features/server/package.json +++ b/lib/vscode/extensions/json-language-features/server/package.json @@ -14,13 +14,13 @@ "dependencies": { "jsonc-parser": "^3.0.0", "request-light": "^0.4.0", - "vscode-json-languageservice": "^4.1.3", + "vscode-json-languageservice": "^4.1.4", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.2" }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/lib/vscode/extensions/json-language-features/server/src/jsonServer.ts b/lib/vscode/extensions/json-language-features/server/src/jsonServer.ts index 0266d536c265..bb3538a0c985 100644 --- a/lib/vscode/extensions/json-language-features/server/src/jsonServer.ts +++ b/lib/vscode/extensions/json-language-features/server/src/jsonServer.ts @@ -6,7 +6,7 @@ import { Connection, TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType, - DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit + DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions } from 'vscode-languageserver'; import { formatError, runSafe, runSafeAsync } from './utils/runner'; @@ -138,6 +138,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) hoverProvider: true, documentSymbolProvider: true, documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, + documentFormattingProvider: params.initializationOptions?.provideFormatter === true, colorProvider: {}, foldingRangeProvider: true, selectionRangeProvider: true, @@ -206,7 +207,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = undefined; let schemaAssociations: ISchemaAssociations | SchemaConfiguration[] | undefined = undefined; - let formatterRegistration: Thenable | null = null; + let formatterRegistrations: Thenable[] | null = null; // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration((change) => { @@ -224,12 +225,16 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) if (dynamicFormatterRegistration) { const enableFormatter = settings && settings.json && settings.json.format && settings.json.format.enable; if (enableFormatter) { - if (!formatterRegistration) { - formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector: [{ language: 'json' }, { language: 'jsonc' }] }); + if (!formatterRegistrations) { + const documentSelector = [{ language: 'json' }, { language: 'jsonc' }]; + formatterRegistrations = [ + connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }), + connection.client.register(DocumentFormattingRequest.type, { documentSelector }) + ]; } - } else if (formatterRegistration) { - formatterRegistration.then(r => r.dispose()); - formatterRegistration = null; + } else if (formatterRegistrations) { + formatterRegistrations.forEach(p => p.then(r => r.dispose())); + formatterRegistrations = null; } } }); @@ -420,19 +425,25 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); }); - connection.onDocumentRangeFormatting((formatParams, token) => { - return runSafe(() => { - const document = documents.get(formatParams.textDocument.uri); - if (document) { - const edits = languageService.format(document, formatParams.range, formatParams.options); - if (edits.length > formatterMaxNumberOfEdits) { - const newText = TextDocument.applyEdits(document, edits); - return [TextEdit.replace(Range.create(Position.create(0, 0), document.positionAt(document.getText().length)), newText)]; - } - return edits; + function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): TextEdit[] { + const document = documents.get(textDocument.uri); + if (document) { + const edits = languageService.format(document, range ?? getFullRange(document), options); + if (edits.length > formatterMaxNumberOfEdits) { + const newText = TextDocument.applyEdits(document, edits); + return [TextEdit.replace(getFullRange(document), newText)]; } - return []; - }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + return edits; + } + return []; + } + + connection.onDocumentRangeFormatting((formatParams, token) => { + return runSafe(() => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + }); + + connection.onDocumentFormatting((formatParams, token) => { + return runSafe(() => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token); }); connection.onDocumentColor((params, token) => { @@ -495,3 +506,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // Listen on the connection connection.listen(); } + +function getFullRange(document: TextDocument): Range { + return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); +} diff --git a/lib/vscode/extensions/json-language-features/server/yarn.lock b/lib/vscode/extensions/json-language-features/server/yarn.lock index b754118b91d2..abe012c86ae8 100644 --- a/lib/vscode/extensions/json-language-features/server/yarn.lock +++ b/lib/vscode/extensions/json-language-features/server/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== agent-base@4: version "4.1.2" @@ -105,10 +105,10 @@ request-light@^0.4.0: https-proxy-agent "^2.2.4" vscode-nls "^4.1.2" -vscode-json-languageservice@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.3.tgz#851564e529e649c13b844f10a80ea1d9095591a9" - integrity sha512-m/wUEt4zgCNUcvGmPr1ELo+ROQNKBgASpdOOAEpcSMwYE/6GzULZ1KfBhbX9or7qnC4E0oX+wwW+lrN3EUWCgw== +vscode-json-languageservice@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.4.tgz#c83d3d812f8f17ab525724c611d8ff5e8834fc84" + integrity sha512-/UqaE58BVFdePM9l971L6xPRLlCLNk01aovf1Pp9hB/8pytmd2s9ZNEnS1JqYyQEJ1k5/fEBsWOdhQlNo4H7VA== dependencies: jsonc-parser "^3.0.0" minimatch "^3.0.4" diff --git a/lib/vscode/extensions/json-language-features/yarn.lock b/lib/vscode/extensions/json-language-features/yarn.lock index a4533a6e53cf..4ba9b7b63016 100644 --- a/lib/vscode/extensions/json-language-features/yarn.lock +++ b/lib/vscode/extensions/json-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== agent-base@4: version "4.2.1" diff --git a/lib/vscode/extensions/json/package.json b/lib/vscode/extensions/json/package.json index a4ccc12cf2da..8f197c31181c 100644 --- a/lib/vscode/extensions/json/package.json +++ b/lib/vscode/extensions/json/package.json @@ -29,7 +29,8 @@ ".ts.map", ".har", ".jslintrc", - ".jsonld" + ".jsonld", + ".ipynb" ], "filenames": [ "composer.lock", @@ -57,6 +58,8 @@ ".babelrc" ], "filenames": [ + "babel.config.json", + ".babelrc.json", ".ember-cli" ], "configuration": "./language-configuration.json" diff --git a/lib/vscode/extensions/julia/cgmanifest.json b/lib/vscode/extensions/julia/cgmanifest.json index 3b2f492cc6c3..e2f4268ed2fa 100644 --- a/lib/vscode/extensions/julia/cgmanifest.json +++ b/lib/vscode/extensions/julia/cgmanifest.json @@ -4,7 +4,7 @@ "component": { "type": "git", "git": { - "name": " JuliaEditorSupport/atom-language-julia", + "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", "commitHash": "008e02c5ec9440fa9f0ea8a891712c7238f24706" } @@ -14,4 +14,4 @@ } ], "version": 1 -} \ No newline at end of file +} diff --git a/lib/vscode/extensions/make/package.json b/lib/vscode/extensions/make/package.json index ef8209df01c8..89a5134c64de 100644 --- a/lib/vscode/extensions/make/package.json +++ b/lib/vscode/extensions/make/package.json @@ -20,6 +20,7 @@ "makefile" ], "extensions": [ + ".mak", ".mk" ], "filenames": [ diff --git a/lib/vscode/extensions/markdown-basics/cgmanifest.json b/lib/vscode/extensions/markdown-basics/cgmanifest.json index 92288d403b93..f3f0717c5adb 100644 --- a/lib/vscode/extensions/markdown-basics/cgmanifest.json +++ b/lib/vscode/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "7019b191c3ee38b6c345f3a2a843f223eb92ca1e" + "commitHash": "a612b96d62aa1ce305c4a55dc9d577316fab39da" } }, "license": "MIT", diff --git a/lib/vscode/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/lib/vscode/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index a61af0d0c060..aaa4c774b407 100644 --- a/lib/vscode/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/lib/vscode/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/7019b191c3ee38b6c345f3a2a843f223eb92ca1e", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/a612b96d62aa1ce305c4a55dc9d577316fab39da", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -63,7 +63,7 @@ "while": "(^|\\G)\\s*(>) ?" }, "fenced_code_block_css": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -96,7 +96,7 @@ ] }, "fenced_code_block_basic": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -129,7 +129,7 @@ ] }, "fenced_code_block_ini": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -162,7 +162,7 @@ ] }, "fenced_code_block_java": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -195,7 +195,7 @@ ] }, "fenced_code_block_lua": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -228,7 +228,7 @@ ] }, "fenced_code_block_makefile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -261,7 +261,7 @@ ] }, "fenced_code_block_perl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -294,7 +294,7 @@ ] }, "fenced_code_block_r": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -327,7 +327,7 @@ ] }, "fenced_code_block_ruby": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -360,7 +360,7 @@ ] }, "fenced_code_block_php": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -396,7 +396,7 @@ ] }, "fenced_code_block_sql": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -429,7 +429,7 @@ ] }, "fenced_code_block_vs_net": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -462,7 +462,7 @@ ] }, "fenced_code_block_xml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -495,7 +495,7 @@ ] }, "fenced_code_block_xsl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -528,7 +528,7 @@ ] }, "fenced_code_block_yaml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -561,7 +561,7 @@ ] }, "fenced_code_block_dosbatch": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -594,7 +594,7 @@ ] }, "fenced_code_block_clojure": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -627,7 +627,7 @@ ] }, "fenced_code_block_coffee": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -660,7 +660,7 @@ ] }, "fenced_code_block_c": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -693,7 +693,7 @@ ] }, "fenced_code_block_cpp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -726,7 +726,7 @@ ] }, "fenced_code_block_diff": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -759,7 +759,7 @@ ] }, "fenced_code_block_dockerfile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -792,7 +792,7 @@ ] }, "fenced_code_block_git_commit": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -825,7 +825,7 @@ ] }, "fenced_code_block_git_rebase": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -858,7 +858,7 @@ ] }, "fenced_code_block_go": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -891,7 +891,7 @@ ] }, "fenced_code_block_groovy": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -924,7 +924,7 @@ ] }, "fenced_code_block_pug": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -957,7 +957,7 @@ ] }, "fenced_code_block_js": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -990,7 +990,7 @@ ] }, "fenced_code_block_js_regexp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1023,7 +1023,7 @@ ] }, "fenced_code_block_json": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1056,7 +1056,7 @@ ] }, "fenced_code_block_jsonc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1089,7 +1089,7 @@ ] }, "fenced_code_block_less": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1122,7 +1122,7 @@ ] }, "fenced_code_block_objc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1155,7 +1155,7 @@ ] }, "fenced_code_block_swift": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1188,7 +1188,7 @@ ] }, "fenced_code_block_scss": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1221,7 +1221,7 @@ ] }, "fenced_code_block_perl6": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1254,7 +1254,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1287,7 +1287,7 @@ ] }, "fenced_code_block_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1320,7 +1320,7 @@ ] }, "fenced_code_block_regexp_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1353,7 +1353,7 @@ ] }, "fenced_code_block_rust": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1386,7 +1386,7 @@ ] }, "fenced_code_block_scala": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1419,7 +1419,7 @@ ] }, "fenced_code_block_shell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1452,7 +1452,7 @@ ] }, "fenced_code_block_ts": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1485,7 +1485,7 @@ ] }, "fenced_code_block_tsx": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1518,7 +1518,7 @@ ] }, "fenced_code_block_csharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1551,7 +1551,7 @@ ] }, "fenced_code_block_fsharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1584,7 +1584,7 @@ ] }, "fenced_code_block_dart": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1617,7 +1617,7 @@ ] }, "fenced_code_block_handlebars": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1650,7 +1650,7 @@ ] }, "fenced_code_block_markdown": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1683,7 +1683,7 @@ ] }, "fenced_code_block_log": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1716,7 +1716,7 @@ ] }, "fenced_code_block_erlang": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1749,7 +1749,7 @@ ] }, "fenced_code_block_elixir": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1975,7 +1975,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -1990,7 +1998,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2005,7 +2021,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2020,7 +2044,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2035,7 +2067,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" @@ -2050,7 +2090,15 @@ "name": "punctuation.definition.heading.markdown" }, "2": { - "name": "entity.name.section.markdown" + "name": "entity.name.section.markdown", + "patterns": [ + { + "include": "#inline" + }, + { + "include": "text.html.derivative" + } + ] }, "3": { "name": "punctuation.definition.heading.markdown" diff --git a/lib/vscode/extensions/markdown-language-features/media/markdown.css b/lib/vscode/extensions/markdown-language-features/media/markdown.css index edaa6f925d16..e12805e1de55 100644 --- a/lib/vscode/extensions/markdown-language-features/media/markdown.css +++ b/lib/vscode/extensions/markdown-language-features/media/markdown.css @@ -106,6 +106,13 @@ body.showEditorSelection li.code-line:hover:before { border-left: none; } +ul ul, +ul ol, +ol ul, +ol ol { + margin-bottom: 0; +} + img { max-width: 100%; max-height: 100%; @@ -152,6 +159,7 @@ h1 { table { border-collapse: collapse; + margin-bottom: 0.7em; } th { diff --git a/lib/vscode/extensions/markdown-language-features/notebook/index.ts b/lib/vscode/extensions/markdown-language-features/notebook/index.ts index 1d7c65233447..371091691b7a 100644 --- a/lib/vscode/extensions/markdown-language-features/notebook/index.ts +++ b/lib/vscode/extensions/markdown-language-features/notebook/index.ts @@ -5,35 +5,23 @@ const MarkdownIt = require('markdown-it'); -export async function activate(ctx: { - dependencies: ReadonlyArray<{ entrypoint: string }> -}) { +export function activate() { let markdownIt = new MarkdownIt({ html: true }); - // Should we load the deps before this point? - // Also could we await inside `renderMarkup`? - await Promise.all(ctx.dependencies.map(async (dep) => { - try { - const api = await import(dep.entrypoint); - if (api?.extendMarkdownIt) { - markdownIt = api.extendMarkdownIt(markdownIt); - } - } catch (e) { - console.error('Could not load markdown entryPoint', e); - } - })); - return { - renderMarkup: (context: { element: HTMLElement, content: string }) => { - const rendered = markdownIt.render(context.content); - context.element.innerHTML = rendered; + renderOutputItem: (outputInfo: { text(): string }, element: HTMLElement) => { + const rendered = markdownIt.render(outputInfo.text()); + element.innerHTML = rendered; // Insert styles into markdown preview shadow dom so that they are applied for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { - context.element.appendChild(markdownStyleNode.cloneNode(true)); + element.insertAdjacentElement('beforebegin', markdownStyleNode.cloneNode(true) as Element); } + }, + extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { + f(markdownIt); } }; } diff --git a/lib/vscode/extensions/markdown-language-features/package.json b/lib/vscode/extensions/markdown-language-features/package.json index 19d1354c2068..ec72a0d53441 100644 --- a/lib/vscode/extensions/markdown-language-features/package.json +++ b/lib/vscode/extensions/markdown-language-features/package.json @@ -40,7 +40,7 @@ } }, "contributes": { - "notebookMarkupRenderers": [ + "notebookRenderer": [ { "id": "markdownItRenderer", "displayName": "Markdown it renderer", @@ -361,7 +361,8 @@ "@types/highlight.js": "10.1.0", "@types/lodash.throttle": "^4.1.3", "@types/markdown-it": "0.0.2", - "@types/node": "^12.19.9", + "@types/node": "14.x", + "@types/vscode-webview": "^1.57.0", "lodash.throttle": "^4.1.1" }, "repository": { diff --git a/lib/vscode/extensions/markdown-language-features/preview-src/index.ts b/lib/vscode/extensions/markdown-language-features/preview-src/index.ts index 4d57d5f3a372..13f4f1f7770e 100644 --- a/lib/vscode/extensions/markdown-language-features/preview-src/index.ts +++ b/lib/vscode/extensions/markdown-language-features/preview-src/index.ts @@ -10,8 +10,6 @@ import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElem import { getSettings, getData } from './settings'; import throttle = require('lodash.throttle'); -declare let acquireVsCodeApi: any; - let scrollDisabledCount = 0; const marker = new ActiveLineMarker(); const settings = getSettings(); diff --git a/lib/vscode/extensions/markdown-language-features/preview-src/tsconfig.json b/lib/vscode/extensions/markdown-language-features/preview-src/tsconfig.json index b1bede72c17e..62af34c62f84 100644 --- a/lib/vscode/extensions/markdown-language-features/preview-src/tsconfig.json +++ b/lib/vscode/extensions/markdown-language-features/preview-src/tsconfig.json @@ -8,5 +8,10 @@ "DOM", "DOM.Iterable" ] + }, + "typeAcquisition": { + "include": [ + "@types/vscode-webview" + ] } } diff --git a/lib/vscode/extensions/markdown-language-features/src/features/preview.ts b/lib/vscode/extensions/markdown-language-features/src/features/preview.ts index 42d6f08fc072..0f16361338b5 100644 --- a/lib/vscode/extensions/markdown-language-features/src/features/preview.ts +++ b/lib/vscode/extensions/markdown-language-features/src/features/preview.ts @@ -11,8 +11,8 @@ import { Logger } from '../logger'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; -import { normalizeResource, WebviewResourceProvider } from '../util/resources'; -import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor'; +import { WebviewResourceProvider } from '../util/resources'; +import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; import { MarkdownEngine } from '../markdownEngine'; @@ -120,6 +120,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = []; private readonly _fileWatchersBySrc = new Map(); + private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); + public readonly onScroll = this._onScrollEmitter.event; constructor( webview: vscode.WebviewPanel, @@ -324,7 +326,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private onDidScrollPreview(line: number) { this.line = line; - + this._onScrollEmitter.fire({ line: this.line, uri: this._resource }); const config = this._previewConfigurations.loadAndCacheConfiguration(this._resource); if (!config.scrollEditorWithPreview) { return; @@ -336,13 +338,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } this.isScrolling = true; - const sourceLine = Math.floor(line); - const fraction = line - sourceLine; - const text = editor.document.lineAt(sourceLine).text; - const start = Math.floor(fraction * text.length); - editor.revealRange( - new vscode.Range(sourceLine, start, sourceLine + 1, 0), - vscode.TextEditorRevealType.AtTop); + scrollEditorToLine(line, editor); } } @@ -427,12 +423,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { baseRoots.push(vscode.Uri.file(path.dirname(this._resource.fsPath))); } - return baseRoots.map(root => normalizeResource(this._resource, root)); + return baseRoots; } private async onDidClickPreviewLink(href: string) { - let [hrefPath, fragment] = decodeURIComponent(href).split('#'); + let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c)); if (hrefPath[0] !== '/') { // We perviously already resolve absolute paths. @@ -460,7 +456,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { //#region WebviewResourceProvider asWebviewUri(resource: vscode.Uri) { - return this._webviewPanel.webview.asWebviewUri(normalizeResource(this._resource, resource)); + return this._webviewPanel.webview.asWebviewUri(resource); } get cspSource() { @@ -497,11 +493,13 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown webview: vscode.WebviewPanel, contentProvider: MarkdownContentProvider, previewConfigurations: MarkdownPreviewConfigurationManager, + topmostLineMonitor: TopmostLineMonitor, logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, + scrollLine?: number, ): StaticMarkdownPreview { - return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine); + return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, engine, scrollLine); } private readonly preview: MarkdownPreview; @@ -511,13 +509,15 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown resource: vscode.Uri, contentProvider: MarkdownContentProvider, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, + topmostLineMonitor: TopmostLineMonitor, logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, + scrollLine?: number, ) { super(); - - this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, { + const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined; + this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, { getAdditionalState: () => { return {}; }, openPreviewLinkToMarkdownFile: () => { /* todo */ } }, engine, contentProvider, _previewConfigurations, logger, contributionProvider)); @@ -529,6 +529,16 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown this._register(this._webviewPanel.onDidChangeViewState(e => { this._onDidChangeViewState.fire(e); })); + + this._register(this.preview.onScroll((scrollInfo) => { + topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo); + })); + + this._register(topmostLineMonitor.onDidChanged(event => { + if (this.preview.isPreviewOf(event.resource)) { + this.preview.scrollTo(event.line); + } + })); } private readonly _onDispose = this._register(new vscode.EventEmitter()); @@ -789,3 +799,18 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow } } +/** + * Change the top-most visible line of `editor` to be at `line` + */ +export function scrollEditorToLine( + line: number, + editor: vscode.TextEditor +) { + const sourceLine = Math.floor(line); + const fraction = line - sourceLine; + const text = editor.document.lineAt(sourceLine).text; + const start = Math.floor(fraction * text.length); + editor.revealRange( + new vscode.Range(sourceLine, start, sourceLine + 1, 0), + vscode.TextEditorRevealType.AtTop); +} diff --git a/lib/vscode/extensions/markdown-language-features/src/features/previewContentProvider.ts b/lib/vscode/extensions/markdown-language-features/src/features/previewContentProvider.ts index 474b1864ed7d..e2b28c092cbc 100644 --- a/lib/vscode/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/lib/vscode/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -81,7 +81,7 @@ export class MarkdownContentProvider { const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); const csp = this.getCsp(resourceProvider, sourceUri, nonce); - const body = await this.engine.render(markdownDocument); + const body = await this.engine.render(markdownDocument, resourceProvider); const html = ` diff --git a/lib/vscode/extensions/markdown-language-features/src/features/previewManager.ts b/lib/vscode/extensions/markdown-language-features/src/features/previewManager.ts index 05729e0873a7..4cf55fa9d39e 100644 --- a/lib/vscode/extensions/markdown-language-features/src/features/previewManager.ts +++ b/lib/vscode/extensions/markdown-language-features/src/features/previewManager.ts @@ -9,9 +9,10 @@ import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable, disposeAll } from '../util/dispose'; import { TopmostLineMonitor } from '../util/topmostLineMonitor'; -import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview } from './preview'; +import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview, scrollEditorToLine } from './preview'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider } from './previewContentProvider'; +import { isMarkdownFile } from '../util/file'; export interface DynamicPreviewSettings { readonly resourceColumn: vscode.ViewColumn; @@ -75,6 +76,17 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview super(); this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this)); this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this)); + + this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => { + + // When at a markdown file, apply existing scroll settings + if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) { + const line = this._topmostLineMonitor.getPreviousStaticEditorLineByUri(textEditor.document.uri); + if (line) { + scrollEditorToLine(line, textEditor); + } + } + })); } public refresh() { @@ -160,14 +172,18 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { + const lineNumber = this._topmostLineMonitor.getPreviousTextEditorLineByUri(document.uri); const preview = StaticMarkdownPreview.revive( document.uri, webview, this._contentProvider, this._previewConfigurations, + this._topmostLineMonitor, this._logger, this._contributions, - this._engine); + this._engine, + lineNumber + ); this.registerStaticPreview(preview); } @@ -175,11 +191,14 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview resource: vscode.Uri, previewSettings: DynamicPreviewSettings ): DynamicMarkdownPreview { + const activeTextEditorURI = vscode.window.activeTextEditor?.document.uri; + const scrollLine = (activeTextEditorURI?.toString() === resource.toString()) ? vscode.window.activeTextEditor?.visibleRanges[0].start.line : undefined; const preview = DynamicMarkdownPreview.create( { resource, resourceColumn: previewSettings.resourceColumn, locked: previewSettings.locked, + line: scrollLine, }, previewSettings.previewColumn, this._contentProvider, diff --git a/lib/vscode/extensions/markdown-language-features/src/markdownEngine.ts b/lib/vscode/extensions/markdown-language-features/src/markdownEngine.ts index 3d84a24ad305..346547f3da9f 100644 --- a/lib/vscode/extensions/markdown-language-features/src/markdownEngine.ts +++ b/lib/vscode/extensions/markdown-language-features/src/markdownEngine.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { MarkdownIt, Token } from 'markdown-it'; -import * as path from 'path'; import * as vscode from 'vscode'; import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions'; import { Slugifier } from './slugify'; import { SkinnyTextDocument } from './tableOfContentsProvider'; import { hash } from './util/hash'; -import { isOfScheme, MarkdownFileExtensions, Schemes } from './util/links'; +import { isOfScheme, Schemes } from './util/links'; +import { WebviewResourceProvider } from './util/resources'; const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g; @@ -62,12 +62,13 @@ export interface RenderOutput { interface RenderEnv { containingImages: { src: string }[]; + currentDocument: vscode.Uri | undefined; + resourceProvider: WebviewResourceProvider | undefined; } export class MarkdownEngine { private md?: Promise; - private currentDocument?: vscode.Uri; private _slugCount = new Map(); private _tokenCache = new TokenCache(); @@ -113,7 +114,7 @@ export class MarkdownEngine { this.addLineNumberRenderer(md, renderName); } - this.addImageStabilizer(md); + this.addImageRenderer(md); this.addFencedRenderer(md); this.addLinkNormalizer(md); this.addLinkValidator(md); @@ -138,8 +139,6 @@ export class MarkdownEngine { return cached; } - this.currentDocument = document.uri; - const tokens = this.tokenizeString(document.getText(), engine); this._tokenCache.update(document, config, tokens); return tokens; @@ -151,7 +150,7 @@ export class MarkdownEngine { return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); } - public async render(input: SkinnyTextDocument | string): Promise { + public async render(input: SkinnyTextDocument | string, resourceProvider?: WebviewResourceProvider): Promise { const config = this.getConfig(typeof input === 'string' ? undefined : input.uri); const engine = await this.getEngine(config); @@ -160,7 +159,9 @@ export class MarkdownEngine { : this.tokenizeDocument(input, config, engine); const env: RenderEnv = { - containingImages: [] + containingImages: [], + currentDocument: typeof input === 'string' ? undefined : input.uri, + resourceProvider, }; const html = engine.renderer.render(tokens, { @@ -193,12 +194,12 @@ export class MarkdownEngine { }; } - private addLineNumberRenderer(md: any, ruleName: string): void { + private addLineNumberRenderer(md: MarkdownIt, ruleName: string): void { const original = md.renderer.rules[ruleName]; - md.renderer.rules[ruleName] = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules[ruleName] = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const token = tokens[idx]; if (token.map && token.map.length) { - token.attrSet('data-line', token.map[0]); + token.attrSet('data-line', token.map[0] + ''); token.attrJoin('class', 'code-line'); } @@ -210,9 +211,9 @@ export class MarkdownEngine { }; } - private addImageStabilizer(md: any): void { + private addImageRenderer(md: MarkdownIt): void { const original = md.renderer.rules.image; - md.renderer.rules.image = (tokens: any, idx: number, options: any, env: RenderEnv, self: any) => { + md.renderer.rules.image = (tokens: Token[], idx: number, options: any, env: RenderEnv, self: any) => { const token = tokens[idx]; token.attrJoin('class', 'loading'); @@ -221,6 +222,11 @@ export class MarkdownEngine { env.containingImages?.push({ src }); const imgHash = hash(src); token.attrSet('id', `image-hash-${imgHash}`); + + if (!token.attrGet('data-src')) { + token.attrSet('src', this.toResourceUri(src, env.currentDocument, env.resourceProvider)); + token.attrSet('data-src', src); + } } if (original) { @@ -231,9 +237,9 @@ export class MarkdownEngine { }; } - private addFencedRenderer(md: any): void { + private addFencedRenderer(md: MarkdownIt): void { const original = md.renderer.rules['fenced']; - md.renderer.rules['fenced'] = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const token = tokens[idx]; if (token.map && token.map.length) { token.attrJoin('class', 'hljs'); @@ -243,7 +249,7 @@ export class MarkdownEngine { }; } - private addLinkNormalizer(md: any): void { + private addLinkNormalizer(md: MarkdownIt): void { const normalizeLink = md.normalizeLink; md.normalizeLink = (link: string) => { try { @@ -252,43 +258,6 @@ export class MarkdownEngine { return normalizeLink(vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }).toString()); } - // Support file:// links - if (isOfScheme(Schemes.file, link)) { - // Ensure link is relative by prepending `/` so that it uses the element URI - // when resolving the absolute URL - return normalizeLink('/' + link.replace(/^file:/, 'file')); - } - - // If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace - if (!/^[a-z\-]+:/i.test(link)) { - // Use a fake scheme for parsing - let uri = vscode.Uri.parse('markdown-link:' + link); - - // Relative paths should be resolved correctly inside the preview but we need to - // handle absolute paths specially (for images) to resolve them relative to the workspace root - if (uri.path[0] === '/') { - const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!); - if (root) { - const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ - fragment: uri.fragment, - query: uri.query, - }); - - // Ensure fileUri is relative by prepending `/` so that it uses the element URI - // when resolving the absolute URL - uri = vscode.Uri.parse('markdown-link:' + '/' + fileUri.toString(true).replace(/^\S+?:/, fileUri.scheme)); - } - } - - const extname = path.extname(uri.fsPath); - - if (uri.fragment && (extname === '' || MarkdownFileExtensions.includes(extname))) { - uri = uri.with({ - fragment: this.slugifier.fromHeading(uri.fragment).value - }); - } - return normalizeLink(uri.toString(true).replace(/^markdown-link:/, '')); - } } catch (e) { // noop } @@ -296,7 +265,7 @@ export class MarkdownEngine { }; } - private addLinkValidator(md: any): void { + private addLinkValidator(md: MarkdownIt): void { const validateLink = md.validateLink; md.validateLink = (link: string) => { return validateLink(link) @@ -306,9 +275,9 @@ export class MarkdownEngine { }; } - private addNamedHeaders(md: any): void { + private addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; - md.renderer.rules.heading_open = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules.heading_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const title = tokens[idx + 1].children.reduce((acc: string, t: any) => acc + t.content, ''); let slug = this.slugifier.fromHeading(title); @@ -331,12 +300,12 @@ export class MarkdownEngine { }; } - private addLinkRenderer(md: any): void { - const old_render = md.renderer.rules.link_open || ((tokens: any, idx: number, options: any, _env: any, self: any) => { + private addLinkRenderer(md: MarkdownIt): void { + const old_render = md.renderer.rules.link_open || ((tokens: Token[], idx: number, options: any, _env: any, self: any) => { return self.renderToken(tokens, idx, options); }); - md.renderer.rules.link_open = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules.link_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => { const token = tokens[idx]; const hrefIndex = token.attrIndex('href'); if (hrefIndex >= 0) { @@ -346,6 +315,50 @@ export class MarkdownEngine { return old_render(tokens, idx, options, env, self); }; } + + private toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { + try { + // Support file:// links + if (isOfScheme(Schemes.file, href)) { + const uri = vscode.Uri.parse(href); + if (resourceProvider) { + return resourceProvider.asWebviewUri(uri).toString(true); + } + // Not sure how to resolve this + return href; + } + + // If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace + if (!/^[a-z\-]+:/i.test(href)) { + // Use a fake scheme for parsing + let uri = vscode.Uri.parse('markdown-link:' + href); + + // Relative paths should be resolved correctly inside the preview but we need to + // handle absolute paths specially to resolve them relative to the workspace root + if (uri.path[0] === '/' && currentDocument) { + const root = vscode.workspace.getWorkspaceFolder(currentDocument); + if (root) { + uri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ + fragment: uri.fragment, + query: uri.query, + }); + + if (resourceProvider) { + return resourceProvider.asWebviewUri(uri).toString(true); + } else { + uri = uri.with({ scheme: 'markdown-link' }); + } + } + } + + return uri.toString(true).replace(/^markdown-link:/, ''); + } + + return href; + } catch { + return href; + } + } } async function getMarkdownOptions(md: () => MarkdownIt) { diff --git a/lib/vscode/extensions/markdown-language-features/src/test/engine.test.ts b/lib/vscode/extensions/markdown-language-features/src/test/engine.test.ts index c3eb7c850566..e398ed088ce9 100644 --- a/lib/vscode/extensions/markdown-language-features/src/test/engine.test.ts +++ b/lib/vscode/extensions/markdown-language-features/src/test/engine.test.ts @@ -37,11 +37,11 @@ suite('markdown.engine', () => { const engine = createNewMarkdownEngine(); assert.deepStrictEqual((await engine.render(input)), { html: '

    ' - + ' ' + + ' ' + ' ' - + ' ' - + ' ' - + '' + + ' ' + + ' ' + + '' + '

    \n' , containingImages: [{ src: 'img.png' }, { src: 'http://example.org/img.png' }, { src: 'img.png' }, { src: './img2.png' }], diff --git a/lib/vscode/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts b/lib/vscode/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts index e74d3b592bcb..d66cbf198b2b 100644 --- a/lib/vscode/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts +++ b/lib/vscode/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts @@ -18,7 +18,7 @@ suite('markdown.WorkspaceSymbolProvider', () => { test('Should not return anything for empty workspace', async () => { const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([])); - assert.deepEqual(await provider.provideWorkspaceSymbols(''), []); + assert.deepStrictEqual(await provider.provideWorkspaceSymbols(''), []); }); test('Should return symbols from workspace with one markdown file', async () => { diff --git a/lib/vscode/extensions/markdown-language-features/src/util/links.ts b/lib/vscode/extensions/markdown-language-features/src/util/links.ts index 3545d03a54d4..02e5aba6590b 100644 --- a/lib/vscode/extensions/markdown-language-features/src/util/links.ts +++ b/lib/vscode/extensions/markdown-language-features/src/util/links.ts @@ -14,7 +14,6 @@ export const Schemes = { data: 'data:', vscode: 'vscode:', 'vscode-insiders': 'vscode-insiders:', - 'vscode-resource': 'vscode-resource:', }; const knownSchemes = [ diff --git a/lib/vscode/extensions/markdown-language-features/src/util/resources.ts b/lib/vscode/extensions/markdown-language-features/src/util/resources.ts index 063c410b39ee..f1f2d0886ab2 100644 --- a/lib/vscode/extensions/markdown-language-features/src/util/resources.ts +++ b/lib/vscode/extensions/markdown-language-features/src/util/resources.ts @@ -11,23 +11,3 @@ export interface WebviewResourceProvider { readonly cspSource: string; } -export function normalizeResource( - base: vscode.Uri, - resource: vscode.Uri -): vscode.Uri { - // If we have a windows path and are loading a workspace with an authority, - // make sure we use a unc path with an explicit localhost authority. - // - // Otherwise, the `` rule will insert the authority into the resolved resource - // URI incorrectly. - if (base.authority && !resource.authority) { - const driveMatch = resource.path.match(/^\/(\w):\//); - if (driveMatch) { - return vscode.Uri.file(`\\\\localhost\\${driveMatch[1]}$\\${resource.fsPath.replace(/^\w:\\/, '')}`).with({ - fragment: resource.fragment, - query: resource.query - }); - } - } - return resource; -} diff --git a/lib/vscode/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/lib/vscode/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index 57395fbc691c..f2e0e2061ca6 100644 --- a/lib/vscode/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/lib/vscode/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -7,18 +7,32 @@ import * as vscode from 'vscode'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from './file'; +export interface LastScrollLocation { + readonly line: number; + readonly uri: vscode.Uri; +} + export class TopmostLineMonitor extends Disposable { private readonly pendingUpdates = new Map(); private readonly throttle = 50; + private previousTextEditorInfo = new Map(); + private previousStaticEditorInfo = new Map(); constructor() { super(); + + if (vscode.window.activeTextEditor) { + const line = getVisibleLine(vscode.window.activeTextEditor); + this.setPreviousTextEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 }); + } + this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => { if (isMarkdownFile(event.textEditor.document)) { const line = getVisibleLine(event.textEditor); if (typeof line === 'number') { this.updateLine(event.textEditor.document.uri, line); + this.setPreviousTextEditorLine({ uri: event.textEditor.document.uri, line: line }); } } })); @@ -27,7 +41,28 @@ export class TopmostLineMonitor extends Disposable { private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); public readonly onDidChanged = this._onChanged.event; - private updateLine( + public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void { + this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); + } + + public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined { + const scrollLoc = this.previousStaticEditorInfo.get(resource.toString()); + this.previousStaticEditorInfo.delete(resource.toString()); + return scrollLoc?.line; + } + + + public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void { + this.previousTextEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); + } + + public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined { + const scrollLoc = this.previousTextEditorInfo.get(resource.toString()); + this.previousTextEditorInfo.delete(resource.toString()); + return scrollLoc?.line; + } + + public updateLine( resource: vscode.Uri, line: number ) { diff --git a/lib/vscode/extensions/markdown-language-features/yarn.lock b/lib/vscode/extensions/markdown-language-features/yarn.lock index 216abe3066b9..3f1174c3684f 100644 --- a/lib/vscode/extensions/markdown-language-features/yarn.lock +++ b/lib/vscode/extensions/markdown-language-features/yarn.lock @@ -26,10 +26,15 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + +"@types/vscode-webview@^1.57.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" + integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== applicationinsights@1.7.4: version "1.7.4" diff --git a/lib/vscode/extensions/merge-conflict/package.json b/lib/vscode/extensions/merge-conflict/package.json index 74f7b848fa82..2c4719f4b71a 100644 --- a/lib/vscode/extensions/merge-conflict/package.json +++ b/lib/vscode/extensions/merge-conflict/package.json @@ -159,7 +159,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/merge-conflict/yarn.lock b/lib/vscode/extensions/merge-conflict/yarn.lock index 687f15f481e5..f7a30098ef45 100644 --- a/lib/vscode/extensions/merge-conflict/yarn.lock +++ b/lib/vscode/extensions/merge-conflict/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== vscode-nls@^4.0.0: version "4.0.0" diff --git a/lib/vscode/extensions/microsoft-authentication/package.json b/lib/vscode/extensions/microsoft-authentication/package.json index 7b47354df0fb..148b5c3625c0 100644 --- a/lib/vscode/extensions/microsoft-authentication/package.json +++ b/lib/vscode/extensions/microsoft-authentication/package.json @@ -46,7 +46,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", diff --git a/lib/vscode/extensions/microsoft-authentication/src/AADHelper.ts b/lib/vscode/extensions/microsoft-authentication/src/AADHelper.ts index 2414055c282c..77618032c12c 100644 --- a/lib/vscode/extensions/microsoft-authentication/src/AADHelper.ts +++ b/lib/vscode/extensions/microsoft-authentication/src/AADHelper.ts @@ -396,19 +396,19 @@ export class AzureActiveDirectoryService { } private getCallbackEnvironment(callbackUri: vscode.Uri): string { - if (callbackUri.authority.endsWith('.workspaces.github.com') || callbackUri.authority.endsWith('.github.dev')) { - return `${callbackUri.authority},`; + if (callbackUri.scheme !== 'https' && callbackUri.scheme !== 'http') { + return callbackUri.scheme; } switch (callbackUri.authority) { case 'online.visualstudio.com': - return 'vso,'; + return 'vso'; case 'online-ppe.core.vsengsaas.visualstudio.com': - return 'vsoppe,'; + return 'vsoppe'; case 'online.dev.core.vsengsaas.visualstudio.com': - return 'vsodev,'; + return 'vsodev'; default: - return `${callbackUri.scheme},`; + return callbackUri.authority; } } @@ -417,7 +417,7 @@ export class AzureActiveDirectoryService { const nonce = randomBytes(16).toString('base64'); const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80); const callbackEnvironment = this.getCallbackEnvironment(callbackUri); - const state = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; + const state = `${callbackEnvironment},${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; const signInUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize`; let uri = vscode.Uri.parse(signInUrl); const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); @@ -566,7 +566,8 @@ export class AzureActiveDirectoryService { }); const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - const endpoint = proxyEndpoints && proxyEndpoints['microsoft'] || `${loginEndpointUrl}${tenant}/oauth2/v2.0/token`; + const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl; + const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`; const result = await fetch(endpoint, { method: 'POST', @@ -602,7 +603,10 @@ export class AzureActiveDirectoryService { let result: Response; try { - result = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, { + const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); + const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl; + const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`; + result = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/lib/vscode/extensions/microsoft-authentication/src/typings/refs.d.ts b/lib/vscode/extensions/microsoft-authentication/src/typings/refs.d.ts index dfd710c06cf3..c9849d48e083 100644 --- a/lib/vscode/extensions/microsoft-authentication/src/typings/refs.d.ts +++ b/lib/vscode/extensions/microsoft-authentication/src/typings/refs.d.ts @@ -5,6 +5,3 @@ /// /// -/// - -// NOTE@coder: add keytar typeref diff --git a/lib/vscode/extensions/microsoft-authentication/yarn.lock b/lib/vscode/extensions/microsoft-authentication/yarn.lock index fada1d6e8bf5..54025e55a213 100644 --- a/lib/vscode/extensions/microsoft-authentication/yarn.lock +++ b/lib/vscode/extensions/microsoft-authentication/yarn.lock @@ -15,10 +15,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/randombytes@^2.0.0": version "2.0.0" diff --git a/lib/vscode/extensions/notebook-markdown-extensions/notebook/emoji.ts b/lib/vscode/extensions/notebook-markdown-extensions/notebook/emoji.ts index bf82f98ba0f7..28557b47047f 100644 --- a/lib/vscode/extensions/notebook-markdown-extensions/notebook/emoji.ts +++ b/lib/vscode/extensions/notebook-markdown-extensions/notebook/emoji.ts @@ -6,6 +6,15 @@ import type * as markdownIt from 'markdown-it'; const emoji = require('markdown-it-emoji'); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(emoji); +export async function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = await ctx.getRenderer('markdownItRenderer'); + if (!markdownItRenderer) { + throw new Error('Could not load markdownItRenderer'); + } + + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(emoji); + }); } diff --git a/lib/vscode/extensions/notebook-markdown-extensions/notebook/katex.ts b/lib/vscode/extensions/notebook-markdown-extensions/notebook/katex.ts index f862fd94940a..27d7be58be73 100644 --- a/lib/vscode/extensions/notebook-markdown-extensions/notebook/katex.ts +++ b/lib/vscode/extensions/notebook-markdown-extensions/notebook/katex.ts @@ -6,15 +6,31 @@ import type * as markdownIt from 'markdown-it'; const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css'); -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.classList.add('markdown-style'); -link.href = styleHref; +export async function activate(ctx: { + getRenderer: (id: string) => Promise +}) { + const markdownItRenderer = await ctx.getRenderer('markdownItRenderer'); + if (!markdownItRenderer) { + throw new Error('Could not load markdownItRenderer'); + } -document.head.append(link); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.classList.add('markdown-style'); + link.href = styleHref; + document.head.append(link); -const katex = require('@iktakahiro/markdown-it-katex'); + const style = document.createElement('style'); + style.classList.add('markdown-style'); + style.textContent = ` + .katex-error { + color: var(--vscode-editorError-foreground); + } + `; + document.head.append(style); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(katex); + const katex = require('@iktakahiro/markdown-it-katex'); + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(katex); + }); } diff --git a/lib/vscode/extensions/notebook-markdown-extensions/package.json b/lib/vscode/extensions/notebook-markdown-extensions/package.json index 1622659825ff..981566798f6f 100644 --- a/lib/vscode/extensions/notebook-markdown-extensions/package.json +++ b/lib/vscode/extensions/notebook-markdown-extensions/package.json @@ -15,21 +15,28 @@ "Other" ], "capabilities": { - "virtualWorkspaces": false + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } }, "contributes": { - "notebookMarkupRenderers": [ + "notebookRenderer": [ { "id": "markdownItRenderer-katex", "displayName": "Markdown it katex renderer", - "entrypoint": "./notebook-out/katex.js", - "dependsOn": "markdownItRenderer" + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/katex.js" + } }, { "id": "markdownItRenderer-emoji", "displayName": "Markdown it emoji renderer", - "entrypoint": "./notebook-out/emoji.js", - "dependsOn": "markdownItRenderer" + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/emoji.js" + } } ] }, diff --git a/lib/vscode/extensions/notebook-markdown-extensions/yarn.lock b/lib/vscode/extensions/notebook-markdown-extensions/yarn.lock index 58952667358a..1ae78c16ae80 100644 --- a/lib/vscode/extensions/notebook-markdown-extensions/yarn.lock +++ b/lib/vscode/extensions/notebook-markdown-extensions/yarn.lock @@ -4,7 +4,7 @@ "@iktakahiro/markdown-it-katex@https://github.com/mjbvz/markdown-it-katex.git": version "4.0.1" - resolved "https://github.com/mjbvz/markdown-it-katex.git#e88925a7cb3fd593a14ed117fb43627c4ba910b6" + resolved "https://github.com/mjbvz/markdown-it-katex.git#2bf0b89c6c22ef0b585f55ccab66d1f7c5356bea" dependencies: katex "^0.13.0" diff --git a/lib/vscode/extensions/npm/package.json b/lib/vscode/extensions/npm/package.json index ab75bdf34eb4..038f091b78e8 100644 --- a/lib/vscode/extensions/npm/package.json +++ b/lib/vscode/extensions/npm/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@types/minimatch": "^3.0.3", - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/which": "^2.0.0" }, "resolutions": { diff --git a/lib/vscode/extensions/npm/package.nls.json b/lib/vscode/extensions/npm/package.nls.json index b8570d0da82e..b3ff9a4d2292 100644 --- a/lib/vscode/extensions/npm/package.nls.json +++ b/lib/vscode/extensions/npm/package.nls.json @@ -1,7 +1,7 @@ { "description": "Extension to add task support for npm scripts.", "displayName": "NPM support for VS Code", - "workspaceTrust": "This extension calls the `tasks.executeTask()` API, which requires trust to run.", + "workspaceTrust": "This extension executes tasks, which require trust to run.", "config.npm.autoDetect": "Controls whether npm scripts should be automatically detected.", "config.npm.runSilent": "Run npm commands with the `--silent` option.", "config.npm.packageManager": "The package manager used to run scripts.", diff --git a/lib/vscode/extensions/npm/yarn.lock b/lib/vscode/extensions/npm/yarn.lock index 6b653dec9d99..29712f0dcc0c 100644 --- a/lib/vscode/extensions/npm/yarn.lock +++ b/lib/vscode/extensions/npm/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/which@^2.0.0": version "2.0.0" diff --git a/lib/vscode/extensions/package.json b/lib/vscode/extensions/package.json index 645ea5c23cb1..06af3843aea1 100644 --- a/lib/vscode/extensions/package.json +++ b/lib/vscode/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "4.2.4" + "typescript": "4.3.2" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/lib/vscode/extensions/php-language-features/package.json b/lib/vscode/extensions/php-language-features/package.json index 3763949d6477..79ae40954769 100644 --- a/lib/vscode/extensions/php-language-features/package.json +++ b/lib/vscode/extensions/php-language-features/package.json @@ -94,7 +94,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/which": "^2.0.0" }, "repository": { diff --git a/lib/vscode/extensions/php-language-features/src/features/validationProvider.ts b/lib/vscode/extensions/php-language-features/src/features/validationProvider.ts index acc6beaf2b57..fb4111c874b8 100644 --- a/lib/vscode/extensions/php-language-features/src/features/validationProvider.ts +++ b/lib/vscode/extensions/php-language-features/src/features/validationProvider.ts @@ -23,7 +23,7 @@ export class LineDecoder { private stringDecoder: StringDecoder; private remaining: string | null; - constructor(encoding: string = 'utf8') { + constructor(encoding: BufferEncoding = 'utf8') { this.stringDecoder = new StringDecoder(encoding); this.remaining = null; } @@ -194,15 +194,17 @@ export default class PHPValidationProvider { if (vscode.workspace.isTrusted) { trigger(); } - } else if (this.config!.executableIsUserDefined !== undefined && !this.config!.executableIsUserDefined) { - const checkedExecutablePath = this.workspaceStore.get(Setting.CheckedExecutablePath, undefined); - if (!checkedExecutablePath || checkedExecutablePath !== this.config!.executable) { - if (await this.showCustomTrustDialog()) { - this.workspaceStore.update(Setting.CheckedExecutablePath, this.config!.executable); - vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true); - } else { - this.pauseValidation = true; - return; + } else { + if (this.config!.executableIsUserDefined !== undefined && !this.config!.executableIsUserDefined) { + const checkedExecutablePath = this.workspaceStore.get(Setting.CheckedExecutablePath, undefined); + if (!checkedExecutablePath || checkedExecutablePath !== this.config!.executable) { + if (await this.showCustomTrustDialog()) { + this.workspaceStore.update(Setting.CheckedExecutablePath, this.config!.executable); + vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true); + } else { + this.pauseValidation = true; + return; + } } } diff --git a/lib/vscode/extensions/php-language-features/yarn.lock b/lib/vscode/extensions/php-language-features/yarn.lock index b0fa5625a6df..09b31c801237 100644 --- a/lib/vscode/extensions/php-language-features/yarn.lock +++ b/lib/vscode/extensions/php-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/which@^2.0.0": version "2.0.0" diff --git a/lib/vscode/extensions/php/cgmanifest.json b/lib/vscode/extensions/php/cgmanifest.json index 15b73aeaf6c8..e9abbe63e635 100644 --- a/lib/vscode/extensions/php/cgmanifest.json +++ b/lib/vscode/extensions/php/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/atom/language-php", - "commitHash": "72739e6341b1b4bf4aa185e928932983baca449e" + "commitHash": "2bf736a814e1a58aa63470c1a29590bd02e924e7" } }, "license": "MIT", - "version": "0.46.0" + "version": "0.46.2" } ], "version": 1 diff --git a/lib/vscode/extensions/php/language-configuration.json b/lib/vscode/extensions/php/language-configuration.json index 9c24c7b724bc..b785c0092ab4 100644 --- a/lib/vscode/extensions/php/language-configuration.json +++ b/lib/vscode/extensions/php/language-configuration.json @@ -26,7 +26,7 @@ ], "indentationRules": { "increaseIndentPattern": "({(?!.*}).*|\\(|\\[|((else(\\s)?)?if|else|for(each)?|while|switch|case).*:)\\s*((/[/*].*|)?$|\\?>)", - "decreaseIndentPattern": "^(.*\\*\\/)?\\s*((\\})|(\\)+[;,])|(\\][;,])|\\b(else:)|\\b((end(if|for(each)?|while|switch));))" + "decreaseIndentPattern": "^(.*\\*\\/)?\\s*((\\})|(\\)+[;,])|(\\]\\)*[;,])|\\b(else:)|\\b((end(if|for(each)?|while|switch));))" }, "folding": { "markers": { diff --git a/lib/vscode/extensions/php/syntaxes/html.tmLanguage.json b/lib/vscode/extensions/php/syntaxes/html.tmLanguage.json index 4bf15294159f..a8dff09d0a02 100644 --- a/lib/vscode/extensions/php/syntaxes/html.tmLanguage.json +++ b/lib/vscode/extensions/php/syntaxes/html.tmLanguage.json @@ -4,111 +4,10 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-php/commit/b6c5e83016b52311cdc622c2579462861ee91587", + "version": "https://github.com/atom/language-php/commit/2bf736a814e1a58aa63470c1a29590bd02e924e7", "name": "PHP", "scopeName": "text.html.php", "injections": { - "L:source.php string.quoted.single.sql.php source.sql.embedded.php": { - "patterns": [ - { - "match": "(#)(\\\\'|[^'])*(?='|$)", - "name": "comment.line.number-sign.sql", - "captures": { - "1": { - "name": "punctuation.definition.comment.sql" - } - } - }, - { - "match": "(--)(\\\\'|[^'])*(?='|$)", - "name": "comment.line.double-dash.sql", - "captures": { - "1": { - "name": "punctuation.definition.comment.sql" - } - } - }, - { - "match": "\\\\[\\\\'`\"]", - "name": "constant.character.escape.php" - }, - { - "match": "\"(?=((\\\\\")|[^\"'])*('|$))", - "name": "string.quoted.double.unclosed.sql" - } - ] - }, - "L:source.php string.quoted.double.sql.php source.sql.embedded.php": { - "patterns": [ - { - "match": "(#)(\\\\\"|[^\"])*(?=\"|$)", - "name": "comment.line.number-sign.sql", - "captures": { - "1": { - "name": "punctuation.definition.comment.sql" - } - } - }, - { - "match": "(--)(\\\\\"|[^\"])*(?=\"|$)", - "name": "comment.line.double-dash.sql", - "captures": { - "1": { - "name": "punctuation.definition.comment.sql" - } - } - }, - { - "match": "\\\\[\\\\'`\"]", - "name": "constant.character.escape.php" - }, - { - "match": "(')([^'\\\\]*)(')", - "name": "string.quoted.single.sql", - "captures": { - "1": { - "name": "punctuation.definition.string.begin.sql" - }, - "2": { - "patterns": [ - { - "include": "source.php#interpolation_double_quoted" - } - ] - }, - "3": { - "name": "punctuation.definition.string.end.sql" - } - } - }, - { - "match": "(`)([^`\\\\]*)(`)", - "name": "string.quoted.other.backtick.sql", - "captures": { - "1": { - "name": "punctuation.definition.string.begin.sql" - }, - "2": { - "patterns": [ - { - "include": "source.php#interpolation_double_quoted" - } - ] - }, - "3": { - "name": "punctuation.definition.string.end.sql" - } - } - }, - { - "match": "'(?=((\\\\')|[^'\"])*(\"|$))", - "name": "string.quoted.single.unclosed.sql" - }, - { - "include": "source.php#interpolation_double_quoted" - } - ] - }, "text.html.php - (meta.embedded | meta.tag), L:((text.html.php meta.tag) - (meta.embedded.block.php | meta.embedded.line.php)), L:(source.js - (meta.embedded.block.php | meta.embedded.line.php)), L:(source.css - (meta.embedded.block.php | meta.embedded.line.php))": { "patterns": [ { diff --git a/lib/vscode/extensions/php/syntaxes/php.tmLanguage.json b/lib/vscode/extensions/php/syntaxes/php.tmLanguage.json index 0dcfc8295c99..d8d7e067384a 100644 --- a/lib/vscode/extensions/php/syntaxes/php.tmLanguage.json +++ b/lib/vscode/extensions/php/syntaxes/php.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-php/commit/5fae657cf989701e9594912772daff33249839b3", + "version": "https://github.com/atom/language-php/commit/2bf736a814e1a58aa63470c1a29590bd02e924e7", "scopeName": "source.php", "patterns": [ { @@ -2824,6 +2824,59 @@ }, "name": "string.quoted.double.sql.php", "patterns": [ + { + "match": "(#)(\\\\\"|[^\"])*(?=\"|$)", + "name": "comment.line.number-sign.sql", + "captures": { + "1": { + "name": "punctuation.definition.comment.sql" + } + } + }, + { + "match": "(--)(\\\\\"|[^\"])*(?=\"|$)", + "name": "comment.line.double-dash.sql", + "captures": { + "1": { + "name": "punctuation.definition.comment.sql" + } + } + }, + { + "match": "\\\\[\\\\\"`']", + "name": "constant.character.escape.php" + }, + { + "match": "'(?=((\\\\')|[^'\"])*(\"|$))", + "name": "string.quoted.single.unclosed.sql" + }, + { + "match": "`(?=((\\\\`)|[^`\"])*(\"|$))", + "name": "string.quoted.other.backtick.unclosed.sql" + }, + { + "begin": "'", + "end": "'", + "name": "string.quoted.single.sql", + "patterns": [ + { + "include": "#interpolation_double_quoted" + } + ] + }, + { + "begin": "`", + "end": "`", + "name": "string.quoted.other.backtick.sql", + "patterns": [ + { + "include": "#interpolation_double_quoted" + } + ] + }, + { + "include": "#interpolation_double_quoted" + }, { "include": "source.sql" } @@ -2845,6 +2898,36 @@ }, "name": "string.quoted.single.sql.php", "patterns": [ + { + "match": "(#)(\\\\'|[^'])*(?='|$)", + "name": "comment.line.number-sign.sql", + "captures": { + "1": { + "name": "punctuation.definition.comment.sql" + } + } + }, + { + "match": "(--)(\\\\'|[^'])*(?='|$)", + "name": "comment.line.double-dash.sql", + "captures": { + "1": { + "name": "punctuation.definition.comment.sql" + } + } + }, + { + "match": "\\\\[\\\\'`\"]", + "name": "constant.character.escape.php" + }, + { + "match": "`(?=((\\\\`)|[^`'])*('|$))", + "name": "string.quoted.other.backtick.unclosed.sql" + }, + { + "match": "\"(?=((\\\\\")|[^\"'])*('|$))", + "name": "string.quoted.double.unclosed.sql" + }, { "include": "source.sql" } @@ -3685,4 +3768,4 @@ "name": "keyword.operator.null-coalescing.php" } } -} +} \ No newline at end of file diff --git a/lib/vscode/extensions/ruby/language-configuration.json b/lib/vscode/extensions/ruby/language-configuration.json index 81fdee540f21..a86f592e3bdb 100644 --- a/lib/vscode/extensions/ruby/language-configuration.json +++ b/lib/vscode/extensions/ruby/language-configuration.json @@ -25,7 +25,7 @@ ["`", "`"] ], "indentationRules": { - "increaseIndentPattern": "^\\s*((begin|class|(private|protected)\\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|while|case)|([^#]*\\sdo\\b)|([^#]*=\\s*(case|if|unless)))\\b([^#\\{;]|(\"|'|\/).*\\4)*(#.*)?$", - "decreaseIndentPattern": "^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(end|rescue|ensure|else|elsif|when)\\b)" + "increaseIndentPattern": "^\\s*((begin|class|(private|protected)\\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\\sdo\\b)|([^#]*=\\s*(case|if|unless)))\\b([^#\\{;]|(\"|'|\/).*\\4)*(#.*)?$", + "decreaseIndentPattern": "^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(end|rescue|ensure|else|elsif|when|in)\\b)" } } diff --git a/lib/vscode/extensions/scss/cgmanifest.json b/lib/vscode/extensions/scss/cgmanifest.json index a67a4f546096..12247769ce22 100644 --- a/lib/vscode/extensions/scss/cgmanifest.json +++ b/lib/vscode/extensions/scss/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "atom/language-sass", "repositoryUrl": "https://github.com/atom/language-sass", - "commitHash": "303bbf0c250fe380b9e57375598cfd916110758b" + "commitHash": "f52ab12f7f9346cc2568129d8c4419bd3d506b47" } }, "license": "MIT", "description": "The file syntaxes/scss.json was derived from the Atom package https://github.com/atom/language-sass which was originally converted from the TextMate bundle https://github.com/alexsancho/SASS.tmbundle.", - "version": "0.61.4" + "version": "0.62.1" } ], "version": 1 diff --git a/lib/vscode/extensions/shellscript/package.json b/lib/vscode/extensions/shellscript/package.json index a8ecba9b104c..3e1e3f3e9c8d 100644 --- a/lib/vscode/extensions/shellscript/package.json +++ b/lib/vscode/extensions/shellscript/package.json @@ -32,7 +32,6 @@ ".bash_profile", ".bash_login", ".ebuild", - ".install", ".profile", ".bash_logout", ".xprofile", diff --git a/lib/vscode/extensions/simple-browser/package.json b/lib/vscode/extensions/simple-browser/package.json index 5fd030b186aa..f2af986a046c 100644 --- a/lib/vscode/extensions/simple-browser/package.json +++ b/lib/vscode/extensions/simple-browser/package.json @@ -70,8 +70,9 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "vscode-codicons": "^0.0.14", - "@types/node": "^12.11.7" + "@types/node": "14.x", + "@types/vscode-webview": "^1.57.0", + "vscode-codicons": "^0.0.14" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/simple-browser/preview-src/index.ts b/lib/vscode/extensions/simple-browser/preview-src/index.ts index 36fe4eb8e158..0b8f9f93531d 100644 --- a/lib/vscode/extensions/simple-browser/preview-src/index.ts +++ b/lib/vscode/extensions/simple-browser/preview-src/index.ts @@ -5,7 +5,6 @@ import { onceDocumentLoaded } from './events'; -declare let acquireVsCodeApi: any; const vscode = acquireVsCodeApi(); function getSettings() { diff --git a/lib/vscode/extensions/simple-browser/yarn.lock b/lib/vscode/extensions/simple-browser/yarn.lock index 804f85a68aee..eb52a2a4a63f 100644 --- a/lib/vscode/extensions/simple-browser/yarn.lock +++ b/lib/vscode/extensions/simple-browser/yarn.lock @@ -2,10 +2,15 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.12.69" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.69.tgz#7cb6a3aa0d16664bf2dcd1450ccb8477464fbd79" - integrity sha512-2F2VQRSFmzqgUEXw75L51MgnnZqc6bKWVSUPfrDPzp6mzGGibeVwyQcpvZvBr5RnsoMRHmC8EcBQiobSeqeJxg== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== + +"@types/vscode-webview@^1.57.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" + integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== applicationinsights@1.7.4: version "1.7.4" diff --git a/lib/vscode/extensions/swift/syntaxes/swift.tmLanguage.json b/lib/vscode/extensions/swift/syntaxes/swift.tmLanguage.json index 3396ff07de9e..be1b132172e1 100644 --- a/lib/vscode/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/lib/vscode/extensions/swift/syntaxes/swift.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/textmate/swift.tmbundle/commit/2ee3d7c63f7dd2c769167278b48e3716d1b60b26", + "version": "https://github.com/textmate/swift.tmbundle/commit/8c7672d74c1baa4e6944a05ac6c57a623532f18b", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -1280,7 +1280,7 @@ "name": "meta.import.swift", "patterns": [ { - "begin": "\\G(?!;|$|//|/\\*)(?:(typealias|struct|class|enum|protocol|var|func)\\s+)?", + "begin": "\\G(?!;|$|//|/\\*)(?:(typealias|struct|class|actor|enum|protocol|var|func)\\s+)?", "beginCaptures": { "1": { "name": "storage.modifier.swift" @@ -1908,7 +1908,7 @@ "type": { "patterns": [ { - "begin": "\\b(class(?!\\s+(?:func|var|let)\\b)|struct)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "begin": "\\b(class(?!\\s+(?:func|var|let)\\b)|struct|actor)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", "beginCaptures": { "1": { "name": "storage.type.$1.swift" @@ -2337,7 +2337,7 @@ ], "repository": { "availability-condition": { - "begin": "\\B(#available)(\\()", + "begin": "\\B(#(?:un)?available)(\\()", "beginCaptures": { "1": { "name": "support.function.availability-condition.swift" @@ -2645,7 +2645,7 @@ "name": "keyword.other.declaration-specifier.swift" }, { - "match": "(?(command: T): T { - for (const id of Array.isArray(command.id) ? command.id : [command.id]) { - this.registerCommand(id, command.execute, command); + if (!this.commands.has(command.id)) { + this.commands.set(command.id, vscode.commands.registerCommand(command.id, command.execute, command)); } return command; } - - private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { - if (this.commands.has(id)) { - return; - } - - this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); - } -} \ No newline at end of file +} diff --git a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/completions.ts b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/completions.ts index 37d32399ffc6..b181a122adf8 100644 --- a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -79,7 +79,6 @@ class MyCompletionItem extends vscode.CompletionItem { this.sortText = tsEntry.sortText; } - // @ts-expect-error until 4.3 protocol update const { sourceDisplay, isSnippet } = tsEntry; if (sourceDisplay) { this.label2 = { name: tsEntry.name, qualifier: Previewer.plainWithLinks(sourceDisplay, client) }; @@ -184,11 +183,9 @@ class MyCompletionItem extends vscode.CompletionItem { const args: Proto.CompletionDetailsRequestArgs = { ...typeConverters.Position.toFileLocationRequestArgs(filepath, this.position), entryNames: [ - // @ts-expect-error until TypeScript 4.3 protocol update this.tsEntry.source || this.tsEntry.data ? { name: this.tsEntry.name, source: this.tsEntry.source, - // @ts-expect-error until TypeScript 4.3 protocol update data: this.tsEntry.data, } : this.tsEntry.name ] @@ -561,7 +558,6 @@ class CompletionAcceptedCommand implements Command { */ this.telemetryReporter.logTelemetry('completions.accept', { isPackageJsonImport: item.tsEntry.isPackageJsonImport ? 'true' : undefined, - // @ts-expect-error until 4.3 protocol update isImportStatementCompletion: item.tsEntry.isImportStatementCompletion ? 'true' : undefined, }); } @@ -753,7 +749,6 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< dotAccessorContext = { range, text }; } } - // @ts-expect-error until 4.3 protocol update isIncomplete = !!response.body.isIncomplete || (response as any).metadata && (response as any).metadata.isIncomplete; entries = response.body.entries; metadata = response.metadata; @@ -792,7 +787,6 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< }; items.push(item); includesPackageJsonImport = includesPackageJsonImport || !!entry.isPackageJsonImport; - // @ts-expect-error until 4.3 protocol update includesImportStatementCompletion = includesImportStatementCompletion || !!entry.isImportStatementCompletion; } } @@ -823,11 +817,15 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< } */ this.telemetryReporter.logTelemetry('completions.execute', { - duration: duration, + duration: String(duration), type: response?.type ?? 'unknown', - count: response?.type === 'response' && response.body ? response.body.entries.length : 0, - updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined, - createAutoImportProviderProgramDurationMs: response?.type === 'response' ? response.performanceData?.createAutoImportProviderProgramDurationMs : undefined, + count: String(response?.type === 'response' && response.body ? response.body.entries.length : 0), + updateGraphDurationMs: response?.type === 'response' && typeof response.performanceData?.updateGraphDurationMs === 'number' + ? String(response.performanceData.updateGraphDurationMs) + : undefined, + createAutoImportProviderProgramDurationMs: response?.type === 'response' && typeof response.performanceData?.createAutoImportProviderProgramDurationMs === 'number' + ? String(response.performanceData.createAutoImportProviderProgramDurationMs) + : undefined, includesPackageJsonImport: includesPackageJsonImport ? 'true' : undefined, includesImportStatementCompletion: includesImportStatementCompletion ? 'true' : undefined, }); @@ -842,7 +840,6 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< return this.client.apiVersion.lt(API.v381) ? undefined : '#'; case ' ': - // @ts-expect-error until 4.3.0 protocol update const space: Proto.CompletionsTriggerCharacter = ' '; return this.client.apiVersion.gte(API.v430) ? space : undefined; diff --git a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 69cb3526d5a9..2ab729661d5e 100644 --- a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -183,7 +183,6 @@ export default class FileConfigurationManager extends Disposable { includeAutomaticOptionalChainCompletions: config.get('suggest.includeAutomaticOptionalChainCompletions', true), provideRefactorNotApplicableReason: true, generateReturnInDocTemplate: config.get('suggest.jsdoc.generateReturns', true), - // @ts-expect-error until 4.3 protocol update includeCompletionsForImportStatements: config.get('suggest.includeCompletionsForImportStatements', true), includeCompletionsWithSnippetText: config.get('suggest.includeCompletionsWithSnippetText', true), displayPartsForJSDoc: true, diff --git a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index 3b8111b7c1f3..10a399bad5b8 100644 --- a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts +++ b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -29,7 +29,7 @@ class OrganizeImportsCommand implements Command { private readonly telemetryReporter: TelemetryReporter, ) { } - public async execute(file: string): Promise { + public async execute(file: string, sortOnly = false): Promise { /* __GDPR__ "organizeImports.execute" : { "${include}": [ @@ -45,7 +45,8 @@ class OrganizeImportsCommand implements Command { args: { file } - } + }, + skipDestructiveCodeActions: sortOnly, }; const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken)); if (response.type !== 'response' || !response.body) { @@ -57,23 +58,42 @@ class OrganizeImportsCommand implements Command { } } -export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvider { - public static readonly minVersion = API.v280; +class ImportsCodeActionProvider implements vscode.CodeActionProvider { + + static register( + client: ITypeScriptServiceClient, + minVersion: API, + kind: vscode.CodeActionKind, + title: string, + sortOnly: boolean, + commandManager: CommandManager, + fileConfigurationManager: FileConfigurationManager, + telemetryReporter: TelemetryReporter, + selector: DocumentSelector + ): vscode.Disposable { + return conditionalRegistration([ + requireMinVersion(client, minVersion), + requireSomeCapability(client, ClientCapability.Semantic), + ], () => { + const provider = new ImportsCodeActionProvider(client, kind, title, sortOnly, commandManager, fileConfigurationManager, telemetryReporter); + return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, { + providedCodeActionKinds: [kind] + }); + }); + } public constructor( private readonly client: ITypeScriptServiceClient, + private readonly kind: vscode.CodeActionKind, + private readonly title: string, + private readonly sortOnly: boolean, commandManager: CommandManager, private readonly fileConfigManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, - ) { commandManager.register(new OrganizeImportsCommand(client, telemetryReporter)); } - public readonly metadata: vscode.CodeActionProviderMetadata = { - providedCodeActionKinds: [vscode.CodeActionKind.SourceOrganizeImports] - }; - public provideCodeActions( document: vscode.TextDocument, _range: vscode.Range, @@ -85,16 +105,14 @@ export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvi return []; } - if (!context.only || !context.only.contains(vscode.CodeActionKind.SourceOrganizeImports)) { + if (!context.only || !context.only.contains(this.kind)) { return []; } this.fileConfigManager.ensureConfigurationForDocument(document, token); - const action = new vscode.CodeAction( - localize('organizeImportsAction.title', "Organize Imports"), - vscode.CodeActionKind.SourceOrganizeImports); - action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file] }; + const action = new vscode.CodeAction(this.title, this.kind); + action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file, this.sortOnly] }; return [action]; } } @@ -106,13 +124,28 @@ export function register( fileConfigurationManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, ) { - return conditionalRegistration([ - requireMinVersion(client, OrganizeImportsCodeActionProvider.minVersion), - requireSomeCapability(client, ClientCapability.Semantic), - ], () => { - const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager, telemetryReporter); - return vscode.languages.registerCodeActionsProvider(selector.semantic, - organizeImportsProvider, - organizeImportsProvider.metadata); - }); + return vscode.Disposable.from( + ImportsCodeActionProvider.register( + client, + API.v280, + vscode.CodeActionKind.SourceOrganizeImports, + localize('organizeImportsAction.title', "Organize Imports"), + false, + commandManager, + fileConfigurationManager, + telemetryReporter, + selector + ), + ImportsCodeActionProvider.register( + client, + API.v430, + vscode.CodeActionKind.Source.append('sortImports'), + localize('sortImportsAction.title', "Sort Imports"), + true, + commandManager, + fileConfigurationManager, + telemetryReporter, + selector + ), + ); } diff --git a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 4284366e2731..d1b861a97fa3 100644 --- a/lib/vscode/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/lib/vscode/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -310,10 +310,12 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { - // Don't show 'infer return type' refactoring unless it has been explicitly requested - // https://github.com/microsoft/TypeScript/issues/42993 - if (!context.only && action.kind?.value === 'refactor.rewrite.function.returnType') { - return false; + if (this.client.apiVersion.lt(API.v430)) { + // Don't show 'infer return type' refactoring unless it has been explicitly requested + // https://github.com/microsoft/TypeScript/issues/42993 + if (!context.only && action.kind?.value === 'refactor.rewrite.function.returnType') { + return false; + } } return true; }); diff --git a/lib/vscode/extensions/typescript-language-features/src/test/testUtils.ts b/lib/vscode/extensions/typescript-language-features/src/test/testUtils.ts index c0ba9a477305..28f3dfc7d928 100644 --- a/lib/vscode/extensions/typescript-language-features/src/test/testUtils.ts +++ b/lib/vscode/extensions/typescript-language-features/src/test/testUtils.ts @@ -89,7 +89,7 @@ export function assertEditorContents(editor: vscode.TextEditor, expectedDocConte if (cursorIndex >= 0) { const expectedCursorPos = editor.document.positionAt(cursorIndex); - assert.deepEqual( + assert.deepStrictEqual( { line: editor.selection.active.line, character: editor.selection.active.line }, { line: expectedCursorPos.line, character: expectedCursorPos.line }, 'Cursor position' diff --git a/lib/vscode/extensions/typescript-language-features/src/tsServer/server.ts b/lib/vscode/extensions/typescript-language-features/src/tsServer/server.ts index 5ede4ef49fc6..b97a1a8b73de 100644 --- a/lib/vscode/extensions/typescript-language-features/src/tsServer/server.ts +++ b/lib/vscode/extensions/typescript-language-features/src/tsServer/server.ts @@ -32,9 +32,11 @@ export interface ITypeScriptServer { kill(): void; - executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExecutionTarget }): undefined; - executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise>; - executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise> | undefined; + /** + * @return A list of all execute requests. If there are multiple entries, the first item is the primary + * request while the rest are secondary ones. + */ + executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Array> | undefined>; dispose(): void; } @@ -202,9 +204,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe } } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExecutionTarget }): undefined; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise>; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise> | undefined { + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Array> | undefined> { const request = this._requestQueue.createRequest(command, args); const requestInfo: RequestItem = { request, @@ -244,7 +244,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe this._requestQueue.enqueue(requestInfo); this.sendNextRequests(); - return result; + return [result]; } private sendNextRequests(): void { @@ -328,9 +328,13 @@ class RequestRouter { private readonly delegate: TsServerDelegate, ) { } - public execute(command: keyof TypeScriptRequests, args: any, executeInfo: ExecuteInfo): Promise> | undefined { + public execute( + command: keyof TypeScriptRequests, + args: any, + executeInfo: ExecuteInfo, + ): Array> | undefined> { if (RequestRouter.sharedCommands.has(command) && typeof executeInfo.executionTarget === 'undefined') { - // Dispatch shared commands to all servers but only return from first one + // Dispatch shared commands to all servers but use first one as the primary response const requestStates: RequestState.State[] = this.servers.map(() => RequestState.Unresolved); @@ -350,15 +354,13 @@ class RequestRouter { token = source.token; } - let firstRequest: Promise> | undefined; + const allRequests: Array> | undefined> = []; for (let serverIndex = 0; serverIndex < this.servers.length; ++serverIndex) { const server = this.servers[serverIndex].server; - const request = server.executeImpl(command, args, { ...executeInfo, token }) as Promise> | undefined; - if (serverIndex === 0) { - firstRequest = request; - } + const request = server.executeImpl(command, args, { ...executeInfo, token })[0]; + allRequests.push(request); if (request) { request .then(result => { @@ -380,7 +382,7 @@ class RequestRouter { } } - return firstRequest; + return allRequests; } for (const { canRun, server } of this.servers) { @@ -460,9 +462,7 @@ export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServ this.mainServer.kill(); } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExecutionTarget }): undefined; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise>; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise> | undefined { + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Array> | undefined> { return this.router.execute(command, args, executeInfo); } } @@ -602,9 +602,7 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ this.semanticServer.kill(); } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExecutionTarget }): undefined; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise>; - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Promise> | undefined { + public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExecutionTarget }): Array> | undefined> { return this.router.execute(command, args, executeInfo); } } diff --git a/lib/vscode/extensions/typescript-language-features/src/tsServer/spawner.ts b/lib/vscode/extensions/typescript-language-features/src/tsServer/spawner.ts index 58fd431edc72..f499c67465ff 100644 --- a/lib/vscode/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/lib/vscode/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -99,6 +99,9 @@ export class TypeScriptServerSpawner { } switch (configuration.separateSyntaxServer) { + case SeparateSyntaxServerConfiguration.ForAllRequests: + return CompositeServerType.SyntaxOnly; + case SeparateSyntaxServerConfiguration.Disabled: return CompositeServerType.Single; diff --git a/lib/vscode/extensions/typescript-language-features/src/tsServer/versionManager.ts b/lib/vscode/extensions/typescript-language-features/src/tsServer/versionManager.ts index 54d88c1192ec..bb06e4505e27 100644 --- a/lib/vscode/extensions/typescript-language-features/src/tsServer/versionManager.ts +++ b/lib/vscode/extensions/typescript-language-features/src/tsServer/versionManager.ts @@ -115,7 +115,7 @@ export class TypeScriptVersionManager extends Disposable { description: version.displayName, detail: version.pathLabel, run: async () => { - const trusted = await vscode.workspace.requestWorkspaceTrust({ modal: true }); + const trusted = await vscode.workspace.requestWorkspaceTrust(); if (trusted) { await this.workspaceState.update(useWorkspaceTsdkStorageKey, true); const tsConfig = vscode.workspace.getConfiguration('typescript'); diff --git a/lib/vscode/extensions/typescript-language-features/src/tsServer/versionStatus.ts b/lib/vscode/extensions/typescript-language-features/src/tsServer/versionStatus.ts index a868c3e2502b..2560f3e8be94 100644 --- a/lib/vscode/extensions/typescript-language-features/src/tsServer/versionStatus.ts +++ b/lib/vscode/extensions/typescript-language-features/src/tsServer/versionStatus.ts @@ -135,12 +135,8 @@ export default class VersionStatus extends Disposable { ) { super(); - this._statusBarEntry = this._register(vscode.window.createStatusBarItem({ - id: 'status.typescript', - name: localize('projectInfo.name', "TypeScript: Project Info"), - alignment: vscode.StatusBarAlignment.Right, - priority: 99 /* to the right of editor status (100) */ - })); + this._statusBarEntry = this._register(vscode.window.createStatusBarItem('status.typescript', vscode.StatusBarAlignment.Right, 99 /* to the right of editor status (100) */)); + this._statusBarEntry.name = localize('projectInfo.name', "TypeScript: Project Info"); const command = new ProjectStatusCommand(this._client, () => this._state); commandManager.register(command); diff --git a/lib/vscode/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/lib/vscode/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index db53b5cf4d34..4f417c27fbfe 100644 --- a/lib/vscode/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/lib/vscode/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -30,6 +30,7 @@ import * as typeConverters from './utils/typeConverters'; import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; import * as ProjectStatus from './utils/largeProjectStatus'; import { ActiveJsTsEditorTracker } from './utils/activeJsTsEditorTracker'; +import { LogLevelMonitor } from './utils/logLevelMonitor'; // Style check diagnostics that can be reported as warnings const styleCheckDiagnostics = new Set([ @@ -147,6 +148,7 @@ export default class TypeScriptServiceClientHost extends Disposable { vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables); this.configurationChanged(); + this._register(new LogLevelMonitor(context)); } private registerExtensionLanguageProvider(description: LanguageDescription, onCompletionAccepted: (item: vscode.CompletionItem) => void) { diff --git a/lib/vscode/extensions/typescript-language-features/src/typescriptServiceClient.ts b/lib/vscode/extensions/typescript-language-features/src/typescriptServiceClient.ts index 2bdf9d855b7a..4990b4ce1c73 100644 --- a/lib/vscode/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/lib/vscode/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -19,7 +19,7 @@ import { TypeScriptVersionManager } from './tsServer/versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; import API from './utils/api'; -import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; +import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; import { Disposable } from './utils/dispose'; import * as fileSchemes from './utils/fileSchemes'; import { Logger } from './utils/logger'; @@ -224,6 +224,12 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public get capabilities() { + if (this._configuration.separateSyntaxServer === SeparateSyntaxServerConfiguration.ForAllRequests) { + return new ClientCapabilities( + ClientCapability.Syntax, + ClientCapability.EnhancedSyntax); + } + if (isWeb()) { return new ClientCapabilities( ClientCapability.Syntax, @@ -676,6 +682,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public hasCapabilityForResource(resource: vscode.Uri, capability: ClientCapability): boolean { + if (!this.capabilities.has(capability)) { + return false; + } + switch (capability) { case ClientCapability.Semantic: { @@ -727,7 +737,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, config?: ExecConfig): Promise> { - let execution: Promise>; + let executions: Array> | undefined>; if (config?.cancelOnResourceChange) { const runningServerState = this.service(); @@ -741,17 +751,18 @@ export default class TypeScriptServiceClient extends Disposable implements IType }; runningServerState.toCancelOnResourceChange.add(inFlight); - execution = this.executeImpl(command, args, { + executions = this.executeImpl(command, args, { isAsync: false, token: source.token, expectsResult: true, ...config, - }).finally(() => { + }); + executions[0]!.finally(() => { runningServerState.toCancelOnResourceChange.delete(inFlight); source.dispose(); }); } else { - execution = this.executeImpl(command, args, { + executions = this.executeImpl(command, args, { isAsync: false, token, expectsResult: true, @@ -760,25 +771,25 @@ export default class TypeScriptServiceClient extends Disposable implements IType } if (config?.nonRecoverable) { - execution.catch(err => this.fatalError(command, err)); + executions[0]!.catch(err => this.fatalError(command, err)); } - return execution; + if (command === 'updateOpen') { + // If update open has completed, consider that the project has loaded + Promise.all(executions).then(() => { + this.loadingIndicator.reset(); + }); + } + + return executions[0]!; } public executeWithoutWaitingForResponse(command: keyof TypeScriptRequests, args: any): void { - const promise = this.executeImpl(command, args, { + this.executeImpl(command, args, { isAsync: false, token: undefined, - expectsResult: command === 'updateOpen' + expectsResult: false }); - - if (command === 'updateOpen') { - // If update open has completed, consider that the project has loaded - promise.then(() => { - this.loadingIndicator.reset(); - }); - } } public executeAsync(command: keyof TypeScriptRequests, args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise> { @@ -786,12 +797,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType isAsync: true, token, expectsResult: true - }); + })[0]!; } - private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, requireSemantic?: boolean }): undefined; - private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Promise>; - private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Promise> | undefined { + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Array> | undefined> { this.bufferSyncSupport.beforeCommand(command); const runningServerState = this.service(); return runningServerState.server.executeImpl(command, args, executeInfo); @@ -812,7 +821,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } */ this.logTelemetry('fatalError', { ...(error instanceof TypeScriptServerError ? error.telemetry : { command }) }); - console.error(`A non-recoverable error occured while executing tsserver command: ${command}`); + console.error(`A non-recoverable error occurred while executing tsserver command: ${command}`); if (error instanceof TypeScriptServerError && error.serverErrorText) { console.error(error.serverErrorText); } @@ -1056,4 +1065,3 @@ class ServerInitializingIndicator extends Disposable { } } } - diff --git a/lib/vscode/extensions/typescript-language-features/src/utils/configuration.ts b/lib/vscode/extensions/typescript-language-features/src/utils/configuration.ts index c43a121079cb..92d5122bbcd6 100644 --- a/lib/vscode/extensions/typescript-language-features/src/utils/configuration.ts +++ b/lib/vscode/extensions/typescript-language-features/src/utils/configuration.ts @@ -48,6 +48,8 @@ export namespace TsServerLogLevel { export const enum SeparateSyntaxServerConfiguration { Disabled, Enabled, + /** Use a single syntax server for every request, even on desktop */ + ForAllRequests, } export class ImplicitProjectConfiguration { @@ -180,7 +182,10 @@ export class TypeScriptServiceConfiguration { } private static readUseSeparateSyntaxServer(configuration: vscode.WorkspaceConfiguration): SeparateSyntaxServerConfiguration { - const value = configuration.get('typescript.tsserver.useSeparateSyntaxServer', true); + const value = configuration.get('typescript.tsserver.useSeparateSyntaxServer', true); + if (value === 'forAllRequests') { + return SeparateSyntaxServerConfiguration.ForAllRequests; + } if (value === true) { return SeparateSyntaxServerConfiguration.Enabled; } diff --git a/lib/vscode/extensions/typescript-language-features/src/utils/largeProjectStatus.ts b/lib/vscode/extensions/typescript-language-features/src/utils/largeProjectStatus.ts index 223d7cb47167..346f95c69796 100644 --- a/lib/vscode/extensions/typescript-language-features/src/utils/largeProjectStatus.ts +++ b/lib/vscode/extensions/typescript-language-features/src/utils/largeProjectStatus.ts @@ -23,12 +23,8 @@ class ExcludeHintItem { constructor( private readonly telemetryReporter: TelemetryReporter ) { - this._item = vscode.window.createStatusBarItem({ - id: 'status.typescript.exclude', - name: localize('statusExclude', "TypeScript: Configure Excludes"), - alignment: vscode.StatusBarAlignment.Right, - priority: 98 /* to the right of typescript version status (99) */ - }); + this._item = vscode.window.createStatusBarItem('status.typescript.exclude', vscode.StatusBarAlignment.Right, 98 /* to the right of typescript version status (99) */); + this._item.name = localize('statusExclude', "TypeScript: Configure Excludes"); this._item.command = 'js.projectStatus.command'; } diff --git a/lib/vscode/extensions/typescript-language-features/src/utils/logLevelMonitor.ts b/lib/vscode/extensions/typescript-language-features/src/utils/logLevelMonitor.ts new file mode 100644 index 000000000000..7a3c752194ea --- /dev/null +++ b/lib/vscode/extensions/typescript-language-features/src/utils/logLevelMonitor.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Disposable } from './dispose'; +import { localize } from '../tsServer/versionProvider'; +import { TsServerLogLevel } from './configuration'; + +export class LogLevelMonitor extends Disposable { + + private static readonly logLevelConfigKey = 'typescript.tsserver.log'; + private static readonly logLevelChangedStorageKey = 'typescript.tsserver.logLevelChanged'; + private static readonly doNotPromptLogLevelStorageKey = 'typescript.tsserver.doNotPromptLogLevel'; + + constructor(private readonly context: vscode.ExtensionContext) { + super(); + + this._register(vscode.workspace.onDidChangeConfiguration(this.onConfigurationChange, this, this._disposables)); + + if (this.shouldNotifyExtendedLogging()) { + this.notifyExtendedLogging(); + } + } + + private onConfigurationChange(event: vscode.ConfigurationChangeEvent) { + const logLevelChanged = event.affectsConfiguration(LogLevelMonitor.logLevelConfigKey); + if (!logLevelChanged) { + return; + } + this.context.globalState.update(LogLevelMonitor.logLevelChangedStorageKey, new Date()); + } + + private get logLevel(): TsServerLogLevel { + return TsServerLogLevel.fromString(vscode.workspace.getConfiguration().get(LogLevelMonitor.logLevelConfigKey, 'off')); + } + + /** + * Last date change if it exists and can be parsed as a date, + * otherwise undefined. + */ + private get lastLogLevelChange(): Date | undefined { + const lastChange = this.context.globalState.get(LogLevelMonitor.logLevelChangedStorageKey); + + if (lastChange) { + const date = new Date(lastChange); + if (date instanceof Date && !isNaN(date.valueOf())) { + return date; + } + } + return undefined; + } + + private get doNotPrompt(): boolean { + return this.context.globalState.get(LogLevelMonitor.doNotPromptLogLevelStorageKey) || false; + } + + private shouldNotifyExtendedLogging(): boolean { + const lastChangeMilliseconds = this.lastLogLevelChange ? new Date(this.lastLogLevelChange).valueOf() : 0; + const lastChangePlusOneWeek = new Date(lastChangeMilliseconds + /* 7 days in milliseconds */ 86400000 * 7); + + if (!this.doNotPrompt && this.logLevel !== TsServerLogLevel.Off && lastChangePlusOneWeek.valueOf() < Date.now()) { + return true; + } + return false; + } + + private notifyExtendedLogging() { + const enum Choice { + DisableLogging = 0, + DoNotShowAgain = 1 + } + interface Item extends vscode.MessageItem { + readonly choice: Choice; + } + + vscode.window.showInformationMessage( + localize( + 'typescript.extendedLogging.isEnabled', + "TS Server logging is currently enabled which may impact performance."), + { + title: localize( + 'typescript.extendedLogging.disableLogging', + "Disable logging"), + choice: Choice.DisableLogging + }, + { + title: localize( + 'typescript.extendedLogging.doNotShowAgain', + "Don't show again"), + choice: Choice.DoNotShowAgain + }) + .then(selection => { + if (!selection) { + return; + } + if (selection.choice === Choice.DisableLogging) { + return vscode.workspace.getConfiguration().update(LogLevelMonitor.logLevelConfigKey, 'off', true); + } else if (selection.choice === Choice.DoNotShowAgain) { + return this.context.globalState.update(LogLevelMonitor.doNotPromptLogLevelStorageKey, true); + } + return; + }); + } +} diff --git a/lib/vscode/extensions/typescript-language-features/yarn.lock b/lib/vscode/extensions/typescript-language-features/yarn.lock index 5ac428c1065f..a94f34e248ee 100644 --- a/lib/vscode/extensions/typescript-language-features/yarn.lock +++ b/lib/vscode/extensions/typescript-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/semver@^5.5.0": version "5.5.0" diff --git a/lib/vscode/extensions/vscode-api-tests/package.json b/lib/vscode/extensions/vscode-api-tests/package.json index e1c16dace634..dcdd8b608a95 100644 --- a/lib/vscode/extensions/vscode-api-tests/package.json +++ b/lib/vscode/extensions/vscode-api-tests/package.json @@ -117,9 +117,9 @@ ] } ], - "notebookProvider": [ + "notebooks": [ { - "viewType": "notebookCoreTest", + "type": "notebookCoreTest", "displayName": "Notebook Core Test", "selector": [ { @@ -129,7 +129,7 @@ ] }, { - "viewType": "notebook.nbdtest", + "type": "notebook.nbdtest", "displayName": "notebook.nbdtest", "selector": [ { @@ -138,7 +138,7 @@ ] }, { - "viewType": "notebook.nbdserializer", + "type": "notebook.nbdserializer", "displayName": "notebook.nbdserializer", "selector": [ { @@ -154,7 +154,7 @@ }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "repository": { "type": "git", diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts index f9f971bdbdab..7c906ba0fe45 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts @@ -52,8 +52,8 @@ suite('vscode API - commands', () => { await commands.executeCommand('t1', 'start'); registration.dispose(); assert.ok(args!); - assert.equal(args!.length, 1); - assert.equal(args![0], 'start'); + assert.strictEqual(args!.length, 1); + assert.strictEqual(args![0], 'start'); }); test('editorCommand with extra args', function () { @@ -68,7 +68,7 @@ suite('vscode API - commands', () => { return commands.executeCommand('t1', 12345, commands); }).then(() => { assert.ok(args); - assert.equal(args.length, 4); + assert.strictEqual(args.length, 4); assert.ok(args[2] === 12345); assert.ok(args[3] === commands); registration.dispose(); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts index 0a9ddf7aa2d6..9c7fa1acff51 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts @@ -15,7 +15,7 @@ suite('vscode API - configuration', () => { test('configurations, language defaults', function () { const defaultLanguageSettings = vscode.workspace.getConfiguration().get('[abcLang]'); - assert.deepEqual(defaultLanguageSettings, { + assert.deepStrictEqual(defaultLanguageSettings, { 'editor.lineNumbers': 'off', 'editor.tabSize': 2 }); @@ -25,25 +25,25 @@ suite('vscode API - configuration', () => { const config = vscode.workspace.getConfiguration('farboo'); assert.ok(config.has('config0')); - assert.equal(config.get('config0'), true); - assert.equal(config.get('config4'), ''); - assert.equal(config['config0'], true); - assert.equal(config['config4'], ''); + assert.strictEqual(config.get('config0'), true); + assert.strictEqual(config.get('config4'), ''); + assert.strictEqual(config['config0'], true); + assert.strictEqual(config['config4'], ''); assert.throws(() => (config)['config4'] = 'valuevalue'); assert.ok(config.has('nested.config1')); - assert.equal(config.get('nested.config1'), 42); + assert.strictEqual(config.get('nested.config1'), 42); assert.ok(config.has('nested.config2')); - assert.equal(config.get('nested.config2'), 'Das Pferd frisst kein Reis.'); + assert.strictEqual(config.get('nested.config2'), 'Das Pferd frisst kein Reis.'); }); test('configuration, name vs property', () => { const config = vscode.workspace.getConfiguration('farboo'); assert.ok(config.has('get')); - assert.equal(config.get('get'), 'get-prop'); - assert.deepEqual(config['get'], config.get); + assert.strictEqual(config.get('get'), 'get-prop'); + assert.deepStrictEqual(config['get'], config.get); assert.throws(() => config['get'] = 'get-prop'); }); }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 145c2ee3bce8..f79a2000cb3f 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -13,7 +13,7 @@ suite('vscode API - debug', function () { teardown(assertNoRpc); test('breakpoints', async function () { - assert.equal(debug.breakpoints.length, 0); + assert.strictEqual(debug.breakpoints.length, 0); let onDidChangeBreakpointsCounter = 0; const toDispose: Disposable[] = []; @@ -22,19 +22,19 @@ suite('vscode API - debug', function () { })); debug.addBreakpoints([{ id: '1', enabled: true }, { id: '2', enabled: false, condition: '2 < 5' }]); - assert.equal(onDidChangeBreakpointsCounter, 1); - assert.equal(debug.breakpoints.length, 2); - assert.equal(debug.breakpoints[0].id, '1'); - assert.equal(debug.breakpoints[1].id, '2'); - assert.equal(debug.breakpoints[1].condition, '2 < 5'); + assert.strictEqual(onDidChangeBreakpointsCounter, 1); + assert.strictEqual(debug.breakpoints.length, 2); + assert.strictEqual(debug.breakpoints[0].id, '1'); + assert.strictEqual(debug.breakpoints[1].id, '2'); + assert.strictEqual(debug.breakpoints[1].condition, '2 < 5'); debug.removeBreakpoints([{ id: '1', enabled: true }]); - assert.equal(onDidChangeBreakpointsCounter, 2); - assert.equal(debug.breakpoints.length, 1); + assert.strictEqual(onDidChangeBreakpointsCounter, 2); + assert.strictEqual(debug.breakpoints.length, 1); debug.removeBreakpoints([{ id: '2', enabled: false }]); - assert.equal(onDidChangeBreakpointsCounter, 3); - assert.equal(debug.breakpoints.length, 0); + assert.strictEqual(onDidChangeBreakpointsCounter, 3); + assert.strictEqual(debug.breakpoints.length, 0); disposeAll(toDispose); }); @@ -79,36 +79,36 @@ suite('vscode API - debug', function () { const initializedPromise = new Promise(resolve => initializedReceived = resolve); const configurationDonePromise = new Promise(resolve => configurationDoneReceived = resolve); const success = await debug.startDebugging(workspace.workspaceFolders![0], 'Launch debug.js'); - assert.equal(success, true); + assert.strictEqual(success, true); await initializedPromise; await configurationDonePromise; await firstVariablesRetrieved; - assert.notEqual(debug.activeDebugSession, undefined); - assert.equal(stoppedEvents, 1); + assert.notStrictEqual(debug.activeDebugSession, undefined); + assert.strictEqual(stoppedEvents, 1); const secondVariablesRetrieved = new Promise(resolve => variablesReceived = resolve); await commands.executeCommand('workbench.action.debug.stepOver'); await secondVariablesRetrieved; - assert.equal(stoppedEvents, 2); + assert.strictEqual(stoppedEvents, 2); const editor = window.activeTextEditor; - assert.notEqual(editor, undefined); - assert.equal(basename(editor!.document.fileName), 'debug.js'); + assert.notStrictEqual(editor, undefined); + assert.strictEqual(basename(editor!.document.fileName), 'debug.js'); const thirdVariablesRetrieved = new Promise(resolve => variablesReceived = resolve); await commands.executeCommand('workbench.action.debug.stepOver'); await thirdVariablesRetrieved; - assert.equal(stoppedEvents, 3); + assert.strictEqual(stoppedEvents, 3); const fourthVariablesRetrieved = new Promise(resolve => variablesReceived = resolve); await commands.executeCommand('workbench.action.debug.stepInto'); await fourthVariablesRetrieved; - assert.equal(stoppedEvents, 4); + assert.strictEqual(stoppedEvents, 4); const fifthVariablesRetrieved = new Promise(resolve => variablesReceived = resolve); await commands.executeCommand('workbench.action.debug.stepOut'); await fifthVariablesRetrieved; - assert.equal(stoppedEvents, 5); + assert.strictEqual(stoppedEvents, 5); let sessionTerminated: () => void; toDispose.push(debug.onDidTerminateDebugSession(() => { @@ -127,6 +127,6 @@ suite('vscode API - debug', function () { } catch (e) { errorCount++; } - assert.equal(errorCount, 1); + assert.strictEqual(errorCount, 1); }); }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index ef16bbe1c61a..faf2f7d80133 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -44,7 +44,7 @@ suite('vscode API - editors', () => { return withRandomFileEditor('', (editor, doc) => { return editor.insertSnippet(snippetString).then(inserted => { assert.ok(inserted); - assert.equal(doc.getText(), 'This is a placeholder snippet'); + assert.strictEqual(doc.getText(), 'This is a placeholder snippet'); assert.ok(doc.isDirty); }); }); @@ -69,7 +69,7 @@ suite('vscode API - editors', () => { await withRandomFileEditor('', async (editor, doc) => { const inserted = await editor.insertSnippet(snippetString); assert.ok(inserted); - assert.equal(doc.getText(), 'running: INTEGRATION-TESTS'); + assert.strictEqual(doc.getText(), 'running: INTEGRATION-TESTS'); assert.ok(doc.isDirty); }); @@ -88,7 +88,7 @@ suite('vscode API - editors', () => { return editor.insertSnippet(snippetString).then(inserted => { assert.ok(inserted); - assert.equal(doc.getText(), 'This has been replaced'); + assert.strictEqual(doc.getText(), 'This has been replaced'); assert.ok(doc.isDirty); }); }); @@ -106,7 +106,7 @@ suite('vscode API - editors', () => { return editor.insertSnippet(snippetString, selection).then(inserted => { assert.ok(inserted); - assert.equal(doc.getText(), 'This has been replaced'); + assert.strictEqual(doc.getText(), 'This has been replaced'); assert.ok(doc.isDirty); }); }); @@ -118,7 +118,7 @@ suite('vscode API - editors', () => { builder.insert(new Position(0, 0), 'Hello World'); }).then(applied => { assert.ok(applied); - assert.equal(doc.getText(), 'Hello World'); + assert.strictEqual(doc.getText(), 'Hello World'); assert.ok(doc.isDirty); }); }); @@ -130,7 +130,7 @@ suite('vscode API - editors', () => { builder.replace(new Range(0, 0, Number.MAX_VALUE, Number.MAX_VALUE), 'new'); }).then(applied => { assert.ok(applied); - assert.equal(doc.getText(), 'new'); + assert.strictEqual(doc.getText(), 'new'); assert.ok(doc.isDirty); }); }); @@ -146,12 +146,12 @@ suite('vscode API - editors', () => { return withRandomFileEditor('Hello world!', async (editor, doc) => { const applied1 = await executeReplace(editor, new Range(0, 0, 0, 1), 'h', false, false); assert.ok(applied1); - assert.equal(doc.getText(), 'hello world!'); + assert.strictEqual(doc.getText(), 'hello world!'); assert.ok(doc.isDirty); const applied2 = await executeReplace(editor, new Range(0, 1, 0, 5), 'ELLO', false, false); assert.ok(applied2); - assert.equal(doc.getText(), 'hELLO world!'); + assert.strictEqual(doc.getText(), 'hELLO world!'); assert.ok(doc.isDirty); await commands.executeCommand('undo'); @@ -161,7 +161,7 @@ suite('vscode API - editors', () => { // it is unclear why this happens, but it can happen for a multitude of reasons await commands.executeCommand('undo'); } - assert.equal(doc.getText(), 'Hello world!'); + assert.strictEqual(doc.getText(), 'Hello world!'); }); }); @@ -169,16 +169,16 @@ suite('vscode API - editors', () => { return withRandomFileEditor('Hello world!', (editor, doc) => { return executeReplace(editor, new Range(0, 0, 0, 1), 'h', false, false).then(applied => { assert.ok(applied); - assert.equal(doc.getText(), 'hello world!'); + assert.strictEqual(doc.getText(), 'hello world!'); assert.ok(doc.isDirty); return executeReplace(editor, new Range(0, 1, 0, 5), 'ELLO', true, false); }).then(applied => { assert.ok(applied); - assert.equal(doc.getText(), 'hELLO world!'); + assert.strictEqual(doc.getText(), 'hELLO world!'); assert.ok(doc.isDirty); return commands.executeCommand('undo'); }).then(_ => { - assert.equal(doc.getText(), 'hello world!'); + assert.strictEqual(doc.getText(), 'hello world!'); }); }); }); @@ -186,26 +186,26 @@ suite('vscode API - editors', () => { test('issue #16573: Extension API: insertSpaces and tabSize are undefined', () => { return withRandomFileEditor('Hello world!\n\tHello world!', (editor, _doc) => { - assert.equal(editor.options.tabSize, 4); - assert.equal(editor.options.insertSpaces, false); - assert.equal(editor.options.cursorStyle, TextEditorCursorStyle.Line); - assert.equal(editor.options.lineNumbers, TextEditorLineNumbersStyle.On); + assert.strictEqual(editor.options.tabSize, 4); + assert.strictEqual(editor.options.insertSpaces, false); + assert.strictEqual(editor.options.cursorStyle, TextEditorCursorStyle.Line); + assert.strictEqual(editor.options.lineNumbers, TextEditorLineNumbersStyle.On); editor.options = { tabSize: 2 }; - assert.equal(editor.options.tabSize, 2); - assert.equal(editor.options.insertSpaces, false); - assert.equal(editor.options.cursorStyle, TextEditorCursorStyle.Line); - assert.equal(editor.options.lineNumbers, TextEditorLineNumbersStyle.On); + assert.strictEqual(editor.options.tabSize, 2); + assert.strictEqual(editor.options.insertSpaces, false); + assert.strictEqual(editor.options.cursorStyle, TextEditorCursorStyle.Line); + assert.strictEqual(editor.options.lineNumbers, TextEditorLineNumbersStyle.On); editor.options.tabSize = 'invalid'; - assert.equal(editor.options.tabSize, 2); - assert.equal(editor.options.insertSpaces, false); - assert.equal(editor.options.cursorStyle, TextEditorCursorStyle.Line); - assert.equal(editor.options.lineNumbers, TextEditorLineNumbersStyle.On); + assert.strictEqual(editor.options.tabSize, 2); + assert.strictEqual(editor.options.insertSpaces, false); + assert.strictEqual(editor.options.cursorStyle, TextEditorCursorStyle.Line); + assert.strictEqual(editor.options.lineNumbers, TextEditorLineNumbersStyle.On); return Promise.resolve(); }); @@ -262,6 +262,6 @@ suite('vscode API - editors', () => { const file = Uri.parse(root.toString() + relativePath); const document = await workspace.openTextDocument(file); - assert.equal(document.getText(), Buffer.from(await workspace.fs.readFile(file)).toString()); + assert.strictEqual(document.getText(), Buffer.from(await workspace.fs.readFile(file)).toString()); } }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts index 42e25bafddd1..73b799525910 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts @@ -12,12 +12,12 @@ suite('vscode API - env', () => { teardown(assertNoRpc); test('env is set', function () { - assert.equal(typeof env.language, 'string'); - assert.equal(typeof env.appRoot, 'string'); - assert.equal(typeof env.appName, 'string'); - assert.equal(typeof env.machineId, 'string'); - assert.equal(typeof env.sessionId, 'string'); - assert.equal(typeof env.shell, 'string'); + assert.strictEqual(typeof env.language, 'string'); + assert.strictEqual(typeof env.appRoot, 'string'); + assert.strictEqual(typeof env.appName, 'string'); + assert.strictEqual(typeof env.machineId, 'string'); + assert.strictEqual(typeof env.sessionId, 'string'); + assert.strictEqual(typeof env.shell, 'string'); }); test('env is readonly', function () { @@ -37,7 +37,7 @@ suite('vscode API - env', () => { // not running in remote, so we expect both extensions assert.ok(knownWorkspaceExtension); assert.ok(knownUiAndWorkspaceExtension); - assert.equal(ExtensionKind.UI, knownUiAndWorkspaceExtension!.extensionKind); + assert.strictEqual(ExtensionKind.UI, knownUiAndWorkspaceExtension!.extensionKind); } else if (typeof remoteName === 'string') { // running in remote, so we only expect workspace extensions assert.ok(knownWorkspaceExtension); @@ -46,7 +46,7 @@ suite('vscode API - env', () => { } else { assert.ok(knownUiAndWorkspaceExtension); } - assert.equal(ExtensionKind.Workspace, knownWorkspaceExtension!.extensionKind); + assert.strictEqual(ExtensionKind.Workspace, knownWorkspaceExtension!.extensionKind); } else { assert.fail(); } @@ -58,9 +58,9 @@ suite('vscode API - env', () => { const kind = env.uiKind; if (result.scheme === 'http' || result.scheme === 'https') { - assert.equal(kind, UIKind.Web); + assert.strictEqual(kind, UIKind.Web); } else { - assert.equal(kind, UIKind.Desktop); + assert.strictEqual(kind, UIKind.Desktop); } }); @@ -70,9 +70,9 @@ suite('vscode API - env', () => { assert.ok(result); if (env.uiKind === UIKind.Desktop) { - assert.equal(uri.scheme, result.scheme); - assert.equal(uri.authority, result.authority); - assert.equal(uri.path, result.path); + assert.strictEqual(uri.scheme, result.scheme); + assert.strictEqual(uri.authority, result.authority); + assert.strictEqual(uri.path, result.path); } else { assert.ok(result.scheme === 'http' || result.scheme === 'https'); } diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts index 0553a4444dbe..ed75f597dc5b 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts @@ -23,7 +23,7 @@ suite('vscode API - languages', () => { } function assertEqualRange(actual: vscode.Range, expected: vscode.Range, message?: string) { - assert.equal(rangeToString(actual), rangeToString(expected), message); + assert.strictEqual(rangeToString(actual), rangeToString(expected), message); } test('setTextDocumentLanguage -> close/open event', async function () { @@ -36,8 +36,8 @@ suite('vscode API - languages', () => { let close = new Promise(resolve => { disposables.push(vscode.workspace.onDidCloseTextDocument(e => { if (e === doc) { - assert.equal(doc.languageId, langIdNow); - assert.equal(clock, 0); + assert.strictEqual(doc.languageId, langIdNow); + assert.strictEqual(clock, 0); clock += 1; resolve(); } @@ -46,8 +46,8 @@ suite('vscode API - languages', () => { let open = new Promise(resolve => { disposables.push(vscode.workspace.onDidOpenTextDocument(e => { if (e === doc) { // same instance! - assert.equal(doc.languageId, 'json'); - assert.equal(clock, 1); + assert.strictEqual(doc.languageId, 'json'); + assert.strictEqual(clock, 1); clock += 1; resolve(); } @@ -55,8 +55,8 @@ suite('vscode API - languages', () => { }); let change = vscode.languages.setTextDocumentLanguage(doc, 'json'); await Promise.all([change, close, open]); - assert.equal(clock, 2); - assert.equal(doc.languageId, 'json'); + assert.strictEqual(clock, 2); + assert.strictEqual(doc.languageId, 'json'); disposables.forEach(disposable => disposable.dispose()); disposables.length = 0; }); @@ -82,7 +82,7 @@ suite('vscode API - languages', () => { col2.set(uri, [new vscode.Diagnostic(new vscode.Range(0, 0, 0, 12), 'error1')]); let diag = vscode.languages.getDiagnostics(uri); - assert.equal(diag.length, 2); + assert.strictEqual(diag.length, 2); let tuples = vscode.languages.getDiagnostics(); let found = false; @@ -111,13 +111,13 @@ suite('vscode API - languages', () => { vscode.languages.registerDocumentLinkProvider({ language: 'java', scheme: testFs.scheme }, linkProvider); const links = await vscode.commands.executeCommand('vscode.executeLinkProvider', doc.uri); - assert.equal(2, links && links.length); + assert.strictEqual(2, links && links.length); let [link1, link2] = links!.sort((l1, l2) => l1.range.start.compareTo(l2.range.start)); - assert.equal(target.toString(), link1.target && link1.target.toString()); + assert.strictEqual(target.toString(), link1.target && link1.target.toString()); assertEqualRange(range, link1.range); - assert.equal('http://a.com/', link2.target && link2.target.toString()); + assert.strictEqual('http://a.com/', link2.target && link2.target.toString()); assertEqualRange(new vscode.Range(new vscode.Position(0, 13), new vscode.Position(0, 25)), link2.range); }); @@ -139,7 +139,7 @@ suite('vscode API - languages', () => { let r1 = vscode.languages.registerCodeActionsProvider({ pattern: '*.far', scheme: 'ttt' }, { provideCodeActions(_document, _range, ctx): vscode.Command[] { - assert.equal(ctx.diagnostics.length, 2); + assert.strictEqual(ctx.diagnostics.length, 2); let [first, second] = ctx.diagnostics; assert.ok(first === diag1); assert.ok(second === diag2); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index 70e6e7042315..701de344f331 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -13,7 +13,6 @@ suite('Notebook Document', function () { deserializeNotebook(_data: Uint8Array): vscode.NotebookData | Thenable { return new vscode.NotebookData( [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '// SIMPLE', 'javascript')], - new vscode.NotebookDocumentMetadata() ); } serializeNotebook(_data: vscode.NotebookData): Uint8Array | Thenable { @@ -25,7 +24,6 @@ suite('Notebook Document', function () { async openNotebook(uri: vscode.Uri, _openContext: vscode.NotebookDocumentOpenContext): Promise { return new vscode.NotebookData( [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, uri.toString(), 'javascript')], - new vscode.NotebookDocumentMetadata() ); } async saveNotebook(_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) { @@ -47,29 +45,25 @@ suite('Notebook Document', function () { await utils.closeAllEditors(); utils.disposeAll(disposables); disposables.length = 0; - - for (let doc of vscode.notebook.notebookDocuments) { - assert.strictEqual(doc.isDirty, false, doc.uri.toString()); - } }); suiteSetup(function () { - disposables.push(vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', complexContentProvider)); - disposables.push(vscode.notebook.registerNotebookSerializer('notebook.nbdserializer', simpleContentProvider)); + disposables.push(vscode.workspace.registerNotebookContentProvider('notebook.nbdtest', complexContentProvider)); + disposables.push(vscode.workspace.registerNotebookSerializer('notebook.nbdserializer', simpleContentProvider)); }); test('cannot register sample provider multiple times', function () { assert.throws(() => { - vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', complexContentProvider); + vscode.workspace.registerNotebookContentProvider('notebook.nbdtest', complexContentProvider); }); // assert.throws(() => { - // vscode.notebook.registerNotebookSerializer('notebook.nbdserializer', simpleContentProvider); + // vscode.workspace.registerNotebookSerializer('notebook.nbdserializer', simpleContentProvider); // }); }); test('cannot open unknown types', async function () { try { - await vscode.notebook.openNotebookDocument(vscode.Uri.parse('some:///thing.notTypeKnown')); + await vscode.workspace.openNotebookDocument(vscode.Uri.parse('some:///thing.notTypeKnown')); assert.ok(false); } catch { assert.ok(true); @@ -78,32 +72,43 @@ suite('Notebook Document', function () { test('document basics', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const notebook = await vscode.notebook.openNotebookDocument(uri); + const notebook = await vscode.workspace.openNotebookDocument(uri); assert.strictEqual(notebook.uri.toString(), uri.toString()); assert.strictEqual(notebook.isDirty, false); assert.strictEqual(notebook.isUntitled, false); assert.strictEqual(notebook.cellCount, 1); - assert.strictEqual(notebook.viewType, 'notebook.nbdtest'); + assert.strictEqual(notebook.notebookType, 'notebook.nbdtest'); }); test('notebook open/close, notebook ready when cell-document open event is fired', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); let didHappen = false; - const p = utils.asPromise(vscode.workspace.onDidOpenTextDocument).then(doc => { - if (doc.uri.scheme !== 'vscode-notebook-cell') { - return; - } - const notebook = vscode.notebook.notebookDocuments.find(notebook => { - const cell = notebook.getCells().find(cell => cell.document === doc); - return Boolean(cell); + + const p = new Promise((resolve, reject) => { + const sub = vscode.workspace.onDidOpenTextDocument(doc => { + if (doc.uri.scheme !== 'vscode-notebook-cell') { + // ignore other open events + return; + } + const notebook = vscode.workspace.notebookDocuments.find(notebook => { + const cell = notebook.getCells().find(cell => cell.document === doc); + return Boolean(cell); + }); + assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); + didHappen = true; + sub.dispose(); + resolve(); }); - assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); - didHappen = true; + + setTimeout(() => { + sub.dispose(); + reject(new Error('TIMEOUT')); + }, 15000); }); - await vscode.notebook.openNotebookDocument(uri); + await vscode.workspace.openNotebookDocument(uri); await p; assert.strictEqual(didHappen, true); }); @@ -111,7 +116,7 @@ suite('Notebook Document', function () { test('notebook open/close, all cell-documents are ready', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const p = utils.asPromise(vscode.notebook.onDidOpenNotebookDocument).then(notebook => { + const p = utils.asPromise(vscode.workspace.onDidOpenNotebookDocument).then(notebook => { for (let i = 0; i < notebook.cellCount; i++) { let cell = notebook.cellAt(i); @@ -125,32 +130,56 @@ suite('Notebook Document', function () { } }); - await vscode.notebook.openNotebookDocument(uri); + await vscode.workspace.openNotebookDocument(uri); await p; }); + test('open untitled notebook', async function () { + const nb = await vscode.workspace.openNotebookDocument('notebook.nbdserializer'); + assert.strictEqual(nb.isUntitled, true); + assert.strictEqual(nb.isClosed, false); + assert.strictEqual(nb.uri.scheme, 'untitled'); + // assert.strictEqual(nb.cellCount, 0); // NotebookSerializer ALWAYS returns something here + }); + + test('open untitled with data', async function () { + const nb = await vscode.workspace.openNotebookDocument( + 'notebook.nbdserializer', + new vscode.NotebookData([ + new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'console.log()', 'javascript'), + new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Hey', 'markdown'), + ]) + ); + assert.strictEqual(nb.isUntitled, true); + assert.strictEqual(nb.isClosed, false); + assert.strictEqual(nb.uri.scheme, 'untitled'); + assert.strictEqual(nb.cellCount, 2); + assert.strictEqual(nb.cellAt(0).kind, vscode.NotebookCellKind.Code); + assert.strictEqual(nb.cellAt(1).kind, vscode.NotebookCellKind.Markup); + }); + test('workspace edit API (replaceCells)', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const document = await vscode.notebook.openNotebookDocument(uri); + const document = await vscode.workspace.openNotebookDocument(uri); assert.strictEqual(document.cellCount, 1); // inserting two new cells { const edit = new vscode.WorkspaceEdit(); edit.replaceNotebookCells(document.uri, new vscode.NotebookRange(0, 0), [{ - kind: vscode.NotebookCellKind.Markdown, - language: 'markdown', + kind: vscode.NotebookCellKind.Markup, + languageId: 'markdown', metadata: undefined, outputs: [], - source: 'new_markdown' + value: 'new_markdown' }, { kind: vscode.NotebookCellKind.Code, - language: 'fooLang', + languageId: 'fooLang', metadata: undefined, outputs: [], - source: 'new_code' + value: 'new_code' }]); const success = await vscode.workspace.applyEdit(edit); @@ -177,17 +206,17 @@ suite('Notebook Document', function () { { const edit = new vscode.WorkspaceEdit(); edit.replaceNotebookCells(document.uri, new vscode.NotebookRange(0, 1), [{ - kind: vscode.NotebookCellKind.Markdown, - language: 'markdown', + kind: vscode.NotebookCellKind.Markup, + languageId: 'markdown', metadata: undefined, outputs: [], - source: 'new2_markdown' + value: 'new2_markdown' }, { kind: vscode.NotebookCellKind.Code, - language: 'fooLang', + languageId: 'fooLang', metadata: undefined, outputs: [], - source: 'new2_code' + value: 'new2_code' }]); const success = await vscode.workspace.applyEdit(edit); assert.strictEqual(success, true); @@ -208,25 +237,25 @@ suite('Notebook Document', function () { test('workspace edit API (replaceCells, event)', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const document = await vscode.notebook.openNotebookDocument(uri); + const document = await vscode.workspace.openNotebookDocument(uri); assert.strictEqual(document.cellCount, 1); const edit = new vscode.WorkspaceEdit(); edit.replaceNotebookCells(document.uri, new vscode.NotebookRange(0, 0), [{ - kind: vscode.NotebookCellKind.Markdown, - language: 'markdown', + kind: vscode.NotebookCellKind.Markup, + languageId: 'markdown', metadata: undefined, outputs: [], - source: 'new_markdown' + value: 'new_markdown' }, { kind: vscode.NotebookCellKind.Code, - language: 'fooLang', + languageId: 'fooLang', metadata: undefined, outputs: [], - source: 'new_code' + value: 'new_code' }]); - const event = utils.asPromise(vscode.notebook.onDidChangeNotebookCells); + const event = utils.asPromise(vscode.notebooks.onDidChangeNotebookCells); const success = await vscode.workspace.applyEdit(edit); assert.strictEqual(success, true); @@ -250,27 +279,27 @@ suite('Notebook Document', function () { test('document save API', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const notebook = await vscode.notebook.openNotebookDocument(uri); + const notebook = await vscode.workspace.openNotebookDocument(uri); assert.strictEqual(notebook.uri.toString(), uri.toString()); assert.strictEqual(notebook.isDirty, false); assert.strictEqual(notebook.isUntitled, false); assert.strictEqual(notebook.cellCount, 1); - assert.strictEqual(notebook.viewType, 'notebook.nbdtest'); + assert.strictEqual(notebook.notebookType, 'notebook.nbdtest'); const edit = new vscode.WorkspaceEdit(); edit.replaceNotebookCells(notebook.uri, new vscode.NotebookRange(0, 0), [{ - kind: vscode.NotebookCellKind.Markdown, - language: 'markdown', + kind: vscode.NotebookCellKind.Markup, + languageId: 'markdown', metadata: undefined, outputs: [], - source: 'new_markdown' + value: 'new_markdown' }, { kind: vscode.NotebookCellKind.Code, - language: 'fooLang', + languageId: 'fooLang', metadata: undefined, outputs: [], - source: 'new_code' + value: 'new_code' }]); const success = await vscode.workspace.applyEdit(edit); @@ -285,7 +314,7 @@ suite('Notebook Document', function () { test('setTextDocumentLanguage for notebook cells', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const notebook = await vscode.notebook.openNotebookDocument(uri); + const notebook = await vscode.workspace.openNotebookDocument(uri); const first = notebook.cellAt(0); assert.strictEqual(first.document.languageId, 'javascript'); @@ -305,7 +334,7 @@ suite('Notebook Document', function () { test('setTextDocumentLanguage when notebook editor is not open', async function () { const uri = await utils.createRandomFile('', undefined, '.nbdtest'); - const notebook = await vscode.notebook.openNotebookDocument(uri); + const notebook = await vscode.workspace.openNotebookDocument(uri); const firstCelUri = notebook.cellAt(0).document.uri; await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); @@ -316,7 +345,7 @@ suite('Notebook Document', function () { test('dirty state - complex', async function () { const resource = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const document = await vscode.notebook.openNotebookDocument(resource); + const document = await vscode.workspace.openNotebookDocument(resource); assert.strictEqual(document.isDirty, false); const edit = new vscode.WorkspaceEdit(); @@ -331,7 +360,7 @@ suite('Notebook Document', function () { test('dirty state - serializer', async function () { const resource = await utils.createRandomFile(undefined, undefined, '.nbdserializer'); - const document = await vscode.notebook.openNotebookDocument(resource); + const document = await vscode.workspace.openNotebookDocument(resource); assert.strictEqual(document.isDirty, false); const edit = new vscode.WorkspaceEdit(); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts index 11792c4f5bbc..c4b290e984b7 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.editor.test.ts @@ -13,7 +13,6 @@ suite('Notebook Editor', function () { deserializeNotebook() { return new vscode.NotebookData( [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '// code cell', 'javascript')], - new vscode.NotebookDocumentMetadata() ); } serializeNotebook() { @@ -30,21 +29,19 @@ suite('Notebook Editor', function () { utils.disposeAll(disposables); disposables.length = 0; - for (let doc of vscode.notebook.notebookDocuments) { + for (let doc of vscode.workspace.notebookDocuments) { assert.strictEqual(doc.isDirty, false, doc.uri.toString()); } }); suiteSetup(function () { - disposables.push(vscode.notebook.registerNotebookSerializer('notebook.nbdtest', contentSerializer)); + disposables.push(vscode.workspace.registerNotebookSerializer('notebook.nbdtest', contentSerializer)); }); test('showNotebookDocment', async function () { - const count1 = vscode.notebook.notebookDocuments.length; - - const p = utils.asPromise(vscode.notebook.onDidOpenNotebookDocument); + const p = utils.asPromise(vscode.workspace.onDidOpenNotebookDocument); const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); const editor = await vscode.window.showNotebookDocument(uri); @@ -53,12 +50,12 @@ suite('Notebook Editor', function () { const event = await p; assert.strictEqual(event.uri.toString(), uri.toString()); - const count2 = vscode.notebook.notebookDocuments.length; - assert.strictEqual(count1 + 1, count2); - + const includes = vscode.workspace.notebookDocuments.includes(editor.document); + assert.strictEqual(true, includes); }); - test('notebook editor has viewColumn', async function () { + // TODO@rebornix deal with getting started + test.skip('notebook editor has viewColumn', async function () { const uri1 = await utils.createRandomFile(undefined, undefined, '.nbdtest'); const editor1 = await vscode.window.showNotebookDocument(uri1); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 1aff42f1ea64..0e8dc300ed46 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -7,49 +7,20 @@ import 'mocha'; import * as assert from 'assert'; import * as vscode from 'vscode'; import { createRandomFile, asPromise, disposeAll, closeAllEditors, revertAllDirty, saveAllEditors, assertNoRpc } from '../utils'; +import { TextDecoder } from 'util'; async function createRandomNotebookFile() { return createRandomFile('', undefined, '.vsctestnb'); } -// Since `workbench.action.splitEditor` command does await properly -// Notebook editor/document events are not guaranteed to be sent to the ext host when promise resolves -// The workaround here is waiting for the first visible notebook editor change event. -async function splitEditor() { - const once = asPromise(vscode.window.onDidChangeVisibleNotebookEditors); - await vscode.commands.executeCommand('workbench.action.splitEditor'); - await once; +async function openRandomNotebookDocument() { + const uri = await createRandomNotebookFile(); + return vscode.workspace.openNotebookDocument(uri); } -async function saveFileAndCloseAll(resource: vscode.Uri) { - const documentClosed = new Promise((resolve, _reject) => { - const d = vscode.notebook.onDidCloseNotebookDocument(e => { - if (e.uri.toString() === resource.toString()) { - d.dispose(); - resolve(); - } - }); - }); - await vscode.commands.executeCommand('workbench.action.files.save'); - await closeAllEditors(); - await documentClosed; -} - -async function saveAllFilesAndCloseAll(resource: vscode.Uri | undefined) { - const documentClosed = new Promise((resolve, _reject) => { - if (!resource) { - return resolve(); - } - const d = vscode.notebook.onDidCloseNotebookDocument(e => { - if (e.uri.toString() === resource.toString()) { - d.dispose(); - resolve(); - } - }); - }); - await vscode.commands.executeCommand('workbench.action.files.saveAll'); +async function saveAllFilesAndCloseAll() { + await saveAllEditors(); await closeAllEditors(); - await documentClosed; } async function withEvent(event: vscode.Event, callback: (e: Promise) => Promise) { @@ -62,11 +33,20 @@ class Kernel { readonly controller: vscode.NotebookController; + readonly associatedNotebooks = new Set(); + constructor(id: string, label: string) { - this.controller = vscode.notebook.createNotebookController(id, 'notebookCoreTest', label); + this.controller = vscode.notebooks.createNotebookController(id, 'notebookCoreTest', label); this.controller.executeHandler = this._execute.bind(this); - this.controller.hasExecutionOrder = true; + this.controller.supportsExecutionOrder = true; this.controller.supportedLanguages = ['typescript', 'javascript']; + this.controller.onDidChangeSelectedNotebooks(e => { + if (e.selected) { + this.associatedNotebooks.add(e.notebook.uri.toString()); + } else { + this.associatedNotebooks.delete(e.notebook.uri.toString()); + } + }); } protected async _execute(cells: vscode.NotebookCell[]): Promise { @@ -76,20 +56,20 @@ class Kernel { } protected async _runCell(cell: vscode.NotebookCell) { - const task = this.controller.createNotebookCellExecutionTask(cell); + const task = this.controller.createNotebookCellExecution(cell); task.start(); task.executionOrder = 1; if (cell.notebook.uri.path.endsWith('customRenderer.vsctestnb')) { await task.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/custom', ['test'], undefined) + vscode.NotebookCellOutputItem.text('test', 'text/custom') ])]); return; } await task.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) + vscode.NotebookCellOutputItem.text('my output', 'text/plain') ])]); - task.end({ success: true }); + task.end(true); } } @@ -98,12 +78,13 @@ function getFocusedCell(editor?: vscode.NotebookEditor) { return editor ? editor.document.cellAt(editor.selections[0].start) : undefined; } -async function assertKernel(controller: vscode.NotebookController): Promise { +async function assertKernel(kernel: Kernel, notebook: vscode.NotebookDocument): Promise { const success = await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', - id: controller.id + id: kernel.controller.id }); - assert.ok(success, `expected selected kernel to be ${controller.id}`); + assert.ok(success, `expected selected kernel to be ${kernel.controller.id}`); + assert.ok(kernel.associatedNotebooks.has(notebook.uri.toString())); } suite('Notebook API tests', function () { @@ -123,38 +104,41 @@ suite('Notebook API tests', function () { }); suiteSetup(function () { - suiteDisposables.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { - openNotebook: async (_resource: vscode.Uri): Promise => { - if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { + suiteDisposables.push(vscode.workspace.registerNotebookContentProvider('notebookCoreTest', { + openNotebook: async (resource: vscode.Uri): Promise => { + if (/.*empty\-.*\.vsctestnb$/.test(resource.path)) { return { - metadata: new vscode.NotebookDocumentMetadata(), + metadata: {}, cells: [] }; } const dto: vscode.NotebookData = { - metadata: new vscode.NotebookDocumentMetadata().with({ custom: { testMetadata: false } }), + metadata: { custom: { testMetadata: false } }, cells: [ { - source: 'test', - language: 'typescript', + value: 'test', + languageId: 'typescript', kind: vscode.NotebookCellKind.Code, outputs: [], - metadata: new vscode.NotebookCellMetadata().with({ custom: { testCellMetadata: 123 } }), - latestExecutionSummary: { startTime: 10, endTime: 20 } + metadata: { custom: { testCellMetadata: 123 } }, + executionSummary: { timing: { startTime: 10, endTime: 20 } } }, { - source: 'test2', - language: 'typescript', + value: 'test2', + languageId: 'typescript', kind: vscode.NotebookCellKind.Code, outputs: [ new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', 'Hello World', { testOutputItemMetadata: true }) + vscode.NotebookCellOutputItem.text('Hello World', 'text/plain') ], - { testOutputMetadata: true }) + { + testOutputMetadata: true, + ['text/plain']: { testOutputItemMetadata: true } + }) ], - latestExecutionSummary: { executionOrder: 5, success: true }, - metadata: new vscode.NotebookCellMetadata().with({ custom: { testCellMetadata: 456 } }) + executionSummary: { executionOrder: 5, success: true }, + metadata: { custom: { testCellMetadata: 456 } } } ] }; @@ -178,12 +162,12 @@ suite('Notebook API tests', function () { let kernel1: Kernel; let kernel2: Kernel; - setup(() => { + setup(async function () { kernel1 = new Kernel('mainKernel', 'Notebook Primary Test Kernel'); - const listener = vscode.notebook.onDidOpenNotebookDocument(async notebook => { - if (notebook.viewType === kernel1.controller.viewType) { + const listener = vscode.workspace.onDidOpenNotebookDocument(async notebook => { + if (notebook.notebookType === kernel1.controller.notebookType) { await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: kernel1.controller.id @@ -195,52 +179,51 @@ suite('Notebook API tests', function () { kernel2 = new class extends Kernel { constructor() { super('secondaryKernel', 'Notebook Secondary Test Kernel'); - this.controller.hasExecutionOrder = false; + this.controller.supportsExecutionOrder = false; } override async _runCell(cell: vscode.NotebookCell) { - const task = this.controller.createNotebookCellExecutionTask(cell); + const task = this.controller.createNotebookCellExecution(cell); task.start(); await task.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) + vscode.NotebookCellOutputItem.text('my second output', 'text/plain') ])]); - task.end({ success: true }); + task.end(true); } }; testDisposables.push(kernel1.controller, listener, kernel2.controller); + await saveAllFilesAndCloseAll(); }); - teardown(() => { + teardown(async function () { disposeAll(testDisposables); testDisposables.length = 0; + await saveAllFilesAndCloseAll(); }); test('shared document in notebook editors', async function () { - const resource = await createRandomNotebookFile(); let counter = 0; - const disposables: vscode.Disposable[] = []; - disposables.push(vscode.notebook.onDidOpenNotebookDocument(() => { + testDisposables.push(vscode.workspace.onDidOpenNotebookDocument(() => { counter++; })); - disposables.push(vscode.notebook.onDidCloseNotebookDocument(() => { - counter--; - })); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const notebook = await openRandomNotebookDocument(); assert.strictEqual(counter, 1); - await splitEditor(); + await vscode.window.showNotebookDocument(notebook, { viewColumn: vscode.ViewColumn.Active }); assert.strictEqual(counter, 1); - await closeAllEditors(); - assert.strictEqual(counter, 0); + assert.strictEqual(vscode.window.visibleNotebookEditors.length, 1); - disposables.forEach(d => d.dispose()); + await vscode.window.showNotebookDocument(notebook, { viewColumn: vscode.ViewColumn.Beside }); + assert.strictEqual(counter, 1); + assert.strictEqual(vscode.window.visibleNotebookEditors.length, 2); }); - test('editor open/close event', async function () { + test('editor onDidChangeVisibleNotebookEditors-event', async function () { const resource = await createRandomNotebookFile(); const firstEditorOpen = asPromise(vscode.window.onDidChangeVisibleNotebookEditors); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.window.showNotebookDocument(resource); await firstEditorOpen; const firstEditorClose = asPromise(vscode.window.onDidChangeVisibleNotebookEditors); @@ -248,7 +231,7 @@ suite('Notebook API tests', function () { await firstEditorClose; }); - test('editor open/close event 2', async function () { + test('editor onDidChangeVisibleNotebookEditors-event 2', async function () { const resource = await createRandomNotebookFile(); let count = 0; const disposables: vscode.Disposable[] = []; @@ -256,10 +239,10 @@ suite('Notebook API tests', function () { count = vscode.window.visibleNotebookEditors.length; })); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.window.showNotebookDocument(resource, { viewColumn: vscode.ViewColumn.Active }); assert.strictEqual(count, 1); - await splitEditor(); + await vscode.window.showNotebookDocument(resource, { viewColumn: vscode.ViewColumn.Beside }); assert.strictEqual(count, 2); await closeAllEditors(); @@ -289,7 +272,7 @@ suite('Notebook API tests', function () { const resource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); + const cellsChangeEvent = asPromise(vscode.notebooks.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); const cellChangeEventRet = await cellsChangeEvent; assert.strictEqual(cellChangeEventRet.document, vscode.window.activeNotebookEditor?.document); @@ -303,11 +286,11 @@ suite('Notebook API tests', function () { ] }); - const moveCellEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); + const moveCellEvent = asPromise(vscode.notebooks.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.moveUp'); await moveCellEvent; - const cellOutputChange = asPromise(vscode.notebook.onDidChangeCellOutputs); + const cellOutputChange = asPromise(vscode.notebooks.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.execute'); const cellOutputsAddedRet = await cellOutputChange; assert.deepStrictEqual(cellOutputsAddedRet, { @@ -316,7 +299,7 @@ suite('Notebook API tests', function () { }); assert.strictEqual(cellOutputsAddedRet.cells[0].outputs.length, 1); - const cellOutputClear = asPromise(vscode.notebook.onDidChangeCellOutputs); + const cellOutputClear = asPromise(vscode.notebooks.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.clearOutputs'); const cellOutputsCleardRet = await cellOutputClear; assert.deepStrictEqual(cellOutputsCleardRet, { @@ -325,7 +308,7 @@ suite('Notebook API tests', function () { }); assert.strictEqual(cellOutputsAddedRet.cells[0].outputs.length, 0); - // const cellChangeLanguage = getEventOncePromise(vscode.notebook.onDidChangeCellLanguage); + // const cellChangeLanguage = getEventOncePromise(vscode.notebooks.onDidChangeCellLanguage); // await vscode.commands.executeCommand('notebook.cell.changeToMarkdown'); // const cellChangeLanguageRet = await cellChangeLanguage; // assert.deepStrictEqual(cellChangeLanguageRet, { @@ -333,8 +316,6 @@ suite('Notebook API tests', function () { // cells: vscode.window.activeNotebookEditor!.document.cellAt(0), // language: 'markdown' // }); - - await saveAllFilesAndCloseAll(undefined); }); test('editor move cell event', async function () { @@ -346,7 +327,7 @@ suite('Notebook API tests', function () { const activeCell = getFocusedCell(vscode.window.activeNotebookEditor); assert.strictEqual(vscode.window.activeNotebookEditor!.document.getCells().indexOf(activeCell!), 0); - const moveChange = asPromise(vscode.notebook.onDidChangeNotebookCells); + const moveChange = asPromise(vscode.notebooks.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.moveDown'); await moveChange; await saveAllEditors(); @@ -355,20 +336,20 @@ suite('Notebook API tests', function () { await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const firstEditor = vscode.window.activeNotebookEditor; assert.strictEqual(firstEditor?.document.cellCount, 2); - await saveAllFilesAndCloseAll(undefined); }); test('notebook editor active/visible', async function () { const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const firstEditor = vscode.window.activeNotebookEditor; - assert.strictEqual(firstEditor && vscode.window.visibleNotebookEditors.indexOf(firstEditor) >= 0, true); + const firstEditor = await vscode.window.showNotebookDocument(resource, { viewColumn: vscode.ViewColumn.Active }); + assert.strictEqual(firstEditor === vscode.window.activeNotebookEditor, true); + assert.strictEqual(vscode.window.visibleNotebookEditors.includes(firstEditor), true); + + const secondEditor = await vscode.window.showNotebookDocument(resource, { viewColumn: vscode.ViewColumn.Beside }); + assert.strictEqual(secondEditor === vscode.window.activeNotebookEditor, true); - await splitEditor(); - const secondEditor = vscode.window.activeNotebookEditor; - assert.strictEqual(secondEditor && vscode.window.visibleNotebookEditors.indexOf(secondEditor) >= 0, true); assert.notStrictEqual(firstEditor, secondEditor); - assert.strictEqual(firstEditor && vscode.window.visibleNotebookEditors.indexOf(firstEditor) >= 0, true); + assert.strictEqual(vscode.window.visibleNotebookEditors.includes(secondEditor), true); + assert.strictEqual(vscode.window.visibleNotebookEditors.includes(firstEditor), true); assert.strictEqual(vscode.window.visibleNotebookEditors.length, 2); const untitledEditorChange = asPromise(vscode.window.onDidChangeActiveNotebookEditor); @@ -386,8 +367,6 @@ suite('Notebook API tests', function () { assert.strictEqual(secondEditor, vscode.window.activeNotebookEditor); assert.strictEqual(vscode.window.visibleNotebookEditors.length, 2); assert.strictEqual(secondEditor && vscode.window.visibleNotebookEditors.indexOf(secondEditor) >= 0, true); - - await saveAllFilesAndCloseAll(undefined); }); test('notebook active editor change', async function () { @@ -399,8 +378,6 @@ suite('Notebook API tests', function () { const firstEditorDeactivate = asPromise(vscode.window.onDidChangeActiveNotebookEditor); await vscode.commands.executeCommand('workbench.action.splitEditor'); await firstEditorDeactivate; - - await saveFileAndCloseAll(resource); }); test('edit API (replaceMetadata)', async function () { @@ -408,7 +385,7 @@ suite('Notebook API tests', function () { await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.window.activeNotebookEditor!.edit(editBuilder => { - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true })); + editBuilder.replaceCellMetadata(0, { inputCollapsed: true }); }); const document = vscode.window.activeNotebookEditor?.document!; @@ -416,17 +393,16 @@ suite('Notebook API tests', function () { assert.strictEqual(document.cellAt(0).metadata.inputCollapsed, true); assert.strictEqual(document.isDirty, true); - await saveFileAndCloseAll(resource); }); test('edit API (replaceMetadata, event)', async function () { const resource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const event = asPromise(vscode.notebook.onDidChangeCellMetadata); + const event = asPromise(vscode.notebooks.onDidChangeCellMetadata); await vscode.window.activeNotebookEditor!.edit(editBuilder => { - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true })); + editBuilder.replaceCellMetadata(0, { inputCollapsed: true }); }); const data = await event; @@ -434,67 +410,59 @@ suite('Notebook API tests', function () { assert.strictEqual(data.cell.metadata.inputCollapsed, true); assert.strictEqual(data.document.isDirty, true); - await saveFileAndCloseAll(resource); }); test('edit API batch edits', async function () { const resource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); - const cellMetadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); + const cellsChangeEvent = asPromise(vscode.notebooks.onDidChangeNotebookCells); + const cellMetadataChangeEvent = asPromise(vscode.notebooks.onDidChangeCellMetadata); const version = vscode.window.activeNotebookEditor!.document.version; await vscode.window.activeNotebookEditor!.edit(editBuilder => { - editBuilder.replaceCells(1, 0, [{ kind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: false })); + editBuilder.replaceCells(1, 0, [{ kind: vscode.NotebookCellKind.Code, languageId: 'javascript', value: 'test 2', outputs: [], metadata: undefined }]); + editBuilder.replaceCellMetadata(0, { inputCollapsed: false }); }); await cellsChangeEvent; await cellMetadataChangeEvent; assert.strictEqual(version + 1, vscode.window.activeNotebookEditor!.document.version); - await saveAllFilesAndCloseAll(resource); }); test('edit API batch edits undo/redo', async function () { const resource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); - const cellMetadataChangeEvent = asPromise(vscode.notebook.onDidChangeCellMetadata); + const cellsChangeEvent = asPromise(vscode.notebooks.onDidChangeNotebookCells); + const cellMetadataChangeEvent = asPromise(vscode.notebooks.onDidChangeCellMetadata); const version = vscode.window.activeNotebookEditor!.document.version; await vscode.window.activeNotebookEditor!.edit(editBuilder => { - editBuilder.replaceCells(1, 0, [{ kind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: false })); + editBuilder.replaceCells(1, 0, [{ kind: vscode.NotebookCellKind.Code, languageId: 'javascript', value: 'test 2', outputs: [], metadata: undefined }]); + editBuilder.replaceCellMetadata(0, { inputCollapsed: false }); }); await cellsChangeEvent; await cellMetadataChangeEvent; assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellCount, 3); - assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellAt(0)?.metadata?.inputCollapsed, false); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellAt(0)?.metadata.inputCollapsed, false); assert.strictEqual(version + 1, vscode.window.activeNotebookEditor!.document.version); await vscode.commands.executeCommand('undo'); assert.strictEqual(version + 2, vscode.window.activeNotebookEditor!.document.version); - assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellAt(0)?.metadata?.inputCollapsed, undefined); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellAt(0)?.metadata.inputCollapsed, undefined); assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellCount, 2); - - await saveAllFilesAndCloseAll(resource); }); test('initialzation should not emit cell change events.', async function () { const resource = await createRandomNotebookFile(); let count = 0; - const disposables: vscode.Disposable[] = []; - disposables.push(vscode.notebook.onDidChangeNotebookCells(() => { + + testDisposables.push(vscode.notebooks.onDidChangeNotebookCells(() => { count++; })); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.strictEqual(count, 0); - - disposables.forEach(d => d.dispose()); - - await saveFileAndCloseAll(resource); }); // }); @@ -509,20 +477,19 @@ suite('Notebook API tests', function () { const secondCell = vscode.window.activeNotebookEditor!.document.cellAt(1); assert.strictEqual(secondCell!.outputs.length, 1); - assert.deepStrictEqual(secondCell!.outputs[0].metadata, { testOutputMetadata: true }); - assert.strictEqual(secondCell!.outputs[0].outputs.length, 1); - assert.strictEqual(secondCell!.outputs[0].outputs[0].mime, 'text/plain'); - assert.strictEqual(secondCell!.outputs[0].outputs[0].value, 'Hello World'); - assert.deepStrictEqual(secondCell!.outputs[0].outputs[0].metadata, { testOutputItemMetadata: true }); - assert.strictEqual(secondCell!.latestExecutionSummary?.executionOrder, 5); - assert.strictEqual(secondCell!.latestExecutionSummary?.success, true); + assert.deepStrictEqual(secondCell!.outputs[0].metadata, { testOutputMetadata: true, ['text/plain']: { testOutputItemMetadata: true } }); + assert.strictEqual(secondCell!.outputs[0].items.length, 1); + assert.strictEqual(secondCell!.outputs[0].items[0].mime, 'text/plain'); + assert.strictEqual(new TextDecoder().decode(secondCell!.outputs[0].items[0].data), 'Hello World'); + assert.strictEqual(secondCell!.executionSummary?.executionOrder, 5); + assert.strictEqual(secondCell!.executionSummary?.success, true); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor)?.document.getText(), ''); await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); const activeCell = getFocusedCell(vscode.window.activeNotebookEditor); - assert.notEqual(getFocusedCell(vscode.window.activeNotebookEditor), undefined); + assert.notStrictEqual(getFocusedCell(vscode.window.activeNotebookEditor), undefined); assert.strictEqual(activeCell!.document.getText(), ''); assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellCount, 4); assert.strictEqual(vscode.window.activeNotebookEditor!.document.getCells().indexOf(activeCell!), 1); @@ -545,7 +512,7 @@ suite('Notebook API tests', function () { // ---- insert cell above and focus ---- // await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); let activeCell = getFocusedCell(vscode.window.activeNotebookEditor); - assert.notEqual(getFocusedCell(vscode.window.activeNotebookEditor), undefined); + assert.notStrictEqual(getFocusedCell(vscode.window.activeNotebookEditor), undefined); assert.strictEqual(activeCell!.document.getText(), ''); assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellCount, 4); assert.strictEqual(vscode.window.activeNotebookEditor!.document.getCells().indexOf(activeCell!), 1); @@ -617,7 +584,7 @@ suite('Notebook API tests', function () { edit.insert(getFocusedCell(vscode.window.activeNotebookEditor)!.document.uri, new vscode.Position(0, 0), 'var abc = 0;'); await vscode.workspace.applyEdit(edit); - const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); + const cellsChangeEvent = asPromise(vscode.notebooks.onDidChangeNotebookCells); await vscode.commands.executeCommand('notebook.cell.joinAbove'); await cellsChangeEvent; @@ -641,8 +608,6 @@ suite('Notebook API tests', function () { const newActiveCell = getFocusedCell(vscode.window.activeNotebookEditor); assert.deepStrictEqual(activeCell, newActiveCell); - - await saveFileAndCloseAll(resource); }); // test('document runnable based on kernel count', async () => { @@ -660,7 +625,7 @@ suite('Notebook API tests', function () { // currentKernelProvider.setHasKernels(true); - // await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + // await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { // await vscode.commands.executeCommand('notebook.execute'); // await event; // assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked @@ -680,8 +645,6 @@ suite('Notebook API tests', function () { await vscode.commands.executeCommand('notebook.execute'); assert.strictEqual(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work - - await saveAllFilesAndCloseAll(undefined); }); test('cell execute command takes arguments 2', async () => { @@ -691,13 +654,13 @@ suite('Notebook API tests', function () { const editor = vscode.window.activeNotebookEditor!; const cell = editor.document.cellAt(0); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { await vscode.commands.executeCommand('notebook.execute'); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked }); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async event => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async event => { await vscode.commands.executeCommand('notebook.cell.clearOutputs'); await event; assert.strictEqual(cell.outputs.length, 0, 'should clear'); @@ -706,14 +669,42 @@ suite('Notebook API tests', function () { const secondResource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { await vscode.commands.executeCommand('notebook.cell.execute', { start: 0, end: 1 }, resource); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked assert.strictEqual(vscode.window.activeNotebookEditor?.document.uri.fsPath, secondResource.fsPath); }); + }); - await saveAllFilesAndCloseAll(undefined); + test('cell execute command takes arguments ICellRange[]', async () => { + const resource = await createRandomNotebookFile(); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + vscode.commands.executeCommand('notebook.cell.execute', { ranges: [{ start: 0, end: 1 }, { start: 1, end: 2 }] }); + let firstCellExecuted = false; + let secondCellExecuted = false; + let resolve: () => void; + const p = new Promise(r => resolve = r); + const listener = vscode.notebooks.onDidChangeCellOutputs(e => { + e.cells.forEach(cell => { + if (cell.index === 0) { + firstCellExecuted = true; + } + + if (cell.index === 1) { + secondCellExecuted = true; + } + }); + + if (firstCellExecuted && secondCellExecuted) { + resolve(); + } + }); + + await p; + listener.dispose(); + await saveAllFilesAndCloseAll(); }); test('document execute command takes arguments', async () => { @@ -723,13 +714,13 @@ suite('Notebook API tests', function () { const editor = vscode.window.activeNotebookEditor!; const cell = editor.document.cellAt(0); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { await vscode.commands.executeCommand('notebook.execute'); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked }); - const clearChangeEvent = asPromise(vscode.notebook.onDidChangeCellOutputs); + const clearChangeEvent = asPromise(vscode.notebooks.onDidChangeCellOutputs); await vscode.commands.executeCommand('notebook.cell.clearOutputs'); await clearChangeEvent; assert.strictEqual(cell.outputs.length, 0, 'should clear'); @@ -737,47 +728,40 @@ suite('Notebook API tests', function () { const secondResource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { await vscode.commands.executeCommand('notebook.execute', resource); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked assert.strictEqual(vscode.window.activeNotebookEditor?.document.uri.fsPath, secondResource.fsPath); }); - - await saveAllFilesAndCloseAll(undefined); }); test('cell execute and select kernel', async function () { - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - assert.strictEqual(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); - const editor = vscode.window.activeNotebookEditor!; + const notebook = await openRandomNotebookDocument(); + const editor = await vscode.window.showNotebookDocument(notebook); + assert.strictEqual(vscode.window.activeNotebookEditor === editor, true, 'notebook first'); + const cell = editor.document.cellAt(0); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { + await assertKernel(kernel1, notebook); await vscode.commands.executeCommand('notebook.cell.execute'); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked - assert.strictEqual(cell.outputs[0].outputs.length, 1); - assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); - assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ - 'my output' - ]); + assert.strictEqual(cell.outputs[0].items.length, 1); + assert.strictEqual(cell.outputs[0].items[0].mime, 'text/plain'); + assert.deepStrictEqual(new TextDecoder().decode(cell.outputs[0].items[0].data), 'my output'); }); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { - await assertKernel(kernel2.controller); + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { + await assertKernel(kernel2, notebook); await vscode.commands.executeCommand('notebook.cell.execute'); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked - assert.strictEqual(cell.outputs[0].outputs.length, 1); - assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); - assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ - 'my second output' - ]); + assert.strictEqual(cell.outputs[0].items.length, 1); + assert.strictEqual(cell.outputs[0].items[0].mime, 'text/plain'); + assert.deepStrictEqual(new TextDecoder().decode(cell.outputs[0].items[0].data), 'my second output'); }); - - await saveAllFilesAndCloseAll(undefined); }); test('set outputs on cancel', async () => { @@ -790,87 +774,78 @@ suite('Notebook API tests', function () { override async _execute(cells: vscode.NotebookCell[]) { for (const cell of cells) { - const task = this.controller.createNotebookCellExecutionTask(cell); + const task = this.controller.createNotebookCellExecution(cell); task.start(); task.token.onCancellationRequested(async () => { await task.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['Canceled'], undefined) + vscode.NotebookCellOutputItem.text('Canceled', 'text/plain') ])]); - task.end({}); + task.end(undefined); }); } } }; - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const editor = vscode.window.activeNotebookEditor!; + const notebook = await openRandomNotebookDocument(); + const editor = await vscode.window.showNotebookDocument(notebook); const cell = editor.document.cellAt(0); - await assertKernel(cancelableKernel.controller); - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { + await assertKernel(cancelableKernel, notebook); + assert.ok(editor === vscode.window.activeNotebookEditor); await vscode.commands.executeCommand('notebook.cell.execute'); await vscode.commands.executeCommand('notebook.cell.cancelExecution'); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked - assert.strictEqual(cell.outputs[0].outputs.length, 1); - assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); - assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ - 'Canceled' - ]); + assert.strictEqual(cell.outputs[0].items.length, 1); + assert.strictEqual(cell.outputs[0].items[0].mime, 'text/plain'); + assert.deepStrictEqual(new TextDecoder().decode(cell.outputs[0].items[0].data), 'Canceled'); }); cancelableKernel.controller.dispose(); - await saveAllFilesAndCloseAll(undefined); }); test('set outputs on interrupt', async () => { const interruptableKernel = new class extends Kernel { - constructor() { super('interruptableKernel', 'Notebook Interruptable Test Kernel'); this.controller.interruptHandler = this.interrupt.bind(this); } - private _task: vscode.NotebookCellExecutionTask | undefined; + private _task: vscode.NotebookCellExecution | undefined; override async _execute(cells: vscode.NotebookCell[]) { - this._task = this.controller.createNotebookCellExecutionTask(cells[0]); + this._task = this.controller.createNotebookCellExecution(cells[0]); this._task.start(); } - async interrupt() { await this._task!.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['Interrupted'], undefined) + vscode.NotebookCellOutputItem.text('Interrupted', 'text/plain') ])]); - this._task!.end({}); + this._task!.end(undefined); } }; - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const editor = vscode.window.activeNotebookEditor!; + const notebook = await openRandomNotebookDocument(); + const editor = await vscode.window.showNotebookDocument(notebook); const cell = editor.document.cellAt(0); - await assertKernel(interruptableKernel.controller); - - await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await withEvent(vscode.notebooks.onDidChangeCellOutputs, async (event) => { + await assertKernel(interruptableKernel, notebook); + assert.ok(editor === vscode.window.activeNotebookEditor); await vscode.commands.executeCommand('notebook.cell.execute'); await vscode.commands.executeCommand('notebook.cell.cancelExecution'); await event; assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked - assert.strictEqual(cell.outputs[0].outputs.length, 1); - assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); - assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ - 'Interrupted' - ]); + assert.strictEqual(cell.outputs[0].items.length, 1); + assert.strictEqual(cell.outputs[0].items[0].mime, 'text/plain'); + assert.deepStrictEqual(new TextDecoder().decode(cell.outputs[0].items[0].data), 'Interrupted'); }); interruptableKernel.controller.dispose(); - await saveAllFilesAndCloseAll(undefined); }); test('onDidChangeCellExecutionState is fired', async () => { @@ -883,14 +858,14 @@ suite('Notebook API tests', function () { let eventCount = 0; let resolve: () => void; const p = new Promise(r => resolve = r); - const listener = vscode.notebook.onDidChangeCellExecutionState(e => { + const listener = vscode.notebooks.onDidChangeNotebookCellExecutionState(e => { if (eventCount === 0) { - assert.strictEqual(e.executionState, vscode.NotebookCellExecutionState.Pending, 'should be set to Pending'); + assert.strictEqual(e.state, vscode.NotebookCellExecutionState.Pending, 'should be set to Pending'); } else if (eventCount === 1) { - assert.strictEqual(e.executionState, vscode.NotebookCellExecutionState.Executing, 'should be set to Executing'); + assert.strictEqual(e.state, vscode.NotebookCellExecutionState.Executing, 'should be set to Executing'); assert.strictEqual(cell.outputs.length, 0, 'no outputs yet: ' + JSON.stringify(cell.outputs[0])); } else if (eventCount === 2) { - assert.strictEqual(e.executionState, vscode.NotebookCellExecutionState.Idle, 'should be set to Idle'); + assert.strictEqual(e.state, vscode.NotebookCellExecutionState.Idle, 'should be set to Idle'); assert.strictEqual(cell.outputs.length, 1, 'should have an output'); resolve(); } @@ -900,7 +875,6 @@ suite('Notebook API tests', function () { await p; listener.dispose(); - await saveAllFilesAndCloseAll(undefined); }); // suite('notebook dirty state', () => { @@ -930,8 +904,6 @@ suite('Notebook API tests', function () { assert.deepStrictEqual(vscode.window.activeNotebookEditor?.document.cellAt(1), getFocusedCell(vscode.window.activeNotebookEditor)); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor)?.document.getText(), 'var abc = 0;'); }); - - await saveFileAndCloseAll(resource); }); // }); @@ -974,8 +946,6 @@ suite('Notebook API tests', function () { // assert.strictEqual(vscode.window.activeNotebookEditor!.document.cellCount, 2); // assert.strictEqual(vscode.window.activeNotebookEditor!.document.getCells().indexOf(getFocusedCell(vscode.window.activeNotebookEditor)!), 1); // assert.strictEqual(vscode.window.activeNotebookEditor?.selection?.document.getText(), 'test'); - - await saveFileAndCloseAll(resource); }); test('multiple tabs: dirty + clean', async function () { @@ -999,10 +969,9 @@ suite('Notebook API tests', function () { assert.deepStrictEqual(vscode.window.activeNotebookEditor?.document.cellCount, 4); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor)?.document.getText(), 'var abc = 0;'); - await saveFileAndCloseAll(resource); }); - test('multiple tabs: two dirty tabs and switching', async function () { + test.skip('multiple tabs: two dirty tabs and switching', async function () { const resource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); @@ -1032,27 +1001,26 @@ suite('Notebook API tests', function () { assert.deepStrictEqual(vscode.window.activeNotebookEditor?.document.cellCount, 3); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor)?.document.getText(), ''); - await saveAllFilesAndCloseAll(secondResource); }); - test.skip('multiple tabs: different editors with same document', async function () { - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const firstNotebookEditor = vscode.window.activeNotebookEditor; + test('multiple tabs: different editors with same document', async function () { + + const notebook = await openRandomNotebookDocument(); + const firstNotebookEditor = await vscode.window.showNotebookDocument(notebook, { viewColumn: vscode.ViewColumn.One }); + assert.ok(firstNotebookEditor === vscode.window.activeNotebookEditor); + assert.strictEqual(firstNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor!)?.document.getText(), 'test'); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor!)?.document.languageId, 'typescript'); - await splitEditor(); - const secondNotebookEditor = vscode.window.activeNotebookEditor; + const secondNotebookEditor = await vscode.window.showNotebookDocument(notebook, { viewColumn: vscode.ViewColumn.Beside }); assert.strictEqual(secondNotebookEditor !== undefined, true, 'notebook first'); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor!)?.document.getText(), 'test'); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor!)?.document.languageId, 'typescript'); - assert.notEqual(firstNotebookEditor, secondNotebookEditor); + assert.notStrictEqual(firstNotebookEditor, secondNotebookEditor); assert.strictEqual(firstNotebookEditor?.document, secondNotebookEditor?.document, 'split notebook editors share the same document'); - await saveAllFilesAndCloseAll(resource); }); test('custom metadata should be supported', async function () { @@ -1063,7 +1031,6 @@ suite('Notebook API tests', function () { assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor)?.metadata.custom!['testCellMetadata'] as number, 123); assert.strictEqual(getFocusedCell(vscode.window.activeNotebookEditor)?.document.languageId, 'typescript'); - await saveFileAndCloseAll(resource); }); @@ -1082,42 +1049,37 @@ suite('Notebook API tests', function () { // assert.strictEqual(vscode.window.activeNotebookEditor!.document.getCells().indexOf(activeCell!), 1); // assert.strictEqual(activeCell?.metadata.custom!['testCellMetadata'] as number, 123); - await saveFileAndCloseAll(resource); }); - test('#106657. Opening a notebook from markers view is broken ', async function () { - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + test.skip('#106657. Opening a notebook from markers view is broken ', async function () { - const document = vscode.window.activeNotebookEditor?.document!; + const document = await openRandomNotebookDocument(); const [cell] = document.getCells(); - await saveAllFilesAndCloseAll(document.uri); assert.strictEqual(vscode.window.activeNotebookEditor, undefined); // opening a cell-uri opens a notebook editor - await vscode.commands.executeCommand('vscode.open', cell.document.uri, vscode.ViewColumn.Active); + await vscode.window.showTextDocument(cell.document, { viewColumn: vscode.ViewColumn.Active }); + // await vscode.commands.executeCommand('vscode.open', cell.document.uri, vscode.ViewColumn.Active); assert.strictEqual(!!vscode.window.activeNotebookEditor, true); - assert.strictEqual(vscode.window.activeNotebookEditor!.document.uri.toString(), resource.toString()); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.uri.toString(), document.uri.toString()); }); - test.skip('Cannot open notebook from cell-uri with vscode.open-command', async function () { - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + test('Cannot open notebook from cell-uri with vscode.open-command', async function () { - const document = vscode.window.activeNotebookEditor?.document!; + const document = await openRandomNotebookDocument(); const [cell] = document.getCells(); - await saveAllFilesAndCloseAll(document.uri); + await saveAllFilesAndCloseAll(); assert.strictEqual(vscode.window.activeNotebookEditor, undefined); // BUG is that the editor opener (https://github.com/microsoft/vscode/blob/8e7877bdc442f1e83a7fec51920d82b696139129/src/vs/editor/browser/services/openerService.ts#L69) // removes the fragment if it matches something numeric. For notebooks that's not wanted... await vscode.commands.executeCommand('vscode.open', cell.document.uri); - assert.strictEqual(vscode.window.activeNotebookEditor!.document.uri.toString(), resource.toString()); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.uri.toString(), document.uri.toString()); }); test('#97830, #97764. Support switch to other editor types', async function () { @@ -1137,8 +1099,6 @@ suite('Notebook API tests', function () { await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); assert.strictEqual(vscode.window.activeTextEditor?.document.uri.path, resource.path); - - await closeAllEditors(); }); // open text editor, pin, and then open a notebook @@ -1151,15 +1111,14 @@ suite('Notebook API tests', function () { // now it's dirty, open the resource with notebook editor should open a new one await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - assert.notEqual(vscode.window.activeNotebookEditor, undefined, 'notebook first'); - // assert.notEqual(vscode.window.activeTextEditor, undefined); + assert.notStrictEqual(vscode.window.activeNotebookEditor, undefined, 'notebook first'); + // assert.notStrictEqual(vscode.window.activeTextEditor, undefined); - await closeAllEditors(); }); test('#102411 - untitled notebook creation failed', async function () { await vscode.commands.executeCommand('workbench.action.files.newUntitledFile', { viewType: 'notebookCoreTest' }); - assert.notEqual(vscode.window.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); + assert.notStrictEqual(vscode.window.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); await closeAllEditors(); }); @@ -1182,26 +1141,26 @@ suite('Notebook API tests', function () { await vscode.workspace.applyEdit(edit); assert.strictEqual(vscode.window.activeNotebookEditor!.document.getCells().length, 3); - assert.notEqual(vscode.window.activeNotebookEditor!.document.cellAt(0).document.getText(), vscode.window.activeNotebookEditor!.document.cellAt(1).document.getText()); + assert.notStrictEqual(vscode.window.activeNotebookEditor!.document.cellAt(0).document.getText(), vscode.window.activeNotebookEditor!.document.cellAt(1).document.getText()); await closeAllEditors(); }); test('#115855 onDidSaveNotebookDocument', async function () { const resource = await createRandomNotebookFile(); - const notebook = await vscode.notebook.openNotebookDocument(resource); + const notebook = await vscode.workspace.openNotebookDocument(resource); const editor = await vscode.window.showNotebookDocument(notebook); - const cellsChangeEvent = asPromise(vscode.notebook.onDidChangeNotebookCells); + const cellsChangeEvent = asPromise(vscode.notebooks.onDidChangeNotebookCells); await editor.edit(editBuilder => { - editBuilder.replaceCells(1, 0, [{ kind: vscode.NotebookCellKind.Code, language: 'javascript', source: 'test 2', outputs: [], metadata: undefined }]); + editBuilder.replaceCells(1, 0, [{ kind: vscode.NotebookCellKind.Code, languageId: 'javascript', value: 'test 2', outputs: [], metadata: undefined }]); }); const cellChangeEventRet = await cellsChangeEvent; assert.strictEqual(cellChangeEventRet.document === notebook, true); assert.strictEqual(cellChangeEventRet.document.isDirty, true); - const saveEvent = asPromise(vscode.notebook.onDidSaveNotebookDocument); + const saveEvent = asPromise(vscode.notebooks.onDidSaveNotebookDocument); await notebook.save(); @@ -1210,6 +1169,9 @@ suite('Notebook API tests', function () { }); test('Output changes are applied once the promise resolves', async function () { + + let called = false; + const verifyOutputSyncKernel = new class extends Kernel { constructor() { @@ -1218,54 +1180,51 @@ suite('Notebook API tests', function () { override async _execute(cells: vscode.NotebookCell[]) { const [cell] = cells; - const task = this.controller.createNotebookCellExecutionTask(cell); + const task = this.controller.createNotebookCellExecution(cell); task.start(); await task.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['Some output'], undefined) + vscode.NotebookCellOutputItem.text('Some output', 'text/plain') ])]); assert.strictEqual(cell.notebook.cellAt(0).outputs.length, 1); - assert.deepStrictEqual(cell.notebook.cellAt(0).outputs[0].outputs[0].value, ['Some output']); - task.end({}); + assert.deepStrictEqual(new TextDecoder().decode(cell.notebook.cellAt(0).outputs[0].items[0].data), 'Some output'); + task.end(undefined); + called = true; } }; - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - await assertKernel(verifyOutputSyncKernel.controller); + const notebook = await openRandomNotebookDocument(); + await vscode.window.showNotebookDocument(notebook); + await assertKernel(verifyOutputSyncKernel, notebook); await vscode.commands.executeCommand('notebook.cell.execute'); - - await saveAllFilesAndCloseAll(undefined); + assert.strictEqual(called, true); verifyOutputSyncKernel.controller.dispose(); }); - test('latestExecutionSummary', async () => { + test('executionSummary', async () => { const resource = await createRandomNotebookFile(); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); const editor = vscode.window.activeNotebookEditor!; const cell = editor.document.cellAt(0); - assert.strictEqual(cell.latestExecutionSummary?.success, undefined); - assert.strictEqual(cell.latestExecutionSummary?.executionOrder, undefined); + assert.strictEqual(cell.executionSummary?.success, undefined); + assert.strictEqual(cell.executionSummary?.executionOrder, undefined); await vscode.commands.executeCommand('notebook.cell.execute'); assert.strictEqual(cell.outputs.length, 1, 'should execute'); - assert.ok(cell.latestExecutionSummary); - assert.strictEqual(cell.latestExecutionSummary!.success, true); - assert.strictEqual(typeof cell.latestExecutionSummary!.executionOrder, 'number'); + assert.ok(cell.executionSummary); + assert.strictEqual(cell.executionSummary!.success, true); + assert.strictEqual(typeof cell.executionSummary!.executionOrder, 'number'); - await saveAllFilesAndCloseAll(undefined); }); - test('initialize latestExecutionSummary', async () => { - const resource = await createRandomNotebookFile(); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - const editor = vscode.window.activeNotebookEditor!; - const cell = editor.document.cellAt(0); + test('initialize executionSummary', async () => { + + const document = await openRandomNotebookDocument(); + const cell = document.cellAt(0); - assert.strictEqual(cell.latestExecutionSummary?.success, undefined); - assert.strictEqual(cell.latestExecutionSummary?.startTime, 10); - assert.strictEqual(cell.latestExecutionSummary?.endTime, 20); + assert.strictEqual(cell.executionSummary?.success, undefined); + assert.strictEqual(cell.executionSummary?.timing?.startTime, 10); + assert.strictEqual(cell.executionSummary?.timing?.endTime, 20); - await saveAllFilesAndCloseAll(undefined); }); @@ -1273,7 +1232,7 @@ suite('Notebook API tests', function () { const emitter = new vscode.EventEmitter(); const onDidCallProvide = emitter.event; suiteSetup(() => { - vscode.notebook.registerNotebookCellStatusBarItemProvider({ viewType: 'notebookCoreTest' }, { + vscode.notebooks.registerNotebookCellStatusBarItemProvider('notebookCoreTest', { async provideCellStatusBarItems(cell: vscode.NotebookCell, _token: vscode.CancellationToken): Promise { emitter.fire(cell); return []; @@ -1288,7 +1247,7 @@ suite('Notebook API tests', function () { await provideCalled; const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCellMetadata(resource, 0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true })); + edit.replaceNotebookCellMetadata(resource, 0, { inputCollapsed: true }); vscode.workspace.applyEdit(edit); await provideCalled; }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts index 701c947426bc..0b3b4e921023 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts @@ -216,10 +216,10 @@ function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void, } try { eventIndex++; - assert.equal('active', expected.events.shift(), `onDidChangeActive (event ${eventIndex})`); + assert.strictEqual('active', expected.events.shift(), `onDidChangeActive (event ${eventIndex})`); const expectedItems = expected.activeItems.shift(); - assert.deepEqual(items.map(item => item.label), expectedItems, `onDidChangeActive event items (event ${eventIndex})`); - assert.deepEqual(quickPick.activeItems.map(item => item.label), expectedItems, `onDidChangeActive active items (event ${eventIndex})`); + assert.deepStrictEqual(items.map(item => item.label), expectedItems, `onDidChangeActive event items (event ${eventIndex})`); + assert.deepStrictEqual(quickPick.activeItems.map(item => item.label), expectedItems, `onDidChangeActive active items (event ${eventIndex})`); } catch (err) { done(err); } @@ -231,10 +231,10 @@ function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void, } try { eventIndex++; - assert.equal('selection', expected.events.shift(), `onDidChangeSelection (event ${eventIndex})`); + assert.strictEqual('selection', expected.events.shift(), `onDidChangeSelection (event ${eventIndex})`); const expectedItems = expected.selectionItems.shift(); - assert.deepEqual(items.map(item => item.label), expectedItems, `onDidChangeSelection event items (event ${eventIndex})`); - assert.deepEqual(quickPick.selectedItems.map(item => item.label), expectedItems, `onDidChangeSelection selected items (event ${eventIndex})`); + assert.deepStrictEqual(items.map(item => item.label), expectedItems, `onDidChangeSelection event items (event ${eventIndex})`); + assert.deepStrictEqual(quickPick.selectedItems.map(item => item.label), expectedItems, `onDidChangeSelection selected items (event ${eventIndex})`); } catch (err) { done(err); } @@ -246,11 +246,11 @@ function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void, } try { eventIndex++; - assert.equal('accept', expected.events.shift(), `onDidAccept (event ${eventIndex})`); + assert.strictEqual('accept', expected.events.shift(), `onDidAccept (event ${eventIndex})`); const expectedActive = expected.acceptedItems.active.shift(); - assert.deepEqual(quickPick.activeItems.map(item => item.label), expectedActive, `onDidAccept active items (event ${eventIndex})`); + assert.deepStrictEqual(quickPick.activeItems.map(item => item.label), expectedActive, `onDidAccept active items (event ${eventIndex})`); const expectedSelection = expected.acceptedItems.selection.shift(); - assert.deepEqual(quickPick.selectedItems.map(item => item.label), expectedSelection, `onDidAccept selected items (event ${eventIndex})`); + assert.deepStrictEqual(quickPick.selectedItems.map(item => item.label), expectedSelection, `onDidAccept selected items (event ${eventIndex})`); if (expected.acceptedItems.dispose.shift()) { quickPick.dispose(); } @@ -265,7 +265,7 @@ function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void, return; } try { - assert.equal('hide', expected.events.shift()); + assert.strictEqual('hide', expected.events.shift()); done(); } catch (err) { done(err); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/rpc.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/rpc.test.ts index 2b336f097189..5fdf01fada48 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/rpc.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/rpc.test.ts @@ -90,8 +90,14 @@ suite('vscode', function () { }); test('no rpc, createNotebookEditorDecorationType(...)', function () { - const item = vscode.notebook.createNotebookEditorDecorationType({ top: {} }); + const item = vscode.notebooks.createNotebookEditorDecorationType({ top: {} }); dispo.push(item); assertNoRpcFromEntry([item, 'NotebookEditorDecorationType']); }); + + test('no rpc, createNotebookController(...)', function () { + const ctrl = vscode.notebooks.createNotebookController('foo', 'bar', ''); + dispo.push(ctrl); + assertNoRpcFromEntry([ctrl, 'NotebookController']); + }); }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index a23d3e55c73f..da2da9499fcc 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -504,6 +504,41 @@ import { assertNoRpc } from '../utils'; const terminal = window.createTerminal({ name: 'foo', pty }); }); + test('should change terminal name', (done) => { + disposables.push(window.onDidOpenTerminal(term => { + try { + equal(terminal, term); + equal(terminal.name, 'foo'); + } catch (e) { + done(e); + return; + } + disposables.push(window.onDidCloseTerminal(t => { + try { + equal(terminal, t); + equal(terminal.name, 'bar'); + } catch (e) { + done(e); + return; + } + done(); + })); + })); + const changeNameEmitter = new EventEmitter(); + const closeEmitter = new EventEmitter(); + const pty: Pseudoterminal = { + onDidWrite: new EventEmitter().event, + onDidChangeName: changeNameEmitter.event, + onDidClose: closeEmitter.event, + open: () => { + changeNameEmitter.fire('bar'); + closeEmitter.fire(undefined); + }, + close: () => { } + }; + const terminal = window.createTerminal({ name: 'foo', pty }); + }); + test('exitStatus.code should be set to the exit code (undefined)', (done) => { disposables.push(window.onDidOpenTerminal(term => { try { diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts index 629fbaace14b..f706b64677f0 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts @@ -17,7 +17,7 @@ function workspaceFile(...segments: string[]) { const testDocument = workspaceFile('bower.json'); -suite.skip('vscode API - webview', () => { +suite('vscode API - webview', () => { const disposables: vscode.Disposable[] = []; function _register(disposable: T) { @@ -400,7 +400,7 @@ suite.skip('vscode API - webview', () => { }); } - test('webviews should transfer ArrayBuffers to and from webviews', async () => { + test.skip('webviews should transfer ArrayBuffers to and from webviews', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true })); const ready = getMessage(webview); webview.webview.html = createHtmlDocumentWithBody(/*html*/` @@ -451,7 +451,7 @@ suite.skip('vscode API - webview', () => { } }); - test('webviews should transfer Typed arrays to and from webviews', async () => { + test.skip('webviews should transfer Typed arrays to and from webviews', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true })); const ready = getMessage(webview); webview.webview.html = createHtmlDocumentWithBody(/*html*/` diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index c30ab5138373..0a403549ce28 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, QuickPickItem, TextEditor } from 'vscode'; +import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, QuickPickItem, TextEditor, StatusBarAlignment } from 'vscode'; import { join } from 'path'; import { closeAllEditors, pathEquals, createRandomFile, assertNoRpc } from '../utils'; @@ -34,20 +34,20 @@ suite('vscode API - window', () => { }); // test('editor, UN-active text editor', () => { - // assert.equal(window.visibleTextEditors.length, 0); + // assert.strictEqual(window.visibleTextEditors.length, 0); // assert.ok(window.activeTextEditor === undefined); // }); test('editor, assign and check view columns', async () => { const doc = await workspace.openTextDocument(join(workspace.rootPath || '', './far.js')); let p1 = window.showTextDocument(doc, ViewColumn.One).then(editor => { - assert.equal(editor.viewColumn, ViewColumn.One); + assert.strictEqual(editor.viewColumn, ViewColumn.One); }); let p2 = window.showTextDocument(doc, ViewColumn.Two).then(editor_1 => { - assert.equal(editor_1.viewColumn, ViewColumn.Two); + assert.strictEqual(editor_1.viewColumn, ViewColumn.Two); }); let p3 = window.showTextDocument(doc, ViewColumn.Three).then(editor_2 => { - assert.equal(editor_2.viewColumn, ViewColumn.Three); + assert.strictEqual(editor_2.viewColumn, ViewColumn.Three); }); return Promise.all([p1, p2, p3]); }); @@ -60,13 +60,13 @@ suite('vscode API - window', () => { const doc = await workspace.openTextDocument(join(workspace.rootPath || '', './far.js')); await window.showTextDocument(doc, ViewColumn.One); - assert.equal(eventCounter, 1); + assert.strictEqual(eventCounter, 1); await window.showTextDocument(doc, ViewColumn.Two); - assert.equal(eventCounter, 2); + assert.strictEqual(eventCounter, 2); await window.showTextDocument(doc, ViewColumn.Three); - assert.equal(eventCounter, 3); + assert.strictEqual(eventCounter, 3); reg.dispose(); }); @@ -138,10 +138,10 @@ suite('vscode API - window', () => { return commands.executeCommand('workbench.action.moveActiveEditorGroupLeft'); }).then(() => { - assert.equal(actualEvents.length, 2); + assert.strictEqual(actualEvents.length, 2); for (const event of actualEvents) { - assert.equal(event.viewColumn, event.textEditor.viewColumn); + assert.strictEqual(event.viewColumn, event.textEditor.viewColumn); } registration1.dispose(); @@ -202,7 +202,7 @@ suite('vscode API - window', () => { assert.ok(window.activeTextEditor); assert.ok(window.activeTextEditor!.document === docB); - assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + assert.strictEqual(window.activeTextEditor!.viewColumn, ViewColumn.Two); const editor = await window.showTextDocument(docC); assert.ok( @@ -210,7 +210,7 @@ suite('vscode API - window', () => { `wanted fileName:${editor.document.fileName}/viewColumn:${editor.viewColumn} but got fileName:${window.activeTextEditor!.document.fileName}/viewColumn:${window.activeTextEditor!.viewColumn}. a:${docA.fileName}, b:${docB.fileName}, c:${docC.fileName}` ); assert.ok(window.activeTextEditor!.document === docC); - assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + assert.strictEqual(window.activeTextEditor!.viewColumn, ViewColumn.Two); }); test('showTextDocument ViewColumn.BESIDE', async () => { @@ -225,12 +225,12 @@ suite('vscode API - window', () => { assert.ok(window.activeTextEditor); assert.ok(window.activeTextEditor!.document === docB); - assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + assert.strictEqual(window.activeTextEditor!.viewColumn, ViewColumn.Two); await window.showTextDocument(docC, ViewColumn.Beside); assert.ok(window.activeTextEditor!.document === docC); - assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Three); + assert.strictEqual(window.activeTextEditor!.viewColumn, ViewColumn.Three); }); test('showTextDocument ViewColumn is always defined (even when opening > ViewColumn.Nine)', async () => { @@ -260,7 +260,7 @@ suite('vscode API - window', () => { assert.ok(window.activeTextEditor); assert.ok(window.activeTextEditor!.document === doc10); - assert.equal(window.activeTextEditor!.viewColumn, 10); + assert.strictEqual(window.activeTextEditor!.viewColumn, 10); }); test('issue #27408 - showTextDocument & vscode.diff always default to ViewColumn.One', async () => { @@ -275,12 +275,12 @@ suite('vscode API - window', () => { assert.ok(window.activeTextEditor); assert.ok(window.activeTextEditor!.document === docB); - assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + assert.strictEqual(window.activeTextEditor!.viewColumn, ViewColumn.Two); await window.showTextDocument(docC, ViewColumn.Active); assert.ok(window.activeTextEditor!.document === docC); - assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + assert.strictEqual(window.activeTextEditor!.viewColumn, ViewColumn.Two); }); test('issue #5362 - Incorrect TextEditor passed by onDidChangeTextEditorSelection', (done) => { @@ -359,7 +359,7 @@ suite('vscode API - window', () => { const p = window.showInputBox(undefined, source.token); source.cancel(); const value = await p; - assert.equal(value, undefined); + assert.strictEqual(value, undefined); }); test('showInputBox - cancel early', async function () { @@ -367,21 +367,21 @@ suite('vscode API - window', () => { source.cancel(); const p = window.showInputBox(undefined, source.token); const value = await p; - assert.equal(value, undefined); + assert.strictEqual(value, undefined); }); test('showInputBox - \'\' on Enter', function () { const p = window.showInputBox(); return Promise.all([ commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'), - p.then(value => assert.equal(value, '')) + p.then(value => assert.strictEqual(value, '')) ]); }); test('showInputBox - default value on Enter', function () { const p = window.showInputBox({ value: 'farboo' }); return Promise.all([ - p.then(value => assert.equal(value, 'farboo')), + p.then(value => assert.strictEqual(value, 'farboo')), commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'), ]); }); @@ -390,7 +390,7 @@ suite('vscode API - window', () => { const p = window.showInputBox(); return Promise.all([ commands.executeCommand('workbench.action.closeQuickOpen'), - p.then(value => assert.equal(value, undefined)) + p.then(value => assert.strictEqual(value, undefined)) ]); }); @@ -398,17 +398,17 @@ suite('vscode API - window', () => { const p = window.showInputBox({ value: 'farboo' }); return Promise.all([ commands.executeCommand('workbench.action.closeQuickOpen'), - p.then(value => assert.equal(value, undefined)) + p.then(value => assert.strictEqual(value, undefined)) ]); }); test('showInputBox - value not empty on second try', async function () { const one = window.showInputBox({ value: 'notempty' }); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - assert.equal(await one, 'notempty'); + assert.strictEqual(await one, 'notempty'); const two = window.showInputBox({ value: 'notempty' }); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - assert.equal(await two, 'notempty'); + assert.strictEqual(await two, 'notempty'); }); test('showQuickPick, accept first', async function () { @@ -417,9 +417,9 @@ suite('vscode API - window', () => { const pick = window.showQuickPick(['eins', 'zwei', 'drei'], { onDidSelectItem: tracker.onDidSelectItem }); - assert.equal(await first, 'eins'); + assert.strictEqual(await first, 'eins'); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - assert.equal(await pick, 'eins'); + assert.strictEqual(await pick, 'eins'); return tracker.done(); }); @@ -429,12 +429,12 @@ suite('vscode API - window', () => { const pick = window.showQuickPick(['eins', 'zwei', 'drei'], { onDidSelectItem: tracker.onDidSelectItem }); - assert.equal(await first, 'eins'); + assert.strictEqual(await first, 'eins'); const second = tracker.nextItem(); await commands.executeCommand('workbench.action.quickOpenSelectNext'); - assert.equal(await second, 'zwei'); + assert.strictEqual(await second, 'zwei'); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - assert.equal(await pick, 'zwei'); + assert.strictEqual(await pick, 'zwei'); return tracker.done(); }); @@ -457,14 +457,14 @@ suite('vscode API - window', () => { // console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.quickOpenSelectNext'); // console.log(`${label}: ${++i}`); - assert.equal(await first, 'eins'); + assert.strictEqual(await first, 'eins'); // console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.quickPickManyToggle'); // console.log(`${label}: ${++i}`); const second = new Promise(resolve => resolves.push(resolve)); await commands.executeCommand('workbench.action.quickOpenSelectNext'); // console.log(`${label}: ${++i}`); - assert.equal(await second, 'zwei'); + assert.strictEqual(await second, 'zwei'); // console.log(`${label}: ${++i}`); await commands.executeCommand('workbench.action.quickPickManyToggle'); // console.log(`${label}: ${++i}`); @@ -503,7 +503,7 @@ suite('vscode API - window', () => { const p = window.showQuickPick(['eins', 'zwei', 'drei'], undefined, source.token); source.cancel(); return p.then(value => { - assert.equal(value, undefined); + assert.strictEqual(value, undefined); }); }); @@ -512,7 +512,7 @@ suite('vscode API - window', () => { source.cancel(); const p = window.showQuickPick(['eins', 'zwei', 'drei'], undefined, source.token); return p.then(value => { - assert.equal(value, undefined); + assert.strictEqual(value, undefined); }); }); @@ -522,7 +522,7 @@ suite('vscode API - window', () => { const result = window.showQuickPick(['eins', 'zwei', 'drei'], { ignoreFocusOut: true }).then(result => { source.cancel(); - assert.equal(result, undefined); + assert.strictEqual(result, undefined); }); window.showQuickPick(['eins', 'zwei', 'drei'], undefined, source.token); @@ -533,7 +533,7 @@ suite('vscode API - window', () => { test('showQuickPick, canceled by input', function () { const result = window.showQuickPick(['eins', 'zwei', 'drei'], { ignoreFocusOut: true }).then(result => { - assert.equal(result, undefined); + assert.strictEqual(result, undefined); }); const source = new CancellationTokenSource(); @@ -553,7 +553,7 @@ suite('vscode API - window', () => { const result = window.showQuickPick(data, undefined, source.token); source.cancel(); const value_1 = await result; - assert.equal(value_1, undefined); + assert.strictEqual(value_1, undefined); }); test('showQuickPick, never resolve promise and cancel - #22453', function () { @@ -561,7 +561,7 @@ suite('vscode API - window', () => { const result = window.showQuickPick(new Promise(_resolve => { })); const a = result.then(value => { - assert.equal(value, undefined); + assert.strictEqual(value, undefined); }); const b = commands.executeCommand('workbench.action.closeQuickOpen'); return Promise.all([a, b]); @@ -598,7 +598,7 @@ suite('vscode API - window', () => { await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); await commands.executeCommand('workbench.action.closeQuickOpen'); - assert.equal(await result, undefined); + assert.strictEqual(await result, undefined); }); function createQuickPickTracker() { @@ -627,7 +627,7 @@ suite('vscode API - window', () => { let subscription = window.onDidChangeTextEditorSelection(e => { assert.ok(e.textEditor === editor); - assert.equal(e.kind, TextEditorSelectionChangeKind.Command); + assert.strictEqual(e.kind, TextEditorSelectionChangeKind.Command); subscription.dispose(); resolve(); @@ -638,4 +638,22 @@ suite('vscode API - window', () => { }); }); + + test('createStatusBar', async function () { + const statusBarEntryWithoutId = window.createStatusBarItem(StatusBarAlignment.Left, 100); + assert.strictEqual(statusBarEntryWithoutId.id, 'vscode.vscode-api-tests'); + assert.strictEqual(statusBarEntryWithoutId.alignment, StatusBarAlignment.Left); + assert.strictEqual(statusBarEntryWithoutId.priority, 100); + assert.strictEqual(statusBarEntryWithoutId.name, undefined); + statusBarEntryWithoutId.name = 'Test Name'; + assert.strictEqual(statusBarEntryWithoutId.name, 'Test Name'); + + const statusBarEntryWithId = window.createStatusBarItem('testId', StatusBarAlignment.Right, 200); + assert.strictEqual(statusBarEntryWithId.alignment, StatusBarAlignment.Right); + assert.strictEqual(statusBarEntryWithId.priority, 200); + assert.strictEqual(statusBarEntryWithId.id, 'testId'); + assert.strictEqual(statusBarEntryWithId.name, undefined); + statusBarEntryWithId.name = 'Test Name'; + assert.strictEqual(statusBarEntryWithId.name, 'Test Name'); + }); }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts index 10ef9563c874..cdb2217512bb 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts @@ -35,12 +35,12 @@ suite('vscode API - workspace events', () => { assert.ok(success); assert.ok(onWillCreate); - assert.equal(onWillCreate?.files.length, 1); - assert.equal(onWillCreate?.files[0].toString(), newUri.toString()); + assert.strictEqual(onWillCreate?.files.length, 1); + assert.strictEqual(onWillCreate?.files[0].toString(), newUri.toString()); assert.ok(onDidCreate); - assert.equal(onDidCreate?.files.length, 1); - assert.equal(onDidCreate?.files[0].toString(), newUri.toString()); + assert.strictEqual(onDidCreate?.files.length, 1); + assert.strictEqual(onDidCreate?.files[0].toString(), newUri.toString()); })); test('onWillCreate/onDidCreate, make changes, edit another file', withLogDisabled(async function () { @@ -62,7 +62,7 @@ suite('vscode API - workspace events', () => { const success = await vscode.workspace.applyEdit(edit); assert.ok(success); - assert.equal(baseDoc.getText(), 'HALLO_NEW'); + assert.strictEqual(baseDoc.getText(), 'HALLO_NEW'); })); test('onWillCreate/onDidCreate, make changes, edit new file fails', withLogDisabled(async function () { @@ -83,8 +83,8 @@ suite('vscode API - workspace events', () => { const success = await vscode.workspace.applyEdit(edit); assert.ok(success); - assert.equal((await vscode.workspace.fs.readFile(newUri)).toString(), ''); - assert.equal((await vscode.workspace.openTextDocument(newUri)).getText(), ''); + assert.strictEqual((await vscode.workspace.fs.readFile(newUri)).toString(), ''); + assert.strictEqual((await vscode.workspace.openTextDocument(newUri)).getText(), ''); })); test('onWillDelete/onDidDelete', withLogDisabled(async function () { @@ -104,12 +104,12 @@ suite('vscode API - workspace events', () => { assert.ok(success); assert.ok(onWilldelete); - assert.equal(onWilldelete?.files.length, 1); - assert.equal(onWilldelete?.files[0].toString(), base.toString()); + assert.strictEqual(onWilldelete?.files.length, 1); + assert.strictEqual(onWilldelete?.files[0].toString(), base.toString()); assert.ok(onDiddelete); - assert.equal(onDiddelete?.files.length, 1); - assert.equal(onDiddelete?.files[0].toString(), base.toString()); + assert.strictEqual(onDiddelete?.files.length, 1); + assert.strictEqual(onDiddelete?.files[0].toString(), base.toString()); })); test('onWillDelete/onDidDelete, make changes', withLogDisabled(async function () { @@ -190,14 +190,14 @@ suite('vscode API - workspace events', () => { assert.ok(success); assert.ok(onWillRename); - assert.equal(onWillRename?.files.length, 1); - assert.equal(onWillRename?.files[0].oldUri.toString(), oldUri.toString()); - assert.equal(onWillRename?.files[0].newUri.toString(), newUri.toString()); + assert.strictEqual(onWillRename?.files.length, 1); + assert.strictEqual(onWillRename?.files[0].oldUri.toString(), oldUri.toString()); + assert.strictEqual(onWillRename?.files[0].newUri.toString(), newUri.toString()); assert.ok(onDidRename); - assert.equal(onDidRename?.files.length, 1); - assert.equal(onDidRename?.files[0].oldUri.toString(), oldUri.toString()); - assert.equal(onDidRename?.files[0].newUri.toString(), newUri.toString()); + assert.strictEqual(onDidRename?.files.length, 1); + assert.strictEqual(onDidRename?.files[0].oldUri.toString(), oldUri.toString()); + assert.strictEqual(onDidRename?.files[0].newUri.toString(), newUri.toString()); })); test('onWillRename - make changes (saved file)', withLogDisabled(function () { @@ -244,15 +244,15 @@ suite('vscode API - workspace events', () => { assert.ok(success); assert.ok(onWillRename); - assert.equal(onWillRename?.files.length, 1); - assert.equal(onWillRename?.files[0].oldUri.toString(), oldUri.toString()); - assert.equal(onWillRename?.files[0].newUri.toString(), newUri.toString()); + assert.strictEqual(onWillRename?.files.length, 1); + assert.strictEqual(onWillRename?.files[0].oldUri.toString(), oldUri.toString()); + assert.strictEqual(onWillRename?.files[0].newUri.toString(), newUri.toString()); const newDocument = await vscode.workspace.openTextDocument(newUri); const anotherDocument = await vscode.workspace.openTextDocument(anotherFile); - assert.equal(newDocument.getText(), withDirtyFile ? 'FOOBARBAR' : 'FOOBAR'); - assert.equal(anotherDocument.getText(), 'FARBOO'); + assert.strictEqual(newDocument.getText(), withDirtyFile ? 'FOOBARBAR' : 'FOOBAR'); + assert.strictEqual(anotherDocument.getText(), 'FARBOO'); assert.ok(newDocument.isDirty); assert.ok(anotherDocument.isDirty); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts index 193ab54be61d..a59c0f85fc13 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts @@ -20,11 +20,11 @@ suite('vscode API - workspace-fs', () => { test('fs.stat', async function () { const stat = await vscode.workspace.fs.stat(root); - assert.equal(stat.type, vscode.FileType.Directory); + assert.strictEqual(stat.type, vscode.FileType.Directory); - assert.equal(typeof stat.size, 'number'); - assert.equal(typeof stat.mtime, 'number'); - assert.equal(typeof stat.ctime, 'number'); + assert.strictEqual(typeof stat.size, 'number'); + assert.strictEqual(typeof stat.mtime, 'number'); + assert.strictEqual(typeof stat.ctime, 'number'); assert.ok(stat.mtime > 0); assert.ok(stat.ctime > 0); @@ -35,8 +35,8 @@ suite('vscode API - workspace-fs', () => { // find far.js const tuple = entries.find(tuple => tuple[0] === 'far.js')!; assert.ok(tuple); - assert.equal(tuple[0], 'far.js'); - assert.equal(tuple[1], vscode.FileType.File); + assert.strictEqual(tuple[0], 'far.js'); + assert.strictEqual(tuple[1], vscode.FileType.File); }); test('fs.stat - bad scheme', async function () { @@ -63,7 +63,7 @@ suite('vscode API - workspace-fs', () => { await vscode.workspace.fs.writeFile(uri, Buffer.from('HELLO')); const stat = await vscode.workspace.fs.stat(uri); - assert.equal(stat.type, vscode.FileType.File); + assert.strictEqual(stat.type, vscode.FileType.File); await vscode.workspace.fs.delete(uri); @@ -129,7 +129,7 @@ suite('vscode API - workspace-fs', () => { assert.ok(false); } catch (e) { assert.ok(e instanceof vscode.FileSystemError); - assert.equal(e.name, vscode.FileSystemError.FileNotFound().name); + assert.strictEqual(e.name, vscode.FileSystemError.FileNotFound().name); } }); @@ -140,7 +140,7 @@ suite('vscode API - workspace-fs', () => { assert.ok(false); } catch (e) { assert.ok(e instanceof vscode.FileSystemError); - assert.equal(e.name, vscode.FileSystemError.Unavailable().name); + assert.strictEqual(e.name, vscode.FileSystemError.Unavailable().name); } }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index e60998c24bee..b17ebdbd5c6b 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -19,17 +19,17 @@ suite('vscode API - workspace', () => { test('MarkdownString', function () { let md = new vscode.MarkdownString(); - assert.equal(md.value, ''); - assert.equal(md.isTrusted, undefined); + assert.strictEqual(md.value, ''); + assert.strictEqual(md.isTrusted, undefined); md = new vscode.MarkdownString('**bold**'); - assert.equal(md.value, '**bold**'); + assert.strictEqual(md.value, '**bold**'); md.appendText('**bold?**'); - assert.equal(md.value, '**bold**\\*\\*bold?\\*\\*'); + assert.strictEqual(md.value, '**bold**\\*\\*bold?\\*\\*'); md.appendMarkdown('**bold**'); - assert.equal(md.value, '**bold**\\*\\*bold?\\*\\***bold**'); + assert.strictEqual(md.value, '**bold**\\*\\*bold?\\*\\***bold**'); }); @@ -49,7 +49,7 @@ suite('vscode API - workspace', () => { test('workspaceFolders', () => { if (vscode.workspace.workspaceFolders) { - assert.equal(vscode.workspace.workspaceFolders.length, 1); + assert.strictEqual(vscode.workspace.workspaceFolders.length, 1); assert.ok(pathEquals(vscode.workspace.workspaceFolders[0].uri.fsPath, join(__dirname, '../../testWorkspace'))); } }); @@ -68,14 +68,14 @@ suite('vscode API - workspace', () => { // not yet there const existing1 = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); - assert.equal(existing1, undefined); + assert.strictEqual(existing1, undefined); // open and assert its there const doc = await vscode.workspace.openTextDocument(uri); assert.ok(doc); - assert.equal(doc.uri.toString(), uri.toString()); + assert.strictEqual(doc.uri.toString(), uri.toString()); const existing2 = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); - assert.equal(existing2 === doc, true); + assert.strictEqual(existing2 === doc, true); }); test('openTextDocument, illegal path', () => { @@ -88,7 +88,7 @@ suite('vscode API - workspace', () => { test('openTextDocument, untitled is dirty', async function () { return vscode.workspace.openTextDocument(vscode.workspace.workspaceFolders![0].uri.with({ scheme: 'untitled', path: posix.join(vscode.workspace.workspaceFolders![0].uri.path, 'newfile.txt') })).then(doc => { - assert.equal(doc.uri.scheme, 'untitled'); + assert.strictEqual(doc.uri.scheme, 'untitled'); assert.ok(doc.isDirty); }); }); @@ -96,31 +96,31 @@ suite('vscode API - workspace', () => { test('openTextDocument, untitled with host', function () { const uri = vscode.Uri.parse('untitled://localhost/c%24/Users/jrieken/code/samples/foobar.txt'); return vscode.workspace.openTextDocument(uri).then(doc => { - assert.equal(doc.uri.scheme, 'untitled'); + assert.strictEqual(doc.uri.scheme, 'untitled'); }); }); test('openTextDocument, untitled without path', function () { return vscode.workspace.openTextDocument().then(doc => { - assert.equal(doc.uri.scheme, 'untitled'); + assert.strictEqual(doc.uri.scheme, 'untitled'); assert.ok(doc.isDirty); }); }); test('openTextDocument, untitled without path but language ID', function () { return vscode.workspace.openTextDocument({ language: 'xml' }).then(doc => { - assert.equal(doc.uri.scheme, 'untitled'); - assert.equal(doc.languageId, 'xml'); + assert.strictEqual(doc.uri.scheme, 'untitled'); + assert.strictEqual(doc.languageId, 'xml'); assert.ok(doc.isDirty); }); }); test('openTextDocument, untitled without path but language ID and content', function () { return vscode.workspace.openTextDocument({ language: 'html', content: '

    Hello world!

    ' }).then(doc => { - assert.equal(doc.uri.scheme, 'untitled'); - assert.equal(doc.languageId, 'html'); + assert.strictEqual(doc.uri.scheme, 'untitled'); + assert.strictEqual(doc.languageId, 'html'); assert.ok(doc.isDirty); - assert.equal(doc.getText(), '

    Hello world!

    '); + assert.strictEqual(doc.getText(), '

    Hello world!

    '); }); }); @@ -128,7 +128,7 @@ suite('vscode API - workspace', () => { const path = join(vscode.workspace.rootPath || '', './newfile.txt'); return vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:' + path)).then(doc => { - assert.equal(doc.uri.scheme, 'untitled'); + assert.strictEqual(doc.uri.scheme, 'untitled'); assert.ok(doc.isDirty); let closed: vscode.TextDocument; @@ -137,7 +137,7 @@ suite('vscode API - workspace', () => { return vscode.window.showTextDocument(doc).then(() => { return doc.save().then((didSave: boolean) => { - assert.equal(didSave, true, `FAILED to save${doc.uri.toString()}`); + assert.strictEqual(didSave, true, `FAILED to save${doc.uri.toString()}`); assert.ok(closed === doc); assert.ok(!doc.isDirty); @@ -161,16 +161,16 @@ suite('vscode API - workspace', () => { return Promise.all([ vscode.workspace.openTextDocument(vscode.Uri.parse('sc://auth')).then(doc => { - assert.equal(doc.uri.authority, 'auth'); - assert.equal(doc.uri.path, ''); + assert.strictEqual(doc.uri.authority, 'auth'); + assert.strictEqual(doc.uri.path, ''); }), vscode.workspace.openTextDocument(vscode.Uri.parse('sc:///path')).then(doc => { - assert.equal(doc.uri.authority, ''); - assert.equal(doc.uri.path, '/path'); + assert.strictEqual(doc.uri.authority, ''); + assert.strictEqual(doc.uri.path, '/path'); }), vscode.workspace.openTextDocument(vscode.Uri.parse('sc://auth/path')).then(doc => { - assert.equal(doc.uri.authority, 'auth'); - assert.equal(doc.uri.path, '/path'); + assert.strictEqual(doc.uri.authority, 'auth'); + assert.strictEqual(doc.uri.path, '/path'); }) ]).then(() => { registration.dispose(); @@ -192,21 +192,21 @@ suite('vscode API - workspace', () => { // lower case (actual case) comes first let docOne = await vscode.workspace.openTextDocument(uriOne); - assert.equal(docOne.uri.toString(), uriOne.toString()); + assert.strictEqual(docOne.uri.toString(), uriOne.toString()); let docONE = await vscode.workspace.openTextDocument(uriONE); - assert.equal(docONE === docOne, true); - assert.equal(docONE.uri.toString(), uriOne.toString()); - assert.equal(docONE.uri.toString() !== uriONE.toString(), true); // yep + assert.strictEqual(docONE === docOne, true); + assert.strictEqual(docONE.uri.toString(), uriOne.toString()); + assert.strictEqual(docONE.uri.toString() !== uriONE.toString(), true); // yep // upper case (NOT the actual case) comes first let docTWO = await vscode.workspace.openTextDocument(uriTWO); - assert.equal(docTWO.uri.toString(), uriTWO.toString()); + assert.strictEqual(docTWO.uri.toString(), uriTWO.toString()); let docTwo = await vscode.workspace.openTextDocument(uriTwo); - assert.equal(docTWO === docTwo, true); - assert.equal(docTwo.uri.toString(), uriTWO.toString()); - assert.equal(docTwo.uri.toString() !== uriTwo.toString(), true); // yep + assert.strictEqual(docTWO === docTwo, true); + assert.strictEqual(docTwo.uri.toString(), uriTWO.toString()); + assert.strictEqual(docTwo.uri.toString() !== uriTwo.toString(), true); // yep reg.dispose(); }); @@ -214,17 +214,17 @@ suite('vscode API - workspace', () => { test('eol, read', () => { const a = createRandomFile('foo\nbar\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { - assert.equal(doc.eol, vscode.EndOfLine.LF); + assert.strictEqual(doc.eol, vscode.EndOfLine.LF); }); }); const b = createRandomFile('foo\nbar\nbar\r\nbaz').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { - assert.equal(doc.eol, vscode.EndOfLine.LF); + assert.strictEqual(doc.eol, vscode.EndOfLine.LF); }); }); const c = createRandomFile('foo\r\nbar\r\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { - assert.equal(doc.eol, vscode.EndOfLine.CRLF); + assert.strictEqual(doc.eol, vscode.EndOfLine.CRLF); }); }); return Promise.all([a, b, c]); @@ -233,14 +233,14 @@ suite('vscode API - workspace', () => { test('eol, change via editor', () => { return createRandomFile('foo\nbar\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { - assert.equal(doc.eol, vscode.EndOfLine.LF); + assert.strictEqual(doc.eol, vscode.EndOfLine.LF); return vscode.window.showTextDocument(doc).then(editor => { return editor.edit(builder => builder.setEndOfLine(vscode.EndOfLine.CRLF)); }).then(value => { assert.ok(value); assert.ok(doc.isDirty); - assert.equal(doc.eol, vscode.EndOfLine.CRLF); + assert.strictEqual(doc.eol, vscode.EndOfLine.CRLF); }); }); }); @@ -249,14 +249,14 @@ suite('vscode API - workspace', () => { test('eol, change via applyEdit', () => { return createRandomFile('foo\nbar\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { - assert.equal(doc.eol, vscode.EndOfLine.LF); + assert.strictEqual(doc.eol, vscode.EndOfLine.LF); const edit = new vscode.WorkspaceEdit(); edit.set(file, [vscode.TextEdit.setEndOfLine(vscode.EndOfLine.CRLF)]); return vscode.workspace.applyEdit(edit).then(value => { assert.ok(value); assert.ok(doc.isDirty); - assert.equal(doc.eol, vscode.EndOfLine.CRLF); + assert.strictEqual(doc.eol, vscode.EndOfLine.CRLF); }); }); }); @@ -271,7 +271,7 @@ suite('vscode API - workspace', () => { const file = await createRandomFile('foo\r\nbar\r\nbar'); const doc = await vscode.workspace.openTextDocument(file); - assert.equal(doc.eol, vscode.EndOfLine.CRLF); + assert.strictEqual(doc.eol, vscode.EndOfLine.CRLF); const edit = new vscode.WorkspaceEdit(); edit.set(file, [vscode.TextEdit.insert(new vscode.Position(0, 0), '-changes-')]); @@ -282,7 +282,7 @@ suite('vscode API - workspace', () => { assert.ok(successSave); assert.ok(called); assert.ok(!doc.isDirty); - assert.equal(doc.eol, vscode.EndOfLine.LF); + assert.strictEqual(doc.eol, vscode.EndOfLine.LF); sub.dispose(); }); @@ -358,10 +358,10 @@ suite('vscode API - workspace', () => { return createRandomFile('foo\nbar\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { return vscode.window.showTextDocument(doc, { selection: new vscode.Range(new vscode.Position(1, 1), new vscode.Position(1, 2)) }).then(editor => { - assert.equal(editor.selection.start.line, 1); - assert.equal(editor.selection.start.character, 1); - assert.equal(editor.selection.end.line, 1); - assert.equal(editor.selection.end.character, 2); + assert.strictEqual(editor.selection.start.line, 1); + assert.strictEqual(editor.selection.start.character, 1); + assert.strictEqual(editor.selection.end.line, 1); + assert.strictEqual(editor.selection.end.character, 2); }); }); }); @@ -377,9 +377,9 @@ suite('vscode API - workspace', () => { const uri = vscode.Uri.parse('foo://testing/virtual.js'); return vscode.workspace.openTextDocument(uri).then(doc => { - assert.equal(doc.getText(), uri.toString()); - assert.equal(doc.isDirty, false); - assert.equal(doc.uri.toString(), uri.toString()); + assert.strictEqual(doc.getText(), uri.toString()); + assert.strictEqual(doc.isDirty, false); + assert.strictEqual(doc.uri.toString(), uri.toString()); registration.dispose(); }); }); @@ -424,8 +424,8 @@ suite('vscode API - workspace', () => { }); return Promise.all([ - vscode.workspace.openTextDocument(vscode.Uri.parse('foo://foo/bla')).then(doc => { assert.equal(doc.getText(), '1'); }), - vscode.workspace.openTextDocument(vscode.Uri.parse('foo://bar/bla')).then(doc => { assert.equal(doc.getText(), '2'); }) + vscode.workspace.openTextDocument(vscode.Uri.parse('foo://foo/bla')).then(doc => { assert.strictEqual(doc.getText(), '1'); }), + vscode.workspace.openTextDocument(vscode.Uri.parse('foo://bar/bla')).then(doc => { assert.strictEqual(doc.getText(), '2'); }) ]).then(() => { registration1.dispose(); registration2.dispose(); @@ -447,7 +447,7 @@ suite('vscode API - workspace', () => { }); return vscode.workspace.openTextDocument(vscode.Uri.parse('foo://foo/bla')).then(doc => { - assert.equal(doc.getText(), '1'); + assert.strictEqual(doc.getText(), '1'); registration1.dispose(); registration2.dispose(); }); @@ -480,7 +480,7 @@ suite('vscode API - workspace', () => { return vscode.window.showTextDocument(doc).then(editor => { assert.ok(editor.document === doc); - assert.equal(editor.document.getText(), 'I am virtual'); + assert.strictEqual(editor.document.getText(), 'I am virtual'); registration.dispose(); }); }); @@ -502,7 +502,7 @@ suite('vscode API - workspace', () => { let [first, second] = docs; assert.ok(first === second); assert.ok(vscode.workspace.textDocuments.some(doc => doc.uri.toString() === uri.toString())); - assert.equal(callCount, 1); + assert.strictEqual(callCount, 1); registration.dispose(); }); }); @@ -518,8 +518,8 @@ suite('vscode API - workspace', () => { const uri = vscode.Uri.parse('foo:doc/empty'); return vscode.workspace.openTextDocument(uri).then(doc => { - assert.equal(doc.getText(), ''); - assert.equal(doc.uri.toString(), uri.toString()); + assert.strictEqual(doc.getText(), ''); + assert.strictEqual(doc.uri.toString(), uri.toString()); registration.dispose(); }); }); @@ -539,14 +539,14 @@ suite('vscode API - workspace', () => { const uri = vscode.Uri.parse('foo://testing/path3'); const doc = await vscode.workspace.openTextDocument(uri); - assert.equal(callCount, 1); - assert.equal(doc.getText(), 'call0'); + assert.strictEqual(callCount, 1); + assert.strictEqual(doc.getText(), 'call0'); return new Promise(resolve => { let subscription = vscode.workspace.onDidChangeTextDocument(event => { assert.ok(event.document === doc); - assert.equal(event.document.getText(), 'call1'); + assert.strictEqual(event.document.getText(), 'call1'); subscription.dispose(); registration.dispose(); resolve(); @@ -558,36 +558,36 @@ suite('vscode API - workspace', () => { test('findFiles', () => { return vscode.workspace.findFiles('**/image.png').then((res) => { - assert.equal(res.length, 2); - assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); + assert.strictEqual(res.length, 2); + assert.strictEqual(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); }); }); test('findFiles - null exclude', async () => { await vscode.workspace.findFiles('**/file.txt').then((res) => { // search.exclude folder is still searched, files.exclude folder is not - assert.equal(res.length, 1); - assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'file.txt'); + assert.strictEqual(res.length, 1); + assert.strictEqual(basename(vscode.workspace.asRelativePath(res[0])), 'file.txt'); }); await vscode.workspace.findFiles('**/file.txt', null).then((res) => { // search.exclude and files.exclude folders are both searched - assert.equal(res.length, 2); - assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'file.txt'); + assert.strictEqual(res.length, 2); + assert.strictEqual(basename(vscode.workspace.asRelativePath(res[0])), 'file.txt'); }); }); test('findFiles - exclude', () => { return vscode.workspace.findFiles('**/image.png').then((res) => { - assert.equal(res.length, 2); - assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); + assert.strictEqual(res.length, 2); + assert.strictEqual(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); }); }); test('findFiles, exclude', () => { return vscode.workspace.findFiles('**/image.png', '**/sub/**').then((res) => { - assert.equal(res.length, 1); - assert.equal(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); + assert.strictEqual(res.length, 1); + assert.strictEqual(basename(vscode.workspace.asRelativePath(res[0])), 'image.png'); }); }); @@ -598,7 +598,7 @@ suite('vscode API - workspace', () => { source.cancel(); return vscode.workspace.findFiles('*.js', null, 100, token).then((res) => { - assert.deepEqual(res, []); + assert.deepStrictEqual(res, []); }); }); @@ -616,10 +616,10 @@ suite('vscode API - workspace', () => { results.push(result); }); - assert.equal(results.length, 1); + assert.strictEqual(results.length, 1); const match = results[0]; assert(match.preview.text.indexOf('foo') >= 0); - assert.equal(basename(vscode.workspace.asRelativePath(match.uri)), '10linefile.ts'); + assert.strictEqual(basename(vscode.workspace.asRelativePath(match.uri)), '10linefile.ts'); }); test('findTextInFiles, cancellation', async () => { @@ -639,8 +639,8 @@ suite('vscode API - workspace', () => { edit.insert(doc.uri, new vscode.Position(0, 0), new Array(1000).join('Hello World')); let success = await vscode.workspace.applyEdit(edit); - assert.equal(success, true); - assert.equal(doc.isDirty, true); + assert.strictEqual(success, true); + assert.strictEqual(doc.isDirty, true); }); test('applyEdit should fail when editing deleted resource', withLogDisabled(async () => { @@ -651,7 +651,7 @@ suite('vscode API - workspace', () => { edit.insert(resource, new vscode.Position(0, 0), ''); let success = await vscode.workspace.applyEdit(edit); - assert.equal(success, false); + assert.strictEqual(success, false); })); test('applyEdit should fail when renaming deleted resource', withLogDisabled(async () => { @@ -662,7 +662,7 @@ suite('vscode API - workspace', () => { edit.renameFile(resource, resource); let success = await vscode.workspace.applyEdit(edit); - assert.equal(success, false); + assert.strictEqual(success, false); })); test('applyEdit should fail when editing renamed from resource', withLogDisabled(async () => { @@ -673,7 +673,7 @@ suite('vscode API - workspace', () => { edit.insert(resource, new vscode.Position(0, 0), ''); let success = await vscode.workspace.applyEdit(edit); - assert.equal(success, false); + assert.strictEqual(success, false); })); test('applyEdit "edit A -> rename A to B -> edit B"', async () => { @@ -699,8 +699,8 @@ suite('vscode API - workspace', () => { assert.ok(await vscode.workspace.applyEdit(edit)); let doc = await vscode.workspace.openTextDocument(newUri); - assert.equal(doc.getText(), 'AFTERBEFORE'); - assert.equal(doc.isDirty, true); + assert.strictEqual(doc.getText(), 'AFTERBEFORE'); + assert.strictEqual(doc.isDirty, true); } function nameWithUnderscore(uri: vscode.Uri) { @@ -719,7 +719,7 @@ suite('vscode API - workspace', () => { assert.ok(await vscode.workspace.applyEdit(we)); let doc = await vscode.workspace.openTextDocument(newUri); - assert.equal(doc.getText(), 'BarHelloFoo'); + assert.strictEqual(doc.getText(), 'BarHelloFoo'); })); test('WorkspaceEdit: Problem recreating a renamed resource #42634', withLogDisabled(async function () { @@ -737,9 +737,9 @@ suite('vscode API - workspace', () => { assert.ok(await vscode.workspace.applyEdit(we)); let newDoc = await vscode.workspace.openTextDocument(newUri); - assert.equal(newDoc.getText(), 'HelloFoo'); + assert.strictEqual(newDoc.getText(), 'HelloFoo'); let doc = await vscode.workspace.openTextDocument(docUri); - assert.equal(doc.getText(), 'Bar'); + assert.strictEqual(doc.getText(), 'Bar'); })); test('WorkspaceEdit api - after saving a deleted file, it still shows up as deleted. #42667', withLogDisabled(async function () { @@ -783,7 +783,7 @@ suite('vscode API - workspace', () => { let doc = await vscode.workspace.openTextDocument(newUri); assert.ok(doc); - assert.equal(doc.getText(), 'Hello'); + assert.strictEqual(doc.getText(), 'Hello'); }); test('WorkspaceEdit: rename resource followed by edit does not work #42638', withLogDisabled(async function () { @@ -797,7 +797,7 @@ suite('vscode API - workspace', () => { assert.ok(await vscode.workspace.applyEdit(we)); let doc = await vscode.workspace.openTextDocument(newUri); - assert.equal(doc.getText(), 'Hello'); + assert.strictEqual(doc.getText(), 'Hello'); })); test('WorkspaceEdit: create & override', withLogDisabled(async function () { @@ -807,12 +807,12 @@ suite('vscode API - workspace', () => { let we = new vscode.WorkspaceEdit(); we.createFile(docUri); assert.ok(!await vscode.workspace.applyEdit(we)); - assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), 'before'); + assert.strictEqual((await vscode.workspace.openTextDocument(docUri)).getText(), 'before'); we = new vscode.WorkspaceEdit(); we.createFile(docUri, { overwrite: true }); assert.ok(await vscode.workspace.applyEdit(we)); - assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), ''); + assert.strictEqual((await vscode.workspace.openTextDocument(docUri)).getText(), ''); })); test('WorkspaceEdit: create & ignoreIfExists', withLogDisabled(async function () { @@ -821,12 +821,12 @@ suite('vscode API - workspace', () => { let we = new vscode.WorkspaceEdit(); we.createFile(docUri, { ignoreIfExists: true }); assert.ok(await vscode.workspace.applyEdit(we)); - assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), 'before'); + assert.strictEqual((await vscode.workspace.openTextDocument(docUri)).getText(), 'before'); we = new vscode.WorkspaceEdit(); we.createFile(docUri, { overwrite: true, ignoreIfExists: true }); assert.ok(await vscode.workspace.applyEdit(we)); - assert.equal((await vscode.workspace.openTextDocument(docUri)).getText(), ''); + assert.strictEqual((await vscode.workspace.openTextDocument(docUri)).getText(), ''); })); test('WorkspaceEdit: rename & ignoreIfExists', withLogDisabled(async function () { @@ -880,9 +880,9 @@ suite('vscode API - workspace', () => { assert.ok(await vscode.workspace.applyEdit(we)); - assert.equal((await vscode.workspace.openTextDocument(f3)).getText(), 'f3'); - assert.equal((await vscode.workspace.openTextDocument(f2)).getText(), 'f2'); - assert.equal((await vscode.workspace.openTextDocument(f1_)).getText(), 'f1'); + assert.strictEqual((await vscode.workspace.openTextDocument(f3)).getText(), 'f3'); + assert.strictEqual((await vscode.workspace.openTextDocument(f2)).getText(), 'f2'); + assert.strictEqual((await vscode.workspace.openTextDocument(f1_)).getText(), 'f1'); try { await vscode.workspace.fs.stat(f1); assert.ok(false); @@ -932,12 +932,12 @@ suite('vscode API - workspace', () => { assert.ok(await vscode.workspace.applyEdit(we)); const document = await vscode.workspace.openTextDocument(newUri); - assert.equal(document.isDirty, true); + assert.strictEqual(document.isDirty, true); await document.save(); - assert.equal(document.isDirty, false); + assert.strictEqual(document.isDirty, false); - assert.equal(document.getText(), expected); + assert.strictEqual(document.getText(), expected); await delay(10); } @@ -959,7 +959,7 @@ suite('vscode API - workspace', () => { const document = await vscode.workspace.openTextDocument(file1); // const expected = 'import1;import2;'; const expected2 = 'import2;import1;'; - assert.equal(document.getText(), expected2); + assert.strictEqual(document.getText(), expected2); }); test('The api workspace.applyEdit failed for some case of mixing resourceChange and textEdit #80688', async function () { @@ -978,7 +978,7 @@ suite('vscode API - workspace', () => { const document = await vscode.workspace.openTextDocument(file1); const expected = 'import1;import2;'; // const expected2 = 'import2;import1;'; - assert.equal(document.getText(), expected); + assert.strictEqual(document.getText(), expected); }); test('Should send a single FileWillRenameEvent instead of separate events when moving multiple files at once#111867', async function () { @@ -1073,8 +1073,8 @@ suite('vscode API - workspace', () => { { const document = await vscode.workspace.openTextDocument(newFile); await vscode.window.showTextDocument(document); - assert.equal(document.getText(), 'hello2'); - assert.equal(document.isDirty, true); + assert.strictEqual(document.getText(), 'hello2'); + assert.strictEqual(document.isDirty, true); } // undo and show the old document @@ -1082,7 +1082,7 @@ suite('vscode API - workspace', () => { await vscode.commands.executeCommand('undo'); const document = await vscode.workspace.openTextDocument(file); await vscode.window.showTextDocument(document); - assert.equal(document.getText(), 'hello'); + assert.strictEqual(document.getText(), 'hello'); } // redo and show the new document @@ -1090,8 +1090,8 @@ suite('vscode API - workspace', () => { await vscode.commands.executeCommand('redo'); const document = await vscode.workspace.openTextDocument(newFile); await vscode.window.showTextDocument(document); - assert.equal(document.getText(), 'hello2'); - assert.equal(document.isDirty, true); + assert.strictEqual(document.getText(), 'hello2'); + assert.strictEqual(document.isDirty, true); } }); @@ -1111,8 +1111,8 @@ suite('vscode API - workspace', () => { // check the document { - assert.equal(document.getText(), 'hello2\nworld'); - assert.equal(document.isDirty, true); + assert.strictEqual(document.getText(), 'hello2\nworld'); + assert.strictEqual(document.isDirty, true); } // apply no-op edit @@ -1125,8 +1125,8 @@ suite('vscode API - workspace', () => { // undo { await vscode.commands.executeCommand('undo'); - assert.equal(document.getText(), 'hello\nworld'); - assert.equal(document.isDirty, false); + assert.strictEqual(document.getText(), 'hello\nworld'); + assert.strictEqual(document.isDirty, false); } }); }); diff --git a/lib/vscode/extensions/vscode-api-tests/src/utils.ts b/lib/vscode/extensions/vscode-api-tests/src/utils.ts index a36b75496267..b346efefce96 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/utils.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/utils.ts @@ -137,3 +137,15 @@ export async function asPromise(event: vscode.Event, timeout = vscode.env. }); }); } + +export function testRepeat(n: number, description: string, callback: (this: any) => any): void { + for (let i = 0; i < n; i++) { + test(`${description} (iteration ${i})`, callback); + } +} + +export function suiteRepeat(n: number, description: string, callback: (this: any) => any): void { + for (let i = 0; i < n; i++) { + suite(`${description} (iteration ${i})`, callback); + } +} diff --git a/lib/vscode/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts b/lib/vscode/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts index 1b4ef88325aa..5721e1ddf1be 100644 --- a/lib/vscode/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts +++ b/lib/vscode/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts @@ -21,7 +21,7 @@ suite('vscode API - workspace', () => { }); test('workspaceFolders', () => { - assert.equal(vscode.workspace.workspaceFolders!.length, 2); + assert.strictEqual(vscode.workspace.workspaceFolders!.length, 2); assert.ok(pathEquals(vscode.workspace.workspaceFolders![0].uri.fsPath, join(__dirname, '../../testWorkspace'))); assert.ok(pathEquals(vscode.workspace.workspaceFolders![1].uri.fsPath, join(__dirname, '../../testWorkspace2'))); assert.ok(pathEquals(vscode.workspace.workspaceFolders![1].name, 'Test Workspace 2')); diff --git a/lib/vscode/extensions/vscode-api-tests/yarn.lock b/lib/vscode/extensions/vscode-api-tests/yarn.lock index 0e3ca71c654a..b54d6b488365 100644 --- a/lib/vscode/extensions/vscode-api-tests/yarn.lock +++ b/lib/vscode/extensions/vscode-api-tests/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== diff --git a/lib/vscode/extensions/vscode-colorize-tests/package.json b/lib/vscode/extensions/vscode-colorize-tests/package.json index 0ec286b33853..b2ce2e9d9cfa 100644 --- a/lib/vscode/extensions/vscode-colorize-tests/package.json +++ b/lib/vscode/extensions/vscode-colorize-tests/package.json @@ -21,7 +21,7 @@ "jsonc-parser": "2.2.1" }, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "contributes": { "semanticTokenTypes": [ diff --git a/lib/vscode/extensions/vscode-colorize-tests/src/colorizer.test.ts b/lib/vscode/extensions/vscode-colorize-tests/src/colorizer.test.ts index 9d809409b401..8c4c70fd0b5c 100644 --- a/lib/vscode/extensions/vscode-colorize-tests/src/colorizer.test.ts +++ b/lib/vscode/extensions/vscode-colorize-tests/src/colorizer.test.ts @@ -64,7 +64,9 @@ suite('colorization', () => { for (const fixture of fs.readdirSync(fixturesPath)) { test(`colorize: ${fixture}`, function (done) { - assertUnchangedTokens(fixturesPath, resultsPath, fixture, done); + commands.executeCommand('workbench.action.closeAllEditors').then(() => { + assertUnchangedTokens(fixturesPath, resultsPath, fixture, done); + }); }); } }); diff --git a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.cs b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.cs index d4ebb2fe753d..c7098217742c 100644 --- a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.cs +++ b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.cs @@ -13,5 +13,15 @@ static void Main(string[] args) System.Console.WriteLine("Circumference = {0:N2}", circumference); } } + + public void TestMethod() + { + ListField = new List(); + + List localVar; + localVar = new List(); + + List localVar2 = new List(); + } } -} \ No newline at end of file +} diff --git a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart new file mode 100644 index 000000000000..e26254197bcd --- /dev/null +++ b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-fixtures/test.dart @@ -0,0 +1,19 @@ +// from https://flutter.dev/ + +import 'package:flutter/material.dart'; + +void main() async { + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: MyApp(), + ), + ), + ); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} \ No newline at end of file diff --git a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json index eb76b0e1d041..399fd0c22084 100644 --- a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json +++ b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json @@ -100,13 +100,13 @@ }, { "c": "%", - "t": "source.batchfile punctuation.definition.variable.batchfile", + "t": "source.batchfile variable.parameter.batchfile punctuation.definition.variable.batchfile", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF" + "hc_black": "variable: #9CDCFE" } }, { diff --git a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json index 5893af16e9db..5ebe142cb8f0 100644 --- a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json +++ b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json @@ -1341,6 +1341,743 @@ "hc_black": "default: #FFFFFF" } }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "public", + "t": "source.cs storage.modifier.cs", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "void", + "t": "source.cs keyword.type.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "TestMethod", + "t": "source.cs entity.name.function.cs", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "source.cs punctuation.parenthesis.open.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ")", + "t": "source.cs punctuation.parenthesis.close.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "{", + "t": "source.cs punctuation.curlybrace.open.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "ListField", + "t": "source.cs variable.other.readwrite.cs", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "=", + "t": "source.cs keyword.operator.assignment.cs", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "new", + "t": "source.cs keyword.other.new.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "List", + "t": "source.cs storage.type.cs", + "r": { + "dark_plus": "storage.type.cs: #4EC9B0", + "light_plus": "storage.type.cs: #267F99", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type.cs: #4EC9B0" + } + }, + { + "c": "<", + "t": "source.cs punctuation.definition.typeparameters.begin.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "int", + "t": "source.cs keyword.type.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": ">", + "t": "source.cs punctuation.definition.typeparameters.end.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "(", + "t": "source.cs punctuation.parenthesis.open.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ")", + "t": "source.cs punctuation.parenthesis.close.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cs punctuation.terminator.statement.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "List", + "t": "source.cs storage.type.cs", + "r": { + "dark_plus": "storage.type.cs: #4EC9B0", + "light_plus": "storage.type.cs: #267F99", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type.cs: #4EC9B0" + } + }, + { + "c": "<", + "t": "source.cs punctuation.definition.typeparameters.begin.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "int", + "t": "source.cs keyword.type.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": ">", + "t": "source.cs punctuation.definition.typeparameters.end.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "localVar", + "t": "source.cs entity.name.variable.local.cs", + "r": { + "dark_plus": "entity.name.variable: #9CDCFE", + "light_plus": "entity.name.variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cs punctuation.terminator.statement.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "localVar", + "t": "source.cs variable.other.readwrite.cs", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "=", + "t": "source.cs keyword.operator.assignment.cs", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "new", + "t": "source.cs keyword.other.new.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "List", + "t": "source.cs storage.type.cs", + "r": { + "dark_plus": "storage.type.cs: #4EC9B0", + "light_plus": "storage.type.cs: #267F99", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type.cs: #4EC9B0" + } + }, + { + "c": "<", + "t": "source.cs punctuation.definition.typeparameters.begin.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "int", + "t": "source.cs keyword.type.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": ">", + "t": "source.cs punctuation.definition.typeparameters.end.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "(", + "t": "source.cs punctuation.parenthesis.open.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ")", + "t": "source.cs punctuation.parenthesis.close.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cs punctuation.terminator.statement.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "List", + "t": "source.cs storage.type.cs", + "r": { + "dark_plus": "storage.type.cs: #4EC9B0", + "light_plus": "storage.type.cs: #267F99", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type.cs: #4EC9B0" + } + }, + { + "c": "<", + "t": "source.cs punctuation.definition.typeparameters.begin.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "int", + "t": "source.cs keyword.type.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": ">", + "t": "source.cs punctuation.definition.typeparameters.end.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "localVar2", + "t": "source.cs entity.name.variable.local.cs", + "r": { + "dark_plus": "entity.name.variable: #9CDCFE", + "light_plus": "entity.name.variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "=", + "t": "source.cs keyword.operator.assignment.cs", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "new", + "t": "source.cs keyword.other.new.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "List", + "t": "source.cs storage.type.cs", + "r": { + "dark_plus": "storage.type.cs: #4EC9B0", + "light_plus": "storage.type.cs: #267F99", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type.cs: #4EC9B0" + } + }, + { + "c": "<", + "t": "source.cs punctuation.definition.typeparameters.begin.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "int", + "t": "source.cs keyword.type.cs", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": ">", + "t": "source.cs punctuation.definition.typeparameters.end.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "(", + "t": "source.cs punctuation.parenthesis.open.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ")", + "t": "source.cs punctuation.parenthesis.close.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cs punctuation.terminator.statement.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "}", + "t": "source.cs punctuation.curlybrace.close.cs", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, { "c": " ", "t": "source.cs", @@ -1374,4 +2111,4 @@ "hc_black": "default: #FFFFFF" } } -] +] \ No newline at end of file diff --git a/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json new file mode 100644 index 000000000000..0d944b2b5a5f --- /dev/null +++ b/lib/vscode/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json @@ -0,0 +1,673 @@ +[ + { + "c": "// from https://flutter.dev/", + "t": "source.dart comment.line.double-slash.dart", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668" + } + }, + { + "c": "import", + "t": "source.dart meta.declaration.dart keyword.other.import.dart", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart meta.declaration.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "'package:flutter/material.dart'", + "t": "source.dart meta.declaration.dart string.interpolated.single.dart", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": ";", + "t": "source.dart meta.declaration.dart punctuation.terminator.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "void", + "t": "source.dart storage.type.primitive.dart", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "main", + "t": "source.dart entity.name.function.dart", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "() ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "async", + "t": "source.dart keyword.control.dart", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" + } + }, + { + "c": " {", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "runApp", + "t": "source.dart entity.name.function.dart", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MaterialApp", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "(", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " debugShowCheckedModeBanner", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":", + "t": "source.dart keyword.operator.ternary.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "false", + "t": "source.dart constant.language.dart", + "r": { + "dark_plus": "constant.language: #569CD6", + "light_plus": "constant.language: #0000FF", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "constant.language: #569CD6" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " home", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":", + "t": "source.dart keyword.operator.ternary.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "Scaffold", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "(", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " body", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ":", + "t": "source.dart keyword.operator.ternary.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MyApp", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "()", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " )", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " )", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ",", + "t": "source.dart punctuation.comma.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " )", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.dart punctuation.terminator.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "}", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "class", + "t": "source.dart keyword.declaration.dart", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MyApp", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "extends", + "t": "source.dart keyword.declaration.dart", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "StatefulWidget", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": " {", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "@override", + "t": "source.dart storage.type.annotation.dart", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "_MyAppState", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "createState", + "t": "source.dart entity.name.function.dart", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "() ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "=>", + "t": "source.dart keyword.operator.closure.dart", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4" + } + }, + { + "c": " ", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "_MyAppState", + "t": "source.dart support.class.dart", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": "()", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.dart punctuation.terminator.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "}", + "t": "source.dart", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + } +] \ No newline at end of file diff --git a/lib/vscode/extensions/vscode-colorize-tests/yarn.lock b/lib/vscode/extensions/vscode-colorize-tests/yarn.lock index a02d17759287..15725cff5029 100644 --- a/lib/vscode/extensions/vscode-colorize-tests/yarn.lock +++ b/lib/vscode/extensions/vscode-colorize-tests/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== jsonc-parser@2.2.1: version "2.2.1" diff --git a/lib/vscode/extensions/vscode-custom-editor-tests/package.json b/lib/vscode/extensions/vscode-custom-editor-tests/package.json index c718e324bc98..997c3a94da6e 100644 --- a/lib/vscode/extensions/vscode-custom-editor-tests/package.json +++ b/lib/vscode/extensions/vscode-custom-editor-tests/package.json @@ -22,7 +22,7 @@ "p-limit": "^3.0.2" }, "devDependencies": { - "@types/node": "^12.19.9", + "@types/node": "14.x", "@types/p-limit": "^2.2.0" }, "contributes": { diff --git a/lib/vscode/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts b/lib/vscode/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts index b4725c22de3e..654afb84cd78 100644 --- a/lib/vscode/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts +++ b/lib/vscode/extensions/vscode-custom-editor-tests/src/test/customEditor.test.ts @@ -92,7 +92,7 @@ suite('CustomEditor tests', () => { await vscode.commands.executeCommand(commands.open, testDocument); const { content } = await listener.nextResponse(); - assert.equal(content, startingContent); + assert.strictEqual(content, startingContent); }); test('Should support basic edits', async () => { @@ -107,7 +107,7 @@ suite('CustomEditor tests', () => { const newContent = `basic edit test`; await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); - assert.equal(content, newContent); + assert.strictEqual(content, newContent); }); test('Should support single undo', async () => { @@ -123,13 +123,13 @@ suite('CustomEditor tests', () => { { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); - assert.equal(content, newContent); + assert.strictEqual(content, newContent); } await delay(100); { await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); - assert.equal(content, startingContent); + assert.strictEqual(content, startingContent); } }); @@ -148,7 +148,7 @@ suite('CustomEditor tests', () => { for (let i = 0; i < count; ++i) { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `${i}`); const { content } = await listener.nextResponse(); - assert.equal(`${i}`, content); + assert.strictEqual(`${i}`, content); } // Then undo them in order @@ -156,14 +156,14 @@ suite('CustomEditor tests', () => { await delay(100); await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); - assert.equal(`${i - 1}`, content); + assert.strictEqual(`${i - 1}`, content); } { await delay(100); await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); - assert.equal(content, startingContent); + assert.strictEqual(content, startingContent); } }); @@ -184,8 +184,8 @@ suite('CustomEditor tests', () => { await vscode.workspace.applyEdit(edit); const response = (await listener.nextResponse()); - assert.equal(response.content, startingContent); - assert.equal(response.source.toString(), newFileName.toString()); + assert.strictEqual(response.content, startingContent); + assert.strictEqual(response.source.toString(), newFileName.toString()); }); test('Should support saving custom editors', async () => { @@ -201,12 +201,12 @@ suite('CustomEditor tests', () => { { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); - assert.equal(content, newContent); + assert.strictEqual(content, newContent); } { await vscode.commands.executeCommand(commands.save); const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString(); - assert.equal(fileContent, newContent); + assert.strictEqual(fileContent, newContent); } }); @@ -223,18 +223,18 @@ suite('CustomEditor tests', () => { { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); - assert.equal(content, newContent); + assert.strictEqual(content, newContent); } { await vscode.commands.executeCommand(commands.save); const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString(); - assert.equal(fileContent, newContent); + assert.strictEqual(fileContent, newContent); } await delay(100); { await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); - assert.equal(content, startingContent); + assert.strictEqual(content, startingContent); } }); @@ -244,14 +244,14 @@ suite('CustomEditor tests', () => { const untitledFile = randomFilePath({ root: testWorkspaceRoot, ext: '.abc' }).with({ scheme: 'untitled' }); await vscode.commands.executeCommand(commands.open, untitledFile); - assert.equal((await listener.nextResponse()).content, ''); + assert.strictEqual((await listener.nextResponse()).content, ''); await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `123`); - assert.equal((await listener.nextResponse()).content, '123'); + assert.strictEqual((await listener.nextResponse()).content, '123'); await vscode.commands.executeCommand(commands.save); const content = await fs.promises.readFile(untitledFile.fsPath); - assert.equal(content.toString(), '123'); + assert.strictEqual(content.toString(), '123'); }); test.skip('When switching away from a non-default custom editors and then back, we should continue using the non-default editor', async () => { diff --git a/lib/vscode/extensions/vscode-custom-editor-tests/yarn.lock b/lib/vscode/extensions/vscode-custom-editor-tests/yarn.lock index 29a81a09a498..76cc46da4b7e 100644 --- a/lib/vscode/extensions/vscode-custom-editor-tests/yarn.lock +++ b/lib/vscode/extensions/vscode-custom-editor-tests/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/p-limit@^2.2.0": version "2.2.0" diff --git a/lib/vscode/extensions/vscode-notebook-tests/package.json b/lib/vscode/extensions/vscode-notebook-tests/package.json index f1bdd988bee5..cc0264bb1a3a 100644 --- a/lib/vscode/extensions/vscode-notebook-tests/package.json +++ b/lib/vscode/extensions/vscode-notebook-tests/package.json @@ -20,7 +20,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" }, "contributes": { "commands": [ @@ -34,9 +34,9 @@ "icon": "$(debug)" } ], - "notebookProvider": [ + "notebooks": [ { - "viewType": "notebookSmokeTest", + "type": "notebookSmokeTest", "displayName": "Notebook Smoke Test", "selector": [ { @@ -46,7 +46,7 @@ ] } ], - "notebookOutputRenderer": [ + "notebookRenderer": [ { "id": "notebookCoreTestRenderer", "displayName": "Notebook Core Test Renderer", @@ -60,7 +60,7 @@ "notebook/cell/title": [ { "command": "vscode-notebook-tests.debugAction", - "when": "notebookViewType == notebookSmokeTest", + "when": "notebookType == notebookSmokeTest", "group": "inline@1" } ] diff --git a/lib/vscode/extensions/vscode-notebook-tests/src/customRenderer.js b/lib/vscode/extensions/vscode-notebook-tests/src/customRenderer.js index f23538e38a76..94fd028eee72 100644 --- a/lib/vscode/extensions/vscode-notebook-tests/src/customRenderer.js +++ b/lib/vscode/extensions/vscode-notebook-tests/src/customRenderer.js @@ -5,14 +5,7 @@ const vscode = acquireVsCodeApi(); -vscode.postMessage({ - type: 'custom_renderer_initialize', - payload: { - firstMessage: true - } -}); - -const notebook = acquireNotebookRendererApi('notebookCoreTestRenderer'); +const notebook = acquireNotebookRendererApi(); notebook.onDidCreateOutput(({ element, mimeType }) => { const div = document.createElement('div'); diff --git a/lib/vscode/extensions/vscode-notebook-tests/src/extension.ts b/lib/vscode/extensions/vscode-notebook-tests/src/extension.ts index ff0b4981d508..7160f07c1a10 100644 --- a/lib/vscode/extensions/vscode-notebook-tests/src/extension.ts +++ b/lib/vscode/extensions/vscode-notebook-tests/src/extension.ts @@ -20,24 +20,24 @@ export function activate(context: vscode.ExtensionContext): any { await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(notebookPath)); })); - context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookSmokeTest', { + context.subscriptions.push(vscode.workspace.registerNotebookContentProvider('notebookSmokeTest', { openNotebook: async (_resource: vscode.Uri) => { const dto: vscode.NotebookData = { - metadata: new vscode.NotebookDocumentMetadata(), + metadata: {}, cells: [ { - source: 'code()', - language: 'typescript', + value: 'code()', + languageId: 'typescript', kind: vscode.NotebookCellKind.Code, outputs: [], - metadata: new vscode.NotebookCellMetadata().with({ custom: { testCellMetadata: 123 } }) + metadata: { custom: { testCellMetadata: 123 } } }, { - source: 'Markdown Cell', - language: 'markdown', - kind: vscode.NotebookCellKind.Markdown, + value: 'Markdown Cell', + languageId: 'markdown', + kind: vscode.NotebookCellKind.Markup, outputs: [], - metadata: new vscode.NotebookCellMetadata().with({ custom: { testCellMetadata: 123 } }) + metadata: { custom: { testCellMetadata: 123 } } } ] }; @@ -58,7 +58,7 @@ export function activate(context: vscode.ExtensionContext): any { } })); - const controller = vscode.notebook.createNotebookController( + const controller = vscode.notebooks.createNotebookController( 'notebookSmokeTest', 'notebookSmokeTest', 'notebookSmokeTest' @@ -66,12 +66,12 @@ export function activate(context: vscode.ExtensionContext): any { controller.executeHandler = (cells) => { for (const cell of cells) { - const task = controller.createNotebookCellExecutionTask(cell); + const task = controller.createNotebookCellExecution(cell); task.start(); task.replaceOutput([new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/html', ['test output'], undefined) + vscode.NotebookCellOutputItem.text('test output', 'text/html') ])]); - task.end({ success: true }); + task.end(true); } }; diff --git a/lib/vscode/extensions/vscode-notebook-tests/yarn.lock b/lib/vscode/extensions/vscode-notebook-tests/yarn.lock index e03bdd573eab..995b2c2f8b32 100644 --- a/lib/vscode/extensions/vscode-notebook-tests/yarn.lock +++ b/lib/vscode/extensions/vscode-notebook-tests/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== diff --git a/lib/vscode/extensions/vscode-test-resolver/package.json b/lib/vscode/extensions/vscode-test-resolver/package.json index 2256c0473594..9aa252ea5110 100644 --- a/lib/vscode/extensions/vscode-test-resolver/package.json +++ b/lib/vscode/extensions/vscode-test-resolver/package.json @@ -27,7 +27,13 @@ ], "main": "./out/extension", "devDependencies": { - "@types/node": "^12.19.9" + "@types/node": "14.x" + }, + "capabilities": { + "untrustedWorkspaces": { + "supported": true + }, + "virtualWorkspaces": true }, "contributes": { "resourceLabelFormatters": [ @@ -83,7 +89,7 @@ "statusBar/remoteIndicator": [ { "command": "vscode-testresolver.newWindow", - "when": "!remoteName", + "when": "!remoteName && !virtualWorkspace", "group": "remote_90_test_1_local@2" }, { diff --git a/lib/vscode/extensions/vscode-test-resolver/src/extension.ts b/lib/vscode/extensions/vscode-test-resolver/src/extension.ts index c338957146bf..78301260ea7f 100644 --- a/lib/vscode/extensions/vscode-test-resolver/src/extension.ts +++ b/lib/vscode/extensions/vscode-test-resolver/src/extension.ts @@ -52,7 +52,7 @@ export function activate(context: vscode.ExtensionContext) { const match = lastProgressLine.match(/Extension host agent listening on (\d+)/); if (match) { isResolved = true; - res(new vscode.ResolvedAuthority('localhost', parseInt(match[1], 10))); // success! + res(new vscode.ResolvedAuthority('127.0.0.1', parseInt(match[1], 10))); // success! } lastProgressLine = ''; } else if (chr === CharCode.Backspace) { @@ -200,7 +200,7 @@ export function activate(context: vscode.ExtensionContext) { } }); }); - proxyServer.listen(0, () => { + proxyServer.listen(0, '127.0.0.1', () => { const port = (proxyServer.address()).port; outputChannel.appendLine(`Going through proxy at port ${port}`); const r: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', port); @@ -216,6 +216,9 @@ export function activate(context: vscode.ExtensionContext) { } const authorityResolverDisposable = vscode.workspace.registerRemoteAuthorityResolver('test', { + async getCanonicalURI(uri: vscode.Uri): Promise { + return vscode.Uri.file(uri.path); + }, resolve(_authority: string): Thenable { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, @@ -265,7 +268,7 @@ export function activate(context: vscode.ExtensionContext) { const port = Number.parseInt(result); vscode.workspace.openTunnel({ remoteAddress: { - host: 'localhost', + host: '127.0.0.1', port: port }, localAddressPort: port + 1 @@ -403,10 +406,10 @@ async function tunnelFactory(tunnelOptions: vscode.TunnelOptions, tunnelCreation if (localPort < 1024 && process.platform !== 'win32') { localPort = 0; } - proxyServer.listen(localPort, () => { + proxyServer.listen(localPort, '127.0.0.1', () => { const localPort = (proxyServer.address()).port; outputChannel.appendLine(`New test resolver tunnel service: Remote ${tunnelOptions.remoteAddress.port} -> local ${localPort}`); - const tunnel = newTunnel({ host: 'localhost', port: localPort }); + const tunnel = newTunnel({ host: '127.0.0.1', port: localPort }); tunnel.onDidDispose(() => proxyServer.close()); res(tunnel); }); @@ -420,8 +423,8 @@ function runHTTPTestServer(port: number): vscode.Disposable { res.end(`Hello, World from test server running on port ${port}!`); }); remoteServers.push(port); - server.listen(port); - const message = `Opened HTTP server on http://localhost:${port}`; + server.listen(port, '127.0.0.1'); + const message = `Opened HTTP server on http://127.0.0.1:${port}`; console.log(message); outputChannel.appendLine(message); return { diff --git a/lib/vscode/extensions/vscode-test-resolver/yarn.lock b/lib/vscode/extensions/vscode-test-resolver/yarn.lock index e03bdd573eab..995b2c2f8b32 100644 --- a/lib/vscode/extensions/vscode-test-resolver/yarn.lock +++ b/lib/vscode/extensions/vscode-test-resolver/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@14.x": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== diff --git a/lib/vscode/extensions/yarn.lock b/lib/vscode/extensions/yarn.lock index 067a5ebba64c..78b52f40dfb9 100644 --- a/lib/vscode/extensions/yarn.lock +++ b/lib/vscode/extensions/yarn.lock @@ -24,10 +24,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== +typescript@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== vscode-grammar-updater@^1.0.3: version "1.0.3" diff --git a/lib/vscode/package.json b/lib/vscode/package.json index 47fd59f1e07f..1403dad8d303 100644 --- a/lib/vscode/package.json +++ b/lib/vscode/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.56.1", - "distro": "278cafaa4343ba7b12773886685e04ece97fbdc1", + "version": "1.57.1", + "distro": "90b97e4e10fb8a1ab3cd4846f4dc56bfea8ea620", "author": { "name": "Microsoft Corporation" }, @@ -14,7 +14,7 @@ "preinstall": "node build/npm/preinstall.js", "postinstall": "node build/npm/postinstall.js", "compile": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile", - "watch": "npm-run-all -lp watch-client watch-extensions watch-extension-media", + "watch": "npm-run-all -lp watch-client watch-extensions", "watchd": "deemon yarn watch", "watch-webd": "deemon yarn watch-web", "kill-watchd": "deemon --kill yarn watch", @@ -24,12 +24,9 @@ "watch-client": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-client", "watch-clientd": "deemon yarn watch-client", "kill-watch-clientd": "deemon --kill yarn watch-client", - "watch-extensions": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extensions", - "watch-extension-media": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extension-media", + "watch-extensions": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", "watch-extensionsd": "deemon yarn watch-extensions", "kill-watch-extensionsd": "deemon --kill yarn watch-extensions", - "watch-extension-mediad": "deemon yarn watch-extension-media", - "kill-watch-extension-mediad": "deemon --kill yarn watch-extension-media", "mocha": "mocha test/unit/node/all.js --delay", "precommit": "node build/hygiene.js", "gulp": "node --max_old_space_size=8192 ./node_modules/gulp/bin/gulp.js", @@ -59,42 +56,39 @@ "core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci", "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci" }, - "dependencies_comment": "Move rimraf to dependencies because it is used in the postinstall script.", "dependencies": { - "@coder/logger": "^1.1.16", "applicationinsights": "1.0.8", "chokidar": "3.5.1", - "graceful-fs": "4.2.3", + "graceful-fs": "4.2.6", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "iconv-lite-umd": "0.6.8", - "jschardet": "2.3.0", + "jschardet": "3.0.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.3", "native-watchdog": "1.3.0", - "node-pty": "0.10.1", + "node-pty": "0.11.0-beta7", "nsfw": "2.1.2", "proxy-agent": "^4.0.1", "proxy-from-env": "^1.1.0", "rimraf": "^3.0.2", - "spdlog": "^0.11.1", + "spdlog": "^0.13.0", "sudo-prompt": "9.2.1", "tas-client-umd": "0.1.4", "v8-inspect-profiler": "^0.0.20", - "vscode-oniguruma": "1.3.1", + "vscode-oniguruma": "1.5.1", "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.3", "vscode-sqlite3": "4.0.11", - "vscode-textmate": "5.2.0", - "xterm": "4.12.0-beta.26", + "vscode-textmate": "5.4.0", + "xterm": "4.13.0-beta.1", "xterm-addon-search": "0.9.0-beta.2", "xterm-addon-unicode11": "0.3.0-beta.5", - "xterm-addon-webgl": "0.11.0-beta.8", + "xterm-addon-webgl": "0.11.1", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, - "devDependencies_comment": "set gulp-rename to ~1.2.0 (instead of ^1.2.0), build breaks on 1.4.4", "devDependencies": { "7zip": "0.0.6", "@types/applicationinsights": "0.20.0", @@ -106,9 +100,10 @@ "@types/graceful-fs": "4.1.2", "@types/gulp-postcss": "^8.0.0", "@types/http-proxy-agent": "^2.0.1", + "@types/keytar": "^4.4.0", "@types/minimist": "^1.2.1", "@types/mocha": "^8.2.0", - "@types/node": "^14.14.37", + "@types/node": "14.x", "@types/sinon": "^1.16.36", "@types/trusted-types": "^1.0.6", "@types/vscode-windows-registry": "^1.0.0", @@ -129,7 +124,6 @@ "cson-parser": "^1.3.3", "css-loader": "^3.2.0", "cssnano": "^5.0.2", - "postcss": "^8.2.1", "debounce": "^1.0.0", "deemon": "^1.4.0", "eslint": "6.8.0", @@ -140,7 +134,6 @@ "file-loader": "^4.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", - "gulp-atom-electron": "^1.30.1", "gulp-bom": "^3.0.0", "gulp-buffer": "0.0.2", "gulp-concat": "^2.6.1", @@ -153,12 +146,11 @@ "gulp-plumber": "^1.2.0", "gulp-postcss": "^9.0.0", "gulp-remote-retry-src": "^0.6.0", - "gulp-rename": "~1.2.0", + "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", "gulp-shell": "^0.6.5", "gulp-sourcemaps": "^3.0.0", "gulp-tsb": "4.0.6", - "gulp-untar": "^0.0.7", "gulp-vinyl-zip": "^2.1.2", "husky": "^0.13.1", "innosetup": "6.0.5", @@ -183,6 +175,7 @@ "optimist": "0.3.5", "p-all": "^1.0.0", "playwright": "1.8.0", + "postcss": "^8.2.1", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -193,7 +186,7 @@ "style-loader": "^1.0.0", "ts-loader": "^6.2.1", "tsec": "0.1.4", - "typescript": "^4.3.0-dev.20210426", + "typescript": "^4.4.0-dev.20210528", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "vinyl": "^2.0.0", @@ -216,14 +209,15 @@ }, "optionalDependencies": { "vscode-windows-registry": "1.0.3", - "windows-foreground-love": "0.2.0", - "windows-mutex": "0.3.0", + "windows-foreground-love": "0.4.0", + "windows-mutex": "0.4.1", "windows-process-tree": "0.3.0" }, "resolutions": { "postcss": "^8.2.1", "elliptic": "^6.5.3", "nwmatcher": "^1.4.4", - "chrome-remote-interface": "^0.30.0" + "chrome-remote-interface": "^0.30.0", + "glob-parent": "^5.1.2" } } diff --git a/lib/vscode/product.json b/lib/vscode/product.json index df09a299f097..787f4f9c44c3 100644 --- a/lib/vscode/product.json +++ b/lib/vscode/product.json @@ -25,15 +25,15 @@ "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", - "ms-vscode.github-browser", - "ms-vscode.github-richnav", "ms-vscode.remotehub", - "ms-vscode.remotehub-insiders" + "ms-vscode.remotehub-insiders", + "GitHub.remotehub", + "GitHub.remotehub-insiders" ], "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.27", + "version": "1.44.28", "repo": "https://github.com/microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -48,7 +48,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.42.6", + "version": "1.42.7", "repo": "https://github.com/microsoft/vscode-node-debug2", "metadata": { "id": "36d19e17-7569-4841-a001-947eb18602b2", @@ -93,7 +93,7 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.56.2", + "version": "1.57.0", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -120,41 +120,8 @@ }, "publisherDisplayName": "Microsoft" } - }, - { - "name": "ms-vscode.remotehub", - "version": "0.5.3", - "repo": "https://github.com/microsoft/vscode-remotehub", - "metadata": { - "id": "ed0ffe1d-36f0-49a0-bafe-da68355eb33a", - "publisherId": { - "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", - "publisherName": "ms-vscode", - "displayName": "Microsoft", - "flags": "verified" - }, - "publisherDisplayName": "Microsoft" - } - } - ], - "webBuiltInExtensions": [ - { - "name": "ms-vscode.remotehub", - "version": "0.5.3", - "repo": "https://github.com/microsoft/vscode-remotehub", - "metadata": { - "id": "ed0ffe1d-36f0-49a0-bafe-da68355eb33a", - "publisherId": { - "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", - "publisherName": "ms-vscode", - "displayName": "Microsoft", - "flags": "verified" - }, - "publisherDisplayName": "Microsoft" - } } ], - "//": "https://github.com/VSCodium/vscodium/pull/155/files", "documentationUrl": "https://go.microsoft.com/fwlink/?LinkID=533484#vscode", "keyboardShortcutsUrlMac": "https://go.microsoft.com/fwlink/?linkid=832143", diff --git a/lib/vscode/remote/package.json b/lib/vscode/remote/package.json index ff56643c0c83..df17e646d37a 100644 --- a/lib/vscode/remote/package.json +++ b/lib/vscode/remote/package.json @@ -6,31 +6,31 @@ "applicationinsights": "1.0.8", "chokidar": "3.5.1", "cookie": "^0.4.0", - "graceful-fs": "4.2.3", + "graceful-fs": "4.2.6", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "iconv-lite-umd": "0.6.8", - "jschardet": "2.3.0", + "jschardet": "3.0.0", "minimist": "^1.2.5", "native-watchdog": "1.3.0", - "node-pty": "0.10.1", + "node-pty": "0.11.0-beta7", "nsfw": "2.1.2", - "spdlog": "^0.11.1", + "spdlog": "^0.13.0", "tas-client-umd": "0.1.4", - "vscode-oniguruma": "1.3.1", + "vscode-oniguruma": "1.5.1", "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.3", - "vscode-textmate": "5.2.0", - "xterm": "4.12.0-beta.26", + "vscode-textmate": "5.4.0", + "xterm": "4.13.0-beta.1", "xterm-addon-search": "0.9.0-beta.2", "xterm-addon-unicode11": "0.3.0-beta.5", - "xterm-addon-webgl": "0.11.0-beta.8", + "xterm-addon-webgl": "0.11.1", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, "optionalDependencies": { - "vscode-windows-registry": "1.0.2", - "windows-process-tree": "0.2.4" + "vscode-windows-registry": "1.0.3", + "windows-process-tree": "0.3.0" } } diff --git a/lib/vscode/remote/web/package.json b/lib/vscode/remote/web/package.json index 317ccc0dcd2d..63e52d40495b 100644 --- a/lib/vscode/remote/web/package.json +++ b/lib/vscode/remote/web/package.json @@ -4,13 +4,13 @@ "private": true, "dependencies": { "iconv-lite-umd": "0.6.8", - "jschardet": "2.3.0", + "jschardet": "3.0.0", "tas-client-umd": "0.1.4", - "vscode-oniguruma": "1.3.1", - "vscode-textmate": "5.2.0", - "xterm": "4.12.0-beta.26", + "vscode-oniguruma": "1.5.1", + "vscode-textmate": "5.4.0", + "xterm": "4.13.0-beta.1", "xterm-addon-search": "0.9.0-beta.2", "xterm-addon-unicode11": "0.3.0-beta.5", - "xterm-addon-webgl": "0.11.0-beta.8" + "xterm-addon-webgl": "0.11.1" } } diff --git a/lib/vscode/remote/web/yarn.lock b/lib/vscode/remote/web/yarn.lock index 6f88d68228b0..31c8ed1e6996 100644 --- a/lib/vscode/remote/web/yarn.lock +++ b/lib/vscode/remote/web/yarn.lock @@ -7,25 +7,25 @@ iconv-lite-umd@0.6.8: resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.8.tgz#5ad310ec126b260621471a2d586f7f37b9958ec0" integrity sha512-zvXJ5gSwMC9JD3wDzH8CoZGc1pbiJn12Tqjk8BXYCnYz3hYL5GRjHW8LEykjXhV9WgNGI4rgpgHcbIiBfrRq6A== -jschardet@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" - integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== +jschardet@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" + integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== tas-client-umd@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.4.tgz#49db4130dd63a8342fabf77185a740fc6a7bea80" integrity sha512-1hFqJeLD3ryNikniIaO7TItlXhS5vx7bJ+wbPDf8o+IifgwwOWK2ARisdEM9SnJd0ccfcwNPG6Po+RiKn5L2hg== -vscode-oniguruma@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" - integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== +vscode-oniguruma@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" + integrity sha512-JrBZH8DCC262TEYcYdeyZusiETu0Vli0xFgdRwNJjDcObcRjbmJP+IFcA3ScBwIXwgFHYKbAgfxtM/Cl+3Spjw== -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +vscode-textmate@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" + integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== xterm-addon-search@0.9.0-beta.2: version "0.9.0-beta.2" @@ -37,12 +37,12 @@ xterm-addon-unicode11@0.3.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.5.tgz#7e490799d530d3b301125c7a4e92317c161761c4" integrity sha512-SgDDL3PoMH1G48JO6T45whKAex4NPxi80UzUVitnrqyd8dFQP+oF6cxqUutULgm9HSGk62qy3mrZvIMGO5VXog== -xterm-addon-webgl@0.11.0-beta.8: - version "0.11.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.0-beta.8.tgz#8cb4925d67c31beb8144275daf46358f42eff9fe" - integrity sha512-udRmQ/jgH8cL8VQOZweytkToIROevVeiA7WY0tIe878Wt2zKY+AYHZV8js3c1W9wHDu5G90BhmzTidJ5UwZK3Q== +xterm-addon-webgl@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.1.tgz#33dd250ab52e9f51d2ff52396447962e6f53e24c" + integrity sha512-xF6DnEoV+rPtzetMBXBZVe1kLKtus7AKdEcyfq2eMHQzhaRvC+pfnU+XiCXC85kueguqu2UkBHXZs5mihK9jOQ== -xterm@4.12.0-beta.26: - version "4.12.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0-beta.26.tgz#57c75b732808795398a66bc1a3e06d09eaff2ada" - integrity sha512-yZB1kMBXQu2G0G1ch7TUi6f893iTZC+tmfjw/PQNZTmN46b4oX1l7rplc3sFcdrICHtmQ0Q5n1u0d6WUAdq1Kw== +xterm@4.13.0-beta.1: + version "4.13.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.13.0-beta.1.tgz#ad2ad321c69a4add6e878c890f278a1da74fd7d0" + integrity sha512-gAMGqBglESTxQWph1uKVyd1jO/6eKsbicNG+Mr/YAsj06TjFVcLw839Iqu6P+DVFEV7lLLspcOb8fwX6qMBH/Q== diff --git a/lib/vscode/remote/yarn.lock b/lib/vscode/remote/yarn.lock index 2a0d2ccc6c6f..efeb6c53ebcd 100644 --- a/lib/vscode/remote/yarn.lock +++ b/lib/vscode/remote/yarn.lock @@ -214,12 +214,7 @@ glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" -graceful-fs@4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@4.2.6, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -309,10 +304,10 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -jschardet@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" - integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== +jschardet@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" + integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== jsonfile@^4.0.0: version "4.0.0" @@ -326,7 +321,7 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@^0.5.1: +mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -343,16 +338,11 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nan@^2.13.2: +nan@^2.13.2, nan@^2.14.0: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nan@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - native-watchdog@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.3.0.tgz#88cee94c9dc766b85c8506eda14c8bd8c9618e27" @@ -363,10 +353,10 @@ node-addon-api@*, node-addon-api@^3.0.2: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== -node-pty@0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" - integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== +node-pty@0.11.0-beta7: + version "0.11.0-beta7" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta7.tgz#aed0888b5032d96c54d8473455e6adfae3bbebbe" + integrity sha512-uApPGLglZRiHQcUMWakbZOrBo8HVWvhzIqNnrWvBGJOvc6m/S5lCdbbg93BURyJqHFmBS0GV+4hwiMNDuGRbSA== dependencies: nan "^2.14.0" @@ -446,13 +436,13 @@ socks@^2.3.3: ip "^1.1.5" smart-buffer "^4.1.0" -spdlog@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.11.1.tgz#29721b31018a5fe6a3ce2531f9d8d43e0bd6b825" - integrity sha512-M+sg9/Tnr0lrfnW2/hqgpoc4Z8Jzq7W8NUn35iiSslj+1uj1pgutI60MCpulDP2QyFzOpC8VsJmYD6Fub7wHoA== +spdlog@^0.13.0: + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" - mkdirp "^0.5.1" + mkdirp "^0.5.5" nan "^2.14.0" string_decoder@~0.10.x: @@ -477,10 +467,10 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -vscode-oniguruma@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" - integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== +vscode-oniguruma@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" + integrity sha512-JrBZH8DCC262TEYcYdeyZusiETu0Vli0xFgdRwNJjDcObcRjbmJP+IFcA3ScBwIXwgFHYKbAgfxtM/Cl+3Spjw== vscode-proxy-agent@^0.11.0: version "0.11.0" @@ -510,10 +500,10 @@ vscode-ripgrep@^1.11.3: https-proxy-agent "^4.0.0" proxy-from-env "^1.1.0" -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +vscode-textmate@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.0.tgz#4b25ffc1f14ac3a90faf9a388c67a01d24257cd7" + integrity sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w== vscode-windows-ca-certs@^0.3.0: version "0.3.0" @@ -522,15 +512,15 @@ vscode-windows-ca-certs@^0.3.0: dependencies: node-addon-api "^3.0.2" -vscode-windows-registry@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a" - integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA== +vscode-windows-registry@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.3.tgz#377e9a8bf75c0acac81a188282a4f16f748ecd47" + integrity sha512-IXCwNAm+H5yPCn6JBz89T9AAMgy5xEN2LxbxrvHPlErmyQqCYtpCCjvisfgT2dCuaJ2T9FfiqIeIrOpDm2Jc4Q== -windows-process-tree@0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.4.tgz#747af587b54cc6c996f2be0836cc8a8fd0dc038f" - integrity sha512-9gag9AHm3Iin/4YC1EwoIfZlqW/rG2eV7rJZ4Fy5NnAMGdewmnwsie5Rz+CJo2vSolqzzfw7hPeu3oOdniNejg== +windows-process-tree@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.0.tgz#cf0d9291b22fba2a7f5a687c8272866e28fbcafd" + integrity sha512-0bKI4gcd5MOsOpn2TdStCSlnjThtH6BdHrocekY9qCgTqgEtdaUs0B5BaqyzF9jXoTSwz38NMdE1F55o4fgv9Q== dependencies: nan "^2.13.2" @@ -549,15 +539,15 @@ xterm-addon-unicode11@0.3.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.5.tgz#7e490799d530d3b301125c7a4e92317c161761c4" integrity sha512-SgDDL3PoMH1G48JO6T45whKAex4NPxi80UzUVitnrqyd8dFQP+oF6cxqUutULgm9HSGk62qy3mrZvIMGO5VXog== -xterm-addon-webgl@0.11.0-beta.8: - version "0.11.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.0-beta.8.tgz#8cb4925d67c31beb8144275daf46358f42eff9fe" - integrity sha512-udRmQ/jgH8cL8VQOZweytkToIROevVeiA7WY0tIe878Wt2zKY+AYHZV8js3c1W9wHDu5G90BhmzTidJ5UwZK3Q== +xterm-addon-webgl@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.1.tgz#33dd250ab52e9f51d2ff52396447962e6f53e24c" + integrity sha512-xF6DnEoV+rPtzetMBXBZVe1kLKtus7AKdEcyfq2eMHQzhaRvC+pfnU+XiCXC85kueguqu2UkBHXZs5mihK9jOQ== -xterm@4.12.0-beta.26: - version "4.12.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0-beta.26.tgz#57c75b732808795398a66bc1a3e06d09eaff2ada" - integrity sha512-yZB1kMBXQu2G0G1ch7TUi6f893iTZC+tmfjw/PQNZTmN46b4oX1l7rplc3sFcdrICHtmQ0Q5n1u0d6WUAdq1Kw== +xterm@4.13.0-beta.1: + version "4.13.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.13.0-beta.1.tgz#ad2ad321c69a4add6e878c890f278a1da74fd7d0" + integrity sha512-gAMGqBglESTxQWph1uKVyd1jO/6eKsbicNG+Mr/YAsj06TjFVcLw839Iqu6P+DVFEV7lLLspcOb8fwX6qMBH/Q== yauzl@^2.9.2: version "2.10.0" diff --git a/lib/vscode/scripts/code.sh b/lib/vscode/scripts/code.sh index 3095f3897bf7..713040b5a27b 100755 --- a/lib/vscode/scripts/code.sh +++ b/lib/vscode/scripts/code.sh @@ -43,7 +43,7 @@ function code() { export ELECTRON_ENABLE_LOGGING=1 # Launch Code - exec "$CODE" . --no-sandbox "$@" + exec "$CODE" . "$@" } function code-wsl() diff --git a/lib/vscode/scripts/test-integration.sh b/lib/vscode/scripts/test-integration.sh index c6c116c2f679..0b2fe97b17d2 100755 --- a/lib/vscode/scripts/test-integration.sh +++ b/lib/vscode/scripts/test-integration.sh @@ -6,8 +6,10 @@ if [[ "$OSTYPE" == "darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) - # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. - LINUX_EXTRA_ARGS="--no-sandbox" + # --disable-setuid-sandbox: setuid sandboxes requires root and is used in containers so we disable this + # --disable-dev-shm-usage --use-gl=swiftshader: when run on docker containers where size of /dev/shm + # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory + LINUX_EXTRA_ARGS="--disable-setuid-sandbox --disable-dev-shm-usage --use-gl=swiftshader" fi VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` @@ -44,13 +46,6 @@ else export ELECTRON_ENABLE_STACK_DUMPING=1 export ELECTRON_ENABLE_LOGGING=1 - # Production builds are run on docker containers where size of /dev/shm partition < 64MB which causes OOM failure - # for chromium compositor that uses the partition for shared memory - if [ "$LINUX_EXTRA_ARGS" ] - then - LINUX_EXTRA_ARGS="$LINUX_EXTRA_ARGS --disable-dev-shm-usage --use-gl=swiftshader" - fi - echo "Storing crash reports into '$VSCODECRASHDIR'." echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." fi diff --git a/lib/vscode/scripts/test.bat b/lib/vscode/scripts/test.bat index b37aa32e0ad0..7232c26193ce 100644 --- a/lib/vscode/scripts/test.bat +++ b/lib/vscode/scripts/test.bat @@ -12,7 +12,7 @@ set NAMESHORT=%NAMESHORT:"=%.exe set CODE=".build\electron\%NAMESHORT%" :: Download Electron if needed -node build\lib\electron.js +call node build\lib\electron.js if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js electron :: Run tests diff --git a/lib/vscode/scripts/test.sh b/lib/vscode/scripts/test.sh index 7594af3d9762..68f75db60d8e 100755 --- a/lib/vscode/scripts/test.sh +++ b/lib/vscode/scripts/test.sh @@ -6,8 +6,10 @@ if [[ "$OSTYPE" == "darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) - # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. - LINUX_EXTRA_ARGS="--no-sandbox --disable-dev-shm-usage --use-gl=swiftshader" + # --disable-setuid-sandbox: setuid sandboxes requires root and is used in containers so we disable this + # --disable-dev-shm-usage --use-gl=swiftshader: when run on docker containers where size of /dev/shm + # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory + LINUX_EXTRA_ARGS="--disable-setuid-sandbox --disable-dev-shm-usage --use-gl=swiftshader" fi cd $ROOT diff --git a/lib/vscode/src/bootstrap-node.js b/lib/vscode/src/bootstrap-node.js index b9cc3f67b84f..9a57b93333cc 100644 --- a/lib/vscode/src/bootstrap-node.js +++ b/lib/vscode/src/bootstrap-node.js @@ -11,6 +11,7 @@ // - Posix: allow to change the current working dir via `VSCODE_CWD` if defined // - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups // TODO@bpasero revisit if chdir() on Windows is needed in the future still +// (find all users of `chdir` in code, there are more locations) function setupCurrentWorkingDirectory() { const path = require('path'); diff --git a/lib/vscode/src/bootstrap-window.js b/lib/vscode/src/bootstrap-window.js index ec6693603214..270dda4cd681 100644 --- a/lib/vscode/src/bootstrap-window.js +++ b/lib/vscode/src/bootstrap-window.js @@ -57,6 +57,11 @@ const configuration = await preloadGlobals.context.resolveConfiguration(); performance.mark('code/didWaitForWindowConfig'); + // Signal DOM modifications are now OK + if (typeof options?.canModifyDOM === 'function') { + options.canModifyDOM(configuration); + } + // Developer settings const { forceDisableShowDevtoolsOnError, @@ -79,11 +84,6 @@ // Enable ASAR support globalThis.MonacoBootstrap.enableASARSupport(configuration.appRoot); - // Signal DOM modifications are now OK - if (typeof options?.canModifyDOM === 'function') { - options.canModifyDOM(configuration); - } - // Get the nls configuration into the process.env as early as possible const nlsConfig = globalThis.MonacoBootstrap.setupNLS(); @@ -179,13 +179,6 @@ require(modulePaths, async result => { try { - // Wait for process environment being fully resolved - performance.mark('code/willWaitForShellEnv'); - if (!safeProcess.env['VSCODE_SKIP_PROCESS_ENV_PATCHING'] /* TODO@bpasero for https://github.com/microsoft/vscode/issues/108804 */) { - await safeProcess.shellEnv(); - } - performance.mark('code/didWaitForShellEnv'); - // Callback only after process environment is resolved const callbackResult = resultCallback(result, configuration); if (callbackResult instanceof Promise) { diff --git a/lib/vscode/src/tsec.exemptions.json b/lib/vscode/src/tsec.exemptions.json index 2f123f56f0ed..bac5c3c40057 100644 --- a/lib/vscode/src/tsec.exemptions.json +++ b/lib/vscode/src/tsec.exemptions.json @@ -20,6 +20,7 @@ "vs/editor/browser/view/domLineBreaksComputer.ts", "vs/editor/browser/view/viewLayer.ts", "vs/editor/browser/widget/diffEditorWidget.ts", + "vs/editor/contrib/inlineCompletions/ghostTextWidget.ts", "vs/editor/browser/widget/diffReview.ts", "vs/editor/standalone/browser/colorizer.ts", "vs/workbench/api/worker/extHostExtensionService.ts", diff --git a/lib/vscode/src/typings/electron.d.ts b/lib/vscode/src/typings/electron.d.ts index d093c27fc464..2c1375caf874 100644 --- a/lib/vscode/src/typings/electron.d.ts +++ b/lib/vscode/src/typings/electron.d.ts @@ -1,4 +1,4 @@ -// Type definitions for Electron 12.0.5 +// Type definitions for Electron 12.0.9 // Project: http://electronjs.org/ // Definitions by: The Electron Team // Definitions: https://github.com/electron/electron-typescript-definitions @@ -412,7 +412,7 @@ declare namespace Electron { * Emitted when the user wants to open a URL with the application. Your * application's `Info.plist` file must define the URL scheme within the * `CFBundleURLTypes` key, and set `NSPrincipalClass` to `AtomApplication`. - * + * You should call `event.preventDefault()` if you want to handle this event. * * @platform darwin @@ -757,7 +757,7 @@ You should call `event.preventDefault()` if you want to handle this event. * this event represents the `applicationWillFinishLaunching` notification of * `NSApplication`. You would usually set up listeners for the `open-file` and * `open-url` events here, and start the crash reporter and auto updater. - * + * In most cases, you should do everything in the `ready` event handler. */ on(event: 'will-finish-launching', listener: Function): this; @@ -818,7 +818,7 @@ This method can only be called before app is ready. disableDomainBlockingFor3DAPIs(): void; /** * Disables hardware acceleration for current app. - * + * This method can only be called before app is ready. */ disableHardwareAcceleration(): void; @@ -840,7 +840,7 @@ This method can only be called before app is ready. /** * On Linux, focuses on the first visible window. On macOS, makes the application * the active app. On Windows, focuses on the application's first window. - * + * You should seek to use the `steal` option as sparingly as possible. */ focus(options?: FocusOptions): void; @@ -936,7 +936,8 @@ You should seek to use the `steal` option as sparingly as possible. */ getJumpListSettings(): JumpListSettings; /** - * The current application locale. Possible return values are documented here. + * The current application locale, fetched using Chromium's `l10n_util` library. + * Possible return values are documented here. * * To set the locale, you'll want to use a command line switch at app startup, * which may be found here. @@ -950,7 +951,7 @@ You should seek to use the `steal` option as sparingly as possible. /** * User operating system's locale two-letter ISO 3166 country code. The value is * taken from native OS APIs. - * + * **Note:** When unable to detect locale country code, it returns empty string. */ getLocaleCountryCode(): string; @@ -1083,7 +1084,7 @@ You should seek to use the `steal` option as sparingly as possible. isReady(): boolean; /** * whether `Secure Keyboard Entry` is enabled. - * + * By default this API will return `false`. * * @platform darwin @@ -1325,7 +1326,7 @@ By default this API will return `false`. * **Note:** The maximum length of a Jump List item's `description` property is 260 * characters. Beyond this limit, the item will not be added to the Jump List, nor * will it be displayed. - * + * Here's a very simple example of creating a custom Jump List: * * @platform win32 @@ -1698,7 +1699,7 @@ Here's a very simple example of creating a custom Jump List: * emitted as `browser-backward`. * * The following app commands are explicitly supported on Linux: - * + * * `browser-backward` * `browser-forward` * @@ -1812,7 +1813,7 @@ Here's a very simple example of creating a custom Jump List: removeListener(event: 'move', listener: Function): this; /** * Emitted once when the window is moved to a new position. - * + * __Note__: On macOS this event is an alias of `move`. * * @platform darwin,win32 @@ -1998,7 +1999,7 @@ __Note__: On macOS this event is an alias of `move`. * normally only triggered when the user right clicks on the non-client area of * your window. This is the window titlebar or any area you have declared as * `-webkit-app-region: drag` in a frameless window. - * + * Calling `event.preventDefault()` will prevent the menu from being displayed. * * @platform win32 @@ -2112,7 +2113,7 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. * * **Note:** This API cannot be called before the `ready` event of the `app` module * is emitted. - * + * **Note:** This method is deprecated. Instead, use `ses.loadExtension(path)`. * * @deprecated @@ -2126,7 +2127,7 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. * * **Note:** This API cannot be called before the `ready` event of the `app` module * is emitted. - * + * **Note:** This method is deprecated. Instead, use `ses.loadExtension(path)`. * * @deprecated @@ -2158,7 +2159,7 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. * * **Note:** This API cannot be called before the `ready` event of the `app` module * is emitted. - * + * **Note:** This method is deprecated. Instead, use `ses.getAllExtensions()`. * * @deprecated @@ -2170,7 +2171,7 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. * * **Note:** This API cannot be called before the `ready` event of the `app` module * is emitted. - * + * **Note:** This method is deprecated. Instead, use `ses.getAllExtensions()`. * * @deprecated @@ -2302,7 +2303,7 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. getMaximumSize(): number[]; /** * Window id in the format of DesktopCapturerSource's id. For example - * "window:1234:0". + * "window:1324:0". * * More precisely the format is `window:id:other_id` where `id` is `HWND` on * Windows, `CGWindowID` (`uint64_t`) on macOS and `Window` (`unsigned long`) on @@ -2388,7 +2389,7 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. isAlwaysOnTop(): boolean; /** * Whether the window can be manually closed by user. - * + * On Linux always returns `true`. * * @platform darwin,win32 @@ -2427,7 +2428,7 @@ On Linux always returns `true`. isKiosk(): boolean; /** * Whether the window can be manually maximized by user. - * + * On Linux always returns `true`. * * @platform darwin,win32 @@ -2447,7 +2448,7 @@ On Linux always returns `true`. isMenuBarVisible(): boolean; /** * Whether the window can be manually minimized by the user. - * + * On Linux always returns `true`. * * @platform darwin,win32 @@ -2503,7 +2504,7 @@ On Linux always returns `true`. isVisible(): boolean; /** * Whether the window is visible on all workspaces. - * + * **Note:** This API always returns false on Windows. */ isVisibleOnAllWorkspaces(): boolean; @@ -2704,7 +2705,7 @@ On Linux always returns `true`. setEnabled(enable: boolean): void; /** * Changes whether the window can be focused. - * + * On macOS it does not remove the focus from the window. * * @platform darwin,win32 @@ -2961,13 +2962,13 @@ On macOS it does not remove the focus from the window. setVibrancy(type: (('appearance-based' | 'light' | 'dark' | 'titlebar' | 'selection' | 'menu' | 'popover' | 'sidebar' | 'medium-light' | 'ultra-dark' | 'header' | 'sheet' | 'window' | 'hud' | 'fullscreen-ui' | 'tooltip' | 'content' | 'under-window' | 'under-page')) | (null)): void; /** * Sets whether the window should be visible on all workspaces. - * + * **Note:** This API does nothing on Windows. */ setVisibleOnAllWorkspaces(visible: boolean, options?: VisibleOnAllWorkspacesOptions): void; /** * Sets whether the window traffic light buttons should be visible. - * + * This cannot be called when `titleBarStyle` is set to `customButtonsOnHover`. * * @platform darwin @@ -3665,7 +3666,7 @@ This cannot be called when `titleBarStyle` is set to `customButtonsOnHover`. removed: boolean) => void): this; /** * A promise which resolves when the cookie store has been flushed - * + * Writes any unwritten cookies data to disk. */ flushStore(): Promise; @@ -3678,13 +3679,13 @@ Writes any unwritten cookies data to disk. get(filter: CookiesGetFilter): Promise; /** * A promise which resolves when the cookie has been removed - * + * Removes the cookies matching `url` and `name` */ remove(url: string, name: string): Promise; /** * A promise which resolves when the cookie has been set - * + * Sets a cookie with `details`. */ set(details: CookiesSetDetails): Promise; @@ -3747,7 +3748,7 @@ Sets a cookie with `details`. * uploaded will be returned; even if a crash report is present on disk it will not * be returned until it is uploaded. In the case that there are no uploaded * reports, `null` is returned. - * + * **Note:** This method is only available in the main process. */ getLastCrashReport(): CrashReport; @@ -3765,7 +3766,7 @@ Sets a cookie with `details`. /** * Whether reports should be submitted to the server. Set through the `start` * method or `setUploadToServer`. - * + * **Note:** This method is only available in the main process. */ getUploadToServer(): boolean; @@ -3777,7 +3778,7 @@ Sets a cookie with `details`. /** * This would normally be controlled by user preferences. This has no effect if * called before `start` is called. - * + * **Note:** This method is only available in the main process. */ setUploadToServer(uploadToServer: boolean): void; @@ -3803,7 +3804,7 @@ Sets a cookie with `details`. * must be at most 39 bytes long, and values must be no longer than 127 bytes. Keys * with names longer than the maximum will be silently ignored. Key values longer * than the maximum length will be truncated. - * + * **Note:** This method is only available in the main process. */ start(options: CrashReporterStartOptions): void; @@ -3927,7 +3928,7 @@ Sets a cookie with `details`. * A promise that resolves with the response defined by the 'returns' attribute of * the command description in the remote debugging protocol or is rejected * indicating the failure of the command. - * + * Send given command to the debugging target. */ sendCommand(method: string, commandParams?: any, sessionId?: string): Promise; @@ -4501,7 +4502,7 @@ Send given command to the debugging target. getState(): ('progressing' | 'completed' | 'cancelled' | 'interrupted'); /** * The total size in bytes of the download item. - * + * If the size is unknown, it returns 0. */ getTotalBytes(): number; @@ -4747,7 +4748,7 @@ If the size is unknown, it returns 0. finishTransactionByDate(date: string): void; /** * Resolves with an array of `Product` objects. - * + * Retrieves the product descriptions. */ getProducts(productIDs: string[]): Promise; @@ -5541,7 +5542,7 @@ Retrieves the product descriptions. * * `echo -e '#import \nint main() { NSLog(@"%@", SYSTEM_IMAGE_NAME); * }' | clang -otest -x objective-c -framework Cocoa - && ./test` - * + * where `SYSTEM_IMAGE_NAME` should be replaced with any value from this list. * * @platform darwin @@ -5756,7 +5757,7 @@ where `SYSTEM_IMAGE_NAME` should be replaced with any value from this list. /** * resolves when the net log has begun recording. - * + * Starts recording network events to `path`. */ startLogging(path: string, options?: StartLoggingOptions): Promise; @@ -6139,7 +6140,7 @@ Calculate system idle time in seconds. isOnBatteryPower(): boolean; /** * A `Boolean` property. True if the system is on battery power. - * + * See `powerMonitor.isOnBatteryPower()`. */ onBatteryPower: boolean; @@ -6469,13 +6470,13 @@ Example: registerStringProtocol(scheme: string, handler: (request: ProtocolRequest, callback: (response: (string) | (ProtocolResponse)) => void) => void): boolean; /** * Whether the protocol was successfully unintercepted - * + * Remove the interceptor installed for `scheme` and restore its original handler. */ uninterceptProtocol(scheme: string): boolean; /** * Whether the protocol was successfully unregistered - * + * Unregisters the custom protocol of `scheme`. */ unregisterProtocol(scheme: string): boolean; @@ -7180,7 +7181,7 @@ e.g. clearAuthCache(): Promise; /** * resolves when the cache clear operation is complete. - * + * Clears the sessionโ€™s HTTP cache. */ clearCache(): Promise; @@ -7196,7 +7197,7 @@ Clears the host resolver cache. clearStorageData(options?: ClearStorageDataOptions): Promise; /** * Resolves when all connections are closed. - * + * **Note:** It will terminate / fail all requests currently in flight. */ closeAllConnections(): Promise; @@ -7346,7 +7347,7 @@ Clears the host resolver cache. * * Calling `setCertificateVerifyProc(null)` will revert back to default certificate * verify proc. - * + * > **NOTE:** The result of this procedure is cached by the network service. */ setCertificateVerifyProc(proc: ((request: Request, callback: (verificationResult: number) => void) => void) | (null)): void; @@ -7554,7 +7555,7 @@ Clears the host resolver cache. * Whether the item was successfully moved to the trash or otherwise deleted. * * > NOTE: This method is deprecated. Use `shell.trashItem` instead. - * + * Move the given file to trash and returns a boolean status for the operation. * * @deprecated @@ -7568,13 +7569,13 @@ Move the given file to trash and returns a boolean status for the operation. /** * Resolves with a string containing the error message corresponding to the failure * if a failure occurred, otherwise "". - * + * Open the given file in the desktop's default manner. */ openPath(path: string): Promise; /** * Resolves the shortcut link at `shortcutPath`. - * + * An exception will be thrown when any error happens. * * @platform win32 @@ -7594,7 +7595,7 @@ An exception will be thrown when any error happens. trashItem(path: string): Promise; /** * Whether the shortcut was created successfully. - * + * Creates or updates a shortcut link at `shortcutPath`. * * @platform win32 @@ -7602,7 +7603,7 @@ Creates or updates a shortcut link at `shortcutPath`. writeShortcutLink(shortcutPath: string, operation: 'create' | 'update' | 'replace', options: ShortcutDetails): boolean; /** * Whether the shortcut was created successfully. - * + * Creates or updates a shortcut link at `shortcutPath`. * * @platform win32 @@ -7776,7 +7777,7 @@ Creates or updates a shortcut link at `shortcutPath`. canPromptTouchID(): boolean; /** * The users current system wide accent color preference in RGBA hexadecimal form. - * + * This API is only available on macOS 10.14 Mojave or newer. * * @platform win32,darwin @@ -7790,7 +7791,7 @@ This API is only available on macOS 10.14 Mojave or newer. * whether scroll animations (e.g. produced by home/end key) should be enabled. * * `prefersReducedMotion` Boolean - Determines whether the user desires reduced * motion based on platform APIs. - * + * Returns an object with system animation settings. */ getAnimationSettings(): AnimationSettings; @@ -7878,7 +7879,7 @@ Returns an object with system animation settings. isAeroGlassEnabled(): boolean; /** * Whether the system is in Dark Mode. - * + * **Deprecated:** Should use the new `nativeTheme.shouldUseDarkColors` API. * * @deprecated @@ -7982,7 +7983,7 @@ Returns an object with system animation settings. * * Note that `type` should match actual type of `value`. An exception is thrown if * they don't. - * + * Some popular `key` and `type`s are: * `ApplePressAndHoldEnabled`: `boolean` @@ -8061,7 +8062,7 @@ Some popular `key` and `type`s are: * * Possible values that can be set are `dark` and `light`, and possible return * values are `dark`, `light`, and `unknown`. - * + * This property is only available on macOS 10.14 Mojave or newer. * * @platform darwin @@ -8786,7 +8787,7 @@ This property is only available on macOS 10.14 Mojave or newer. /** * Pops up the context menu of the tray icon. When `menu` is passed, the `menu` * will be shown instead of the tray icon's context menu. - * + * The `position` is only available on Windows, and it is (0, 0) by default. * * @platform darwin,win32 @@ -8805,7 +8806,7 @@ The `position` is only available on Windows, and it is (0, 0) by default. /** * Sets the option to ignore double click events. Ignoring these events allows you * to detect every individual click of the tray icon. - * + * This value is set to false by default. * * @platform darwin @@ -8914,7 +8915,7 @@ This value is set to false by default. * Emitted before dispatching the `keydown` and `keyup` events in the page. Calling * `event.preventDefault` will prevent the page `keydown`/`keyup` events and the * menu shortcuts. - * + * To only prevent the menu shortcuts, use `setIgnoreMenuShortcuts`: */ on(event: 'before-input-event', listener: (event: Event, @@ -8939,7 +8940,7 @@ To only prevent the menu shortcuts, use `setIgnoreMenuShortcuts`: input: Input) => void): this; /** * Emitted when failed to verify the `certificate` for `url`. - * + * The usage is the same with the `certificate-error` event of `app`. */ on(event: 'certificate-error', listener: (event: Event, @@ -9609,7 +9610,7 @@ The usage is the same with the `certificate-error` event of `app`. removeListener(event: 'leave-html-full-screen', listener: Function): this; /** * Emitted when `webContents` wants to do basic auth. - * + * The usage is the same with the `login` event of `app`. */ on(event: 'login', listener: (event: Event, @@ -10009,7 +10010,7 @@ The usage is the same with the `login` event of `app`. callback: (deviceId: string) => void) => void): this; /** * Emitted when a client certificate is requested. - * + * The usage is the same with the `select-client-certificate` event of `app`. */ on(event: 'select-client-certificate', listener: (event: Event, @@ -10303,7 +10304,7 @@ Code execution will be suspended until web page stop loading. /** * A promise that resolves with the result of the executed code or is rejected if * the result of the code is a rejected promise. - * + * Works like `executeJavaScript` but evaluates `scripts` in an isolated context. */ executeJavaScriptInIsolatedWorld(worldId: number, scripts: WebSource[], userGesture?: boolean): Promise; @@ -10406,7 +10407,7 @@ Works like `executeJavaScript` but evaluates `scripts` in an isolated context. * Increase the capturer count by one. The page is considered visible when its * browser window is hidden and the capturer count is non-zero. If you would like * the page to stay hidden, you should ensure that `stayHidden` is set to true. - * + * This also affects the Page Visibility API. */ incrementCapturerCount(size?: Size, stayHidden?: boolean): void; @@ -10559,7 +10560,7 @@ For example: * printing. * * Use `page-break-before: always;` CSS style to force to print to a new page. - * + * Example usage: */ print(options?: WebContentsPrintOptions, callback?: (success: boolean, failureReason: string) => void): void; @@ -10574,7 +10575,7 @@ Example usage: * By default, an empty `options` will be regarded as: * * Use `page-break-before: always; ` CSS style to force to print to a new page. - * + * An example of `webContents.printToPDF`: */ printToPDF(options: PrintToPDFOptions): Promise; @@ -10628,7 +10629,7 @@ An example of `webContents.printToPDF`: * * The renderer process can handle the message by listening to `channel` with the * `ipcRenderer` module. - * + * An example of sending messages from the main process to the renderer process: */ send(channel: string, ...args: any[]): void; @@ -10652,7 +10653,7 @@ An example of sending messages from the main process to the renderer process: * * If you want to get the `frameId` of a given renderer context you should use the * `webFrame.routingId` value. E.g. - * + * You can also read `frameId` from all incoming IPC messages in the main process. */ sendToFrame(frameId: (number) | ([number, number]), channel: string, ...args: any[]): void; @@ -10681,7 +10682,7 @@ You can also read `frameId` from all incoming IPC messages in the main process. * caller's responsibility to destroy `devToolsWebContents`. * * An example of showing devtools in a `` tag: - * + * An example of showing devtools in a `BrowserWindow`: */ setDevToolsWebContents(devToolsWebContents: WebContents): void; @@ -10765,7 +10766,7 @@ The factor must be greater than 0.0. stopPainting(): void; /** * Indicates whether the snapshot has been created successfully. - * + * Takes a V8 heap snapshot and saves it to `filePath`. */ takeHeapSnapshot(filePath: string): Promise; @@ -11064,7 +11065,7 @@ For example: * The `uploadData` is an array of `UploadData` objects. * * The `callback` has to be called with an `response` object. - * + * Some examples of valid `urls`: */ onBeforeRequest(filter: Filter, listener: ((details: OnBeforeRequestListenerDetails, callback: (response: Response) => void) => void) | (null)): void; @@ -11075,7 +11076,7 @@ Some examples of valid `urls`: * The `uploadData` is an array of `UploadData` objects. * * The `callback` has to be called with an `response` object. - * + * Some examples of valid `urls`: */ onBeforeRequest(listener: ((details: OnBeforeRequestListenerDetails, callback: (response: Response) => void) => void) | (null)): void; @@ -11083,7 +11084,7 @@ Some examples of valid `urls`: * The `listener` will be called with `listener(details, callback)` before sending * an HTTP request, once the request headers are available. This may occur after a * TCP connection is made to the server, but before any http data is sent. - * + * The `callback` has to be called with a `response` object. */ onBeforeSendHeaders(filter: Filter, listener: ((details: OnBeforeSendHeadersListenerDetails, callback: (beforeSendResponse: BeforeSendResponse) => void) => void) | (null)): void; @@ -11091,7 +11092,7 @@ The `callback` has to be called with a `response` object. * The `listener` will be called with `listener(details, callback)` before sending * an HTTP request, once the request headers are available. This may occur after a * TCP connection is made to the server, but before any http data is sent. - * + * The `callback` has to be called with a `response` object. */ onBeforeSendHeaders(listener: ((details: OnBeforeSendHeadersListenerDetails, callback: (beforeSendResponse: BeforeSendResponse) => void) => void) | (null)): void; @@ -11116,14 +11117,14 @@ The `callback` has to be called with a `response` object. /** * The `listener` will be called with `listener(details, callback)` when HTTP * response headers of a request have been received. - * + * The `callback` has to be called with a `response` object. */ onHeadersReceived(filter: Filter, listener: ((details: OnHeadersReceivedListenerDetails, callback: (headersReceivedResponse: HeadersReceivedResponse) => void) => void) | (null)): void; /** * The `listener` will be called with `listener(details, callback)` when HTTP * response headers of a request have been received. - * + * The `callback` has to be called with a `response` object. */ onHeadersReceived(listener: ((details: OnHeadersReceivedListenerDetails, callback: (headersReceivedResponse: HeadersReceivedResponse) => void) => void) | (null)): void; @@ -11244,7 +11245,7 @@ The `callback` has to be called with a `response` object. removeEventListener(event: 'found-in-page', listener: (event: FoundInPageEvent) => void): this; /** * Fired when the guest page attempts to open a new browser window. - * + * The following example code opens the new url in system's default browser. */ addEventListener(event: 'new-window', listener: (event: NewWindowEvent) => void, useCapture?: boolean): this; @@ -11536,7 +11537,7 @@ Calling `event.preventDefault()` does __NOT__ have any effect. print(options?: WebviewTagPrintOptions): Promise; /** * Resolves with the generated PDF data. - * + * Prints `webview`'s web page as PDF, Same as `webContents.printToPDF(options)`. */ printToPDF(options: PrintToPDFOptions): Promise; @@ -11575,13 +11576,13 @@ Prints `webview`'s web page as PDF, Same as `webContents.printToPDF(options)`. * Send an asynchronous message to renderer process via `channel`, you can also * send arbitrary arguments. The renderer process can handle the message by * listening to the `channel` event with the `ipcRenderer` module. - * + * See webContents.send for examples. */ send(channel: string, ...args: any[]): Promise; /** * Sends an input `event` to the page. - * + * See webContents.sendInputEvent for detailed description of `event` object. */ sendInputEvent(event: (MouseInputEvent) | (MouseWheelInputEvent) | (KeyboardInputEvent)): Promise; @@ -12769,7 +12770,8 @@ See webContents.sendInputEvent for detailed description of `event` object. */ forward?: boolean; /** - * Whether the operation is first request or a follow up, defaults to `false`. + * Whether to begin a new text finding session with this request. Should be `true` + * for initial requests, and `false` for follow-up requests. Defaults to `false`. */ findNext?: boolean; /** @@ -13991,6 +13993,9 @@ See webContents.sendInputEvent for detailed description of `event` object. } interface SaveDialogOptions { + /** + * The dialog title. Cannot be displayed on some _Linux_ desktop environments. + */ title?: string; /** * Absolute directory path, absolute file path, or file name to use by default. @@ -14051,6 +14056,9 @@ See webContents.sendInputEvent for detailed description of `event` object. } interface SaveDialogSyncOptions { + /** + * The dialog title. Cannot be displayed on some _Linux_ desktop environments. + */ title?: string; /** * Absolute directory path, absolute file path, or file name to use by default. @@ -15871,7 +15879,7 @@ declare namespace NodeJS { * * `allocated` Integer - Size of all allocated objects in Kilobytes. * * `marked` Integer - Size of all marked objects in Kilobytes. * * `total` Integer - Total allocated space in Kilobytes. - * + * * Returns an object with Blink memory information. It can be useful for debugging * rendering / DOM related memory issues. Note that all values are reported in * Kilobytes. @@ -15881,7 +15889,7 @@ declare namespace NodeJS { /** * The number of milliseconds since epoch, or `null` if the information is * unavailable - * + * * Indicates the creation time of the application. The time is represented as * number of milliseconds since epoch. It returns null if it is unable to get the * process creation time. @@ -15897,7 +15905,7 @@ declare namespace NodeJS { * * `mallocedMemory` Integer * * `peakMallocedMemory` Integer * * `doesZapGarbage` Boolean - * + * * Returns an object with V8 heap statistics. Note that all statistics are reported * in Kilobytes. */ @@ -15905,11 +15913,11 @@ declare namespace NodeJS { getIOCounters(): Electron.IOCounters; /** * Resolves with a ProcessMemoryInfo - * + * * Returns an object giving memory usage statistics about the current process. Note * that all statistics are reported in Kilobytes. This api should be called after * app ready. - * + * * Chromium does not provide `residentSet` value for macOS. This is because macOS * performs in-memory compression of pages that haven't been recently used. As a * result the resident set size value is not what one would expect. `private` @@ -15926,16 +15934,16 @@ declare namespace NodeJS { * Kilobytes available to the system. * * `swapFree` Integer _Windows_ _Linux_ - The free amount of swap memory in * Kilobytes available to the system. - * + * * Returns an object giving memory usage statistics about the entire system. Note * that all statistics are reported in Kilobytes. */ getSystemMemoryInfo(): Electron.SystemMemoryInfo; /** * The version of the host operating system. - * + * * Example: - * + * * **Note:** It returns the actual operating system version instead of kernel * version on macOS unlike `os.release()`. */ @@ -15953,7 +15961,7 @@ declare namespace NodeJS { setFdLimit(maxDescriptors: number): void; /** * Indicates whether the snapshot has been created successfully. - * + * Takes a V8 heap snapshot and saves it to `filePath`. */ takeHeapSnapshot(filePath: string): boolean; @@ -16030,7 +16038,7 @@ Takes a V8 heap snapshot and saves it to `filePath`. traceProcessWarnings: boolean; /** * A `String` representing the current process's type, can be: - * + * * * `browser` - The main process * * `renderer` - A renderer process * `worker` - In a web worker diff --git a/lib/vscode/src/vs/base/browser/browser.ts b/lib/vscode/src/vs/base/browser/browser.ts index 485df81cb29f..ed6b696e40cb 100644 --- a/lib/vscode/src/vs/base/browser/browser.ts +++ b/lib/vscode/src/vs/base/browser/browser.ts @@ -115,7 +115,6 @@ export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0); export const isChrome = (userAgent.indexOf('Chrome') >= 0); export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0)); export const isWebkitWebView = (!isChrome && !isSafari && isWebKit); -export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0)); export const isEdgeLegacyWebView = (userAgent.indexOf('Edge/') >= 0) && (userAgent.indexOf('WebView/') >= 0); export const isElectron = (userAgent.indexOf('Electron/') >= 0); export const isAndroid = (userAgent.indexOf('Android') >= 0); diff --git a/lib/vscode/src/vs/base/browser/dom.ts b/lib/vscode/src/vs/base/browser/dom.ts index 022fe7c0c193..a5d014dc8a80 100644 --- a/lib/vscode/src/vs/base/browser/dom.ts +++ b/lib/vscode/src/vs/base/browser/dom.ts @@ -17,6 +17,7 @@ import { FileAccess, RemoteAuthorities } from 'vs/base/common/network'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { insane, InsaneOptions } from 'vs/base/common/insane/insane'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { withNullAsUndefined } from 'vs/base/common/types'; export function clearNode(node: HTMLElement): void { while (node.firstChild) { @@ -347,16 +348,7 @@ export function getClientArea(element: HTMLElement): Dimension { // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight if (platform.isIOS && window.visualViewport) { - const width = window.visualViewport.width; - const height = window.visualViewport.height - ( - browser.isStandalone - // in PWA mode, the visual viewport always includes the safe-area-inset-bottom (which is for the home indicator) - // even when you are using the onscreen monitor, the visual viewport will include the area between system statusbar and the onscreen keyboard - // plus the area between onscreen keyboard and the bottom bezel, which is 20px on iOS. - ? (20 + 4) // + 4px for body margin - : 0 - ); - return new Dimension(width, height); + return new Dimension(window.visualViewport.width, window.visualViewport.height); } // Try innerWidth / innerHeight @@ -1187,28 +1179,44 @@ export function computeScreenAwareSize(cssPx: number): number { } /** + * Open safely a new window. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the brower's popup blocker. + * If you want to tell if the browser blocked the new window, use `windowOpenNoOpenerWithSuccess`. + * * See https://github.com/microsoft/monaco-editor/issues/601 * To protect against malicious code in the linked site, particularly phishing attempts, * the window.opener should be set to null to prevent the linked site from having access * to change the location of the current page. * See https://mathiasbynens.github.io/rel-noopener/ */ -export function windowOpenNoOpener(url: string): boolean { - if (browser.isElectron || browser.isEdgeLegacyWebView) { - // In VSCode, window.open() always returns null... - // The same is true for a WebView (see https://github.com/microsoft/monaco-editor/issues/628) - // Also call directly window.open in sandboxed Electron (see https://github.com/microsoft/monaco-editor/issues/2220) - window.open(url); +export function windowOpenNoOpener(url: string): void { + // By using 'noopener' in the `windowFeatures` argument, the newly created window will + // not be able to use `window.opener` to reach back to the current page. + // See https://stackoverflow.com/a/46958731 + // See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener + // However, this also doesn't allow us to realize if the browser blocked + // the creation of the window. + window.open(url, '_blank', 'noopener'); +} + +/** + * Open safely a new window. This technique is not appropiate in certain contexts, + * like for example when the JS context is executing inside a sandboxed iframe. + * If it is not necessary to know if the browser blocked the new window, use + * `windowOpenNoOpener`. + * + * See https://github.com/microsoft/monaco-editor/issues/601 + * See https://github.com/microsoft/monaco-editor/issues/2474 + * See https://mathiasbynens.github.io/rel-noopener/ + */ +export function windowOpenNoOpenerWithSuccess(url: string): boolean { + const newTab = window.open(); + if (newTab) { + (newTab as any).opener = null; + newTab.location.href = url; return true; - } else { - let newTab = window.open(); - if (newTab) { - (newTab as any).opener = null; - newTab.location.href = url; - return true; - } - return false; } + return false; } export function animate(fn: () => void): IDisposable { @@ -1266,6 +1274,29 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void setTimeout(() => document.body.removeChild(anchor)); } +export function triggerUpload(): Promise { + return new Promise(resolve => { + + // In order to upload to the browser, create a + // input element of type `file` and click it + // to gather the selected files + const input = document.createElement('input'); + document.body.appendChild(input); + input.type = 'file'; + input.multiple = true; + + // Resolve once the input event has fired once + Event.once(Event.fromDOMEventEmitter(input, 'input'))(() => { + resolve(withNullAsUndefined(input.files)); + }); + + input.click(); + + // Ensure to remove the element from DOM eventually + setTimeout(() => document.body.removeChild(input)); + }); +} + export enum DetectedFullscreenMode { /** @@ -1450,6 +1481,9 @@ export class ModifierKeyEmitter extends Emitter { }; this._subscriptions.add(domEvent(window, 'keydown', true)(e => { + if (e.defaultPrevented) { + return; + } const event = new StandardKeyboardEvent(e); // If Alt-key keydown event is repeated, ignore it #112347 @@ -1484,6 +1518,10 @@ export class ModifierKeyEmitter extends Emitter { })); this._subscriptions.add(domEvent(window, 'keyup', true)(e => { + if (e.defaultPrevented) { + return; + } + if (!e.altKey && this._keyStatus.altKey) { this._keyStatus.lastKeyReleased = 'alt'; } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { @@ -1574,3 +1612,13 @@ export function getCookieValue(name: string): string | undefined { return match ? match.pop() : undefined; } + +export function addMatchMediaChangeListener(query: string, callback: () => void): void { + const mediaQueryList = window.matchMedia(query); + if (typeof mediaQueryList.addEventListener === 'function') { + mediaQueryList.addEventListener('change', callback); + } else { + // Safari 13.x + mediaQueryList.addListener(callback); + } +} diff --git a/lib/vscode/src/vs/base/browser/event.ts b/lib/vscode/src/vs/base/browser/event.ts index 9ae46f15147a..a1214db777db 100644 --- a/lib/vscode/src/vs/base/browser/event.ts +++ b/lib/vscode/src/vs/base/browser/event.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { GestureEvent } from 'vs/base/browser/touch'; import { Event as BaseEvent, Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; export type EventHandler = HTMLElement | HTMLDocument | Window; @@ -12,6 +14,9 @@ export interface IDomEvent { (element: EventHandler, type: string, useCapture?: boolean): BaseEvent; } +/** + * @deprecated Use `DomEmitter` instead + */ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => { const fn = (e: Event) => emitter.fire(e); const emitter = new Emitter({ @@ -26,6 +31,35 @@ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapt return emitter.event; }; +export interface DOMEventMap extends HTMLElementEventMap { + '-monaco-gesturetap': GestureEvent; + '-monaco-gesturechange': GestureEvent; + '-monaco-gesturestart': GestureEvent; + '-monaco-gesturesend': GestureEvent; + '-monaco-gesturecontextmenu': GestureEvent; +} + +export class DomEmitter implements IDisposable { + + private emitter: Emitter; + + get event(): BaseEvent { + return this.emitter.event; + } + + constructor(element: EventHandler, type: K, useCapture?: boolean) { + const fn = (e: Event) => this.emitter.fire(e as DOMEventMap[K]); + this.emitter = new Emitter({ + onFirstListenerAdd: () => element.addEventListener(type, fn, useCapture), + onLastListenerRemove: () => element.removeEventListener(type, fn, useCapture) + }); + } + + dispose(): void { + this.emitter.dispose(); + } +} + export interface CancellableEvent { preventDefault(): void; stopPropagation(): void; diff --git a/lib/vscode/src/vs/base/browser/globalMouseMoveMonitor.ts b/lib/vscode/src/vs/base/browser/globalMouseMoveMonitor.ts index 4911b59e0801..6745aafbb135 100644 --- a/lib/vscode/src/vs/base/browser/globalMouseMoveMonitor.ts +++ b/lib/vscode/src/vs/base/browser/globalMouseMoveMonitor.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IframeUtils } from 'vs/base/browser/iframe'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { isIOS } from 'vs/base/common/platform'; export interface IStandardMouseMoveEventData { leftButton: boolean; @@ -88,7 +89,7 @@ export class GlobalMouseMoveMonitor implements I this._onStopCallback = onStopCallback; const windowChain = IframeUtils.getSameOriginWindowChain(); - const mouseMove = 'mousemove'; + const mouseMove = isIOS ? 'pointermove' : 'mousemove'; // Safari sends wrong event, workaround for #122653 const mouseUp = 'mouseup'; const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document); diff --git a/lib/vscode/src/vs/base/browser/markdownRenderer.ts b/lib/vscode/src/vs/base/browser/markdownRenderer.ts index 5378627e57ae..78451aa3776a 100644 --- a/lib/vscode/src/vs/base/browser/markdownRenderer.ts +++ b/lib/vscode/src/vs/base/browser/markdownRenderer.ts @@ -312,6 +312,14 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti }; } +/** + * 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 typeof string === 'string' ? string : renderMarkdownAsPlaintext(string); +} + /** * Strips all markdown from `markdown`. For example `# Header` would be output as `Header`. */ @@ -386,6 +394,7 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString) { const unescapeInfo = new Map([ ['"', '"'], + [' ', ' '], ['&', '&'], [''', '\''], ['<', '<'], diff --git a/lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.css b/lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.css index 7020bdc67008..71ecf512f7c0 100644 --- a/lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.css @@ -75,6 +75,16 @@ margin-right: .8em; } +.monaco-action-bar .action-item .action-label.separator { + width: 1px; + height: 16px; + margin: 5px 4px !important; + cursor: default; + min-width: 1px; + padding: 0; + background-color: #bbb; +} + .secondary-actions .monaco-action-bar .action-label { margin-left: 6px; } diff --git a/lib/vscode/src/vs/base/browser/ui/button/button.css b/lib/vscode/src/vs/base/browser/ui/button/button.css index fb4ebbf081df..46a5b0e04819 100644 --- a/lib/vscode/src/vs/base/browser/ui/button/button.css +++ b/lib/vscode/src/vs/base/browser/ui/button/button.css @@ -35,6 +35,7 @@ .monaco-button-dropdown { display: flex; + cursor: pointer; } .monaco-button-dropdown > .monaco-dropdown-button { diff --git a/lib/vscode/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/lib/vscode/src/vs/base/browser/ui/centered/centeredViewLayout.ts index 52cc64b8a369..c774bfaf5eaa 100644 --- a/lib/vscode/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/lib/vscode/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -165,6 +165,7 @@ export class CenteredViewLayout implements IDisposable { this.splitView = undefined; this.emptyViews = undefined; this.container.appendChild(this.view.element); + this.view.layout(this.width, this.height, 0, 0); } } diff --git a/lib/vscode/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/lib/vscode/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 016e9a1e000a..eec2d491b9c1 100644 Binary files a/lib/vscode/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/lib/vscode/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/lib/vscode/src/vs/base/browser/ui/dialog/dialog.ts b/lib/vscode/src/vs/base/browser/ui/dialog/dialog.ts index 2c5d315a82b2..6df00351bfc7 100644 --- a/lib/vscode/src/vs/base/browser/ui/dialog/dialog.ts +++ b/lib/vscode/src/vs/base/browser/ui/dialog/dialog.ts @@ -101,16 +101,17 @@ export class Dialog extends Disposable { this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons')); const messageRowElement = this.element.appendChild($('.dialog-message-row')); - this.iconElement = messageRowElement.appendChild($('.dialog-icon')); + this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon')); + this.iconElement.setAttribute('aria-label', this.getIconAriaLabel()); this.messageContainer = messageRowElement.appendChild($('.dialog-message-container')); if (this.options.detail || this.options.renderBody) { const messageElement = this.messageContainer.appendChild($('.dialog-message')); - const messageTextElement = messageElement.appendChild($('.dialog-message-text')); + const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text')); messageTextElement.innerText = this.message; } - this.messageDetailElement = this.messageContainer.appendChild($('.dialog-message-detail')); + this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail')); if (this.options.detail || !this.options.renderBody) { this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message; } else { @@ -118,8 +119,12 @@ export class Dialog extends Disposable { } if (this.options.renderBody) { - const customBody = this.messageContainer.appendChild($('.dialog-message-body')); + const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body')); this.options.renderBody(customBody); + + for (const el of this.messageContainer.querySelectorAll('a')) { + el.tabIndex = 0; + } } if (this.options.inputs) { @@ -157,7 +162,7 @@ export class Dialog extends Disposable { this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); } - private getAriaLabel(): string { + private getIconAriaLabel(): string { let typeLabel = nls.localize('dialogInfoMessage', 'Info'); switch (this.options.type) { case 'error': @@ -176,7 +181,7 @@ export class Dialog extends Disposable { break; } - return `${typeLabel}: ${this.message} ${this.options.detail || ''}`; + return typeLabel; } updateMessage(message: string): void { @@ -218,6 +223,10 @@ export class Dialog extends Disposable { this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => { const evt = new StandardKeyboardEvent(e); + if (evt.equals(KeyMod.Alt)) { + evt.preventDefault(); + } + if (evt.equals(KeyCode.Enter)) { // Enter in input field should OK the dialog @@ -246,6 +255,17 @@ export class Dialog extends Disposable { // Build a list of focusable elements in their visual order const focusableElements: { focus: () => void }[] = []; let focusedIndex = -1; + + if (this.messageContainer) { + const links = this.messageContainer.querySelectorAll('a'); + for (const link of links) { + focusableElements.push(link); + if (link === document.activeElement) { + focusedIndex = focusableElements.length - 1; + } + } + } + for (const input of this.inputs) { focusableElements.push(input); if (input.hasFocus()) { @@ -371,7 +391,7 @@ export class Dialog extends Disposable { this.applyStyles(); - this.element.setAttribute('aria-label', this.getAriaLabel()); + this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body'); show(this.element); // Focus first element (input or button) diff --git a/lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.css b/lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.css index b363b5578963..aeb837a6da79 100644 --- a/lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.css +++ b/lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.css @@ -37,3 +37,10 @@ line-height: 16px; margin-left: -4px; } + +.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label > .action-label { + display: block; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; +} diff --git a/lib/vscode/src/vs/base/browser/ui/grid/grid.ts b/lib/vscode/src/vs/base/browser/ui/grid/grid.ts index 38bc8c53d9e9..512d2d8a7ec0 100644 --- a/lib/vscode/src/vs/base/browser/ui/grid/grid.ts +++ b/lib/vscode/src/vs/base/browser/ui/grid/grid.ts @@ -210,7 +210,9 @@ export class Grid extends Disposable { get minimumHeight(): number { return this.gridview.minimumHeight; } get maximumWidth(): number { return this.gridview.maximumWidth; } get maximumHeight(): number { return this.gridview.maximumHeight; } - get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; } + + readonly onDidChange: Event<{ width: number; height: number; } | undefined>; + readonly onDidScroll: Event; get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; } set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; } @@ -232,8 +234,8 @@ export class Grid extends Disposable { } else { this.gridview = new GridView(options); } - this._register(this.gridview); + this._register(this.gridview); this._register(this.gridview.onDidSashReset(this.onDidSashReset, this)); const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number' @@ -243,6 +245,9 @@ export class Grid extends Disposable { if (!(view instanceof GridView)) { this._addView(view, size, [0]); } + + this.onDidChange = this.gridview.onDidChange; + this.onDidScroll = this.gridview.onDidScroll; } style(styles: IGridStyles): void { diff --git a/lib/vscode/src/vs/base/browser/ui/grid/gridview.ts b/lib/vscode/src/vs/base/browser/ui/grid/gridview.ts index 323f71fd6336..996b968ddc06 100644 --- a/lib/vscode/src/vs/base/browser/ui/grid/gridview.ts +++ b/lib/vscode/src/vs/base/browser/ui/grid/gridview.ts @@ -9,9 +9,10 @@ import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { SplitView, IView as ISplitView, Sizing, LayoutPriority, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { $ } from 'vs/base/browser/dom'; -import { tail2 as tail } from 'vs/base/common/arrays'; +import { equals as arrayEquals, tail2 as tail } from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { clamp } from 'vs/base/common/numbers'; +import { isUndefined } from 'vs/base/common/types'; export { Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; @@ -242,6 +243,10 @@ class BranchNode implements ISplitView, IDisposable { private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + private _onDidScroll = new Emitter(); + private onDidScrollDisposable: IDisposable = Disposable.None; + readonly onDidScroll: Event = this._onDidScroll.event; + private childrenChangeDisposable: IDisposable = Disposable.None; private readonly _onDidSashReset = new Emitter(); @@ -343,11 +348,7 @@ class BranchNode implements ISplitView, IDisposable { const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]); this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset); - const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined); - this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange); - - const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location]))); - this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset); + this.updateChildrenEvents(); } style(styles: IGridViewStyles): void { @@ -566,6 +567,11 @@ class BranchNode implements ISplitView, IDisposable { } private onDidChildrenChange(): void { + this.updateChildrenEvents(); + this._onDidChange.fire(undefined); + } + + private updateChildrenEvents(): void { const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined); this.childrenChangeDisposable.dispose(); this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange); @@ -574,7 +580,9 @@ class BranchNode implements ISplitView, IDisposable { this.childrenSashResetDisposable.dispose(); this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset); - this._onDidChange.fire(undefined); + const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll)); + this.onDidScrollDisposable.dispose(); + this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll); } trySet2x2(other: BranchNode): IDisposable { @@ -646,6 +654,25 @@ class BranchNode implements ISplitView, IDisposable { } } +/** + * Creates a latched event that avoids being fired when the view + * constraints do not change at all. + */ +function createLatchedOnDidChangeViewEvent(view: IView): Event { + const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split(view.onDidChange, isUndefined); + + return Event.any( + onDidSetViewSize, + Event.map( + Event.latch( + Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])), + arrayEquals + ), + _ => undefined + ) + ); +} + class LeafNode implements ISplitView, IDisposable { private _size: number = 0; @@ -657,6 +684,7 @@ class LeafNode implements ISplitView, IDisposable { private absoluteOffset: number = 0; private absoluteOrthogonalOffset: number = 0; + readonly onDidScroll: Event = Event.None; readonly onDidSashReset: Event = Event.None; private _onDidLinkedWidthNodeChange = new Relay(); @@ -691,7 +719,8 @@ class LeafNode implements ISplitView, IDisposable { this._orthogonalSize = orthogonalSize; this._size = size; - this._onDidViewChange = Event.map(this.view.onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height)); + const onDidChange = createLatchedOnDidChangeViewEvent(view); + this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height)); this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event); } @@ -778,7 +807,25 @@ class LeafNode implements ISplitView, IDisposable { this._orthogonalSize = ctx.orthogonalSize; this.absoluteOffset = ctx.absoluteOffset + offset; this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset; - this.view.layout(this.width, this.height, this.top, this.left); + + this._layout(this.width, this.height, this.top, this.left); + } + + private cachedWidth: number = 0; + private cachedHeight: number = 0; + private cachedTop: number = 0; + private cachedLeft: number = 0; + + private _layout(width: number, height: number, top: number, left: number): void { + if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) { + return; + } + + this.cachedWidth = width; + this.cachedHeight = height; + this.cachedTop = top; + this.cachedLeft = left; + this.view.layout(width, height, top, left); } setVisible(visible: boolean): void { @@ -852,6 +899,7 @@ export class GridView implements IDisposable { this.element.appendChild(root.element); this.onDidSashResetRelay.input = root.onDidSashReset; this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO + this._onDidScroll.input = root.onDidScroll; } get orientation(): Orientation { @@ -877,6 +925,9 @@ export class GridView implements IDisposable { get maximumWidth(): number { return this.root.maximumHeight; } get maximumHeight(): number { return this.root.maximumHeight; } + private _onDidScroll = new Relay(); + readonly onDidScroll = this._onDidScroll.event; + private _onDidChange = new Relay(); readonly onDidChange = this._onDidChange.event; diff --git a/lib/vscode/src/vs/base/browser/ui/hover/hover.css b/lib/vscode/src/vs/base/browser/ui/hover/hover.css index f22533eebad4..8296d64a6971 100644 --- a/lib/vscode/src/vs/base/browser/ui/hover/hover.css +++ b/lib/vscode/src/vs/base/browser/ui/hover/hover.css @@ -138,3 +138,8 @@ margin-bottom: 4px; display: inline-block; } + +.monaco-hover-content .action-container a { + -webkit-user-select: none; + user-select: none; +} diff --git a/lib/vscode/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts b/lib/vscode/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts index 33e7c72fd65d..83c61d9bd159 100644 --- a/lib/vscode/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts +++ b/lib/vscode/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts @@ -16,9 +16,11 @@ export interface IHoverDelegateOptions { text: IMarkdownString | string; target: IHoverDelegateTarget | HTMLElement; hoverPosition?: HoverPosition; + showPointer?: boolean; } export interface IHoverDelegate { showHover(options: IHoverDelegateOptions): IDisposable | undefined; delay: number; + placement?: 'mouse' | 'element'; } diff --git a/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabel.ts index ba6727884bf5..d4103d7c8b20 100644 --- a/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/base/common/range'; import { equals } from 'vs/base/common/objects'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { isFunction, isString } from 'vs/base/common/types'; -import { domEvent } from 'vs/base/browser/event'; -import { localize } from 'vs/nls'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; @@ -91,17 +88,17 @@ class FastLabelNode { export class IconLabel extends Disposable { - private domNode: FastLabelNode; + private readonly domNode: FastLabelNode; - private nameNode: Label | LabelWithHighlights; + private readonly nameNode: Label | LabelWithHighlights; - private descriptionContainer: FastLabelNode; + private readonly descriptionContainer: FastLabelNode; private descriptionNode: FastLabelNode | HighlightedLabel | undefined; - private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; + private readonly descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; - private labelContainer: HTMLElement; + private readonly labelContainer: HTMLElement; - private hoverDelegate: IHoverDelegate | undefined = undefined; + private readonly hoverDelegate: IHoverDelegate | undefined; private readonly customHovers: Map = new Map(); constructor(container: HTMLElement, options?: IIconLabelCreationOptions) { @@ -114,7 +111,7 @@ export class IconLabel extends Disposable { const nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container')); this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container')))); - if (options?.supportHighlights) { + if (options?.supportHighlights || options?.supportIcons) { this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons); } else { this.nameNode = new Label(nameContainer); @@ -126,9 +123,7 @@ export class IconLabel extends Disposable { this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); } - if (options?.hoverDelegate) { - this.hoverDelegate = options.hoverDelegate; - } + this.hoverDelegate = options?.hoverDelegate; } get element(): HTMLElement { @@ -185,116 +180,21 @@ export class IconLabel extends Disposable { } if (!this.hoverDelegate) { - return this.setupNativeHover(htmlElement, tooltip); - } else { - return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip); - } - } - - private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { - if (hoverOptions && isHovering) { - if (mouseX !== undefined) { - (hoverOptions.target).x = mouseX + 10; - } - return hoverDelegate.showHover(hoverOptions); - } - return undefined; - } - - private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { - if (isString(markdownTooltip)) { - return async () => markdownTooltip; - } else if (isFunction(markdownTooltip.markdown)) { - return markdownTooltip.markdown; + setupNativeHover(htmlElement, tooltip); } else { - const markdown = markdownTooltip.markdown; - return async () => markdown; - } - } - - private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void { - htmlElement.setAttribute('title', ''); - htmlElement.removeAttribute('title'); - let tooltip = this.getTooltipForCustom(markdownTooltip); - - let hoverOptions: IHoverDelegateOptions | undefined; - let mouseX: number | undefined; - let isHovering = false; - let tokenSource: CancellationTokenSource; - let hoverDisposable: IDisposable | undefined; - function mouseOver(this: HTMLElement, e: MouseEvent): void { - if (isHovering) { - return; - } - tokenSource = new CancellationTokenSource(); - function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void { - const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; - if (isMouseDown) { - hoverDisposable?.dispose(); - hoverDisposable = undefined; - } - if (isMouseDown || (e).fromElement === htmlElement) { - isHovering = false; - hoverOptions = undefined; - tokenSource.dispose(true); - mouseLeaveDisposable.dispose(); - mouseDownDisposable.dispose(); - } - } - const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); - const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); - isHovering = true; - - function mouseMove(this: HTMLElement, e: MouseEvent): void { - mouseX = e.x; + const hoverDisposable = setupCustomHover(this.hoverDelegate, htmlElement, tooltip); + if (hoverDisposable) { + this.customHovers.set(htmlElement, hoverDisposable); } - const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); - setTimeout(async () => { - if (isHovering && tooltip) { - // Re-use the already computed hover options if they exist. - if (!hoverOptions) { - const target: IHoverDelegateTarget = { - targetElements: [this], - dispose: () => { } - }; - hoverOptions = { - text: localize('iconLabel.loading', "Loading..."), - target, - hoverPosition: HoverPosition.BELOW - }; - hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - - const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); - if (resolvedTooltip) { - hoverOptions = { - text: resolvedTooltip, - target, - hoverPosition: HoverPosition.BELOW - }; - // awaiting the tooltip could take a while. Make sure we're still hovering. - hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); - } else if (hoverDisposable) { - hoverDisposable.dispose(); - hoverDisposable = undefined; - } - } - - } - mouseMoveDisposable.dispose(); - }, hoverDelegate.delay); } - const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); - this.customHovers.set(htmlElement, mouseOverDisposable); } - private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { - let stringTooltip: string = ''; - if (isString(tooltip)) { - stringTooltip = tooltip; - } else if (tooltip?.markdownNotSupportedFallback) { - stringTooltip = tooltip.markdownNotSupportedFallback; + public override dispose() { + super.dispose(); + for (const disposable of this.customHovers.values()) { + disposable.dispose(); } - htmlElement.title = stringTooltip; + this.customHovers.clear(); } } diff --git a/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts new file mode 100644 index 000000000000..a80a53e02bb5 --- /dev/null +++ b/lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isFunction, isString } from 'vs/base/common/types'; +import * as dom from 'vs/base/browser/dom'; +import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { DomEmitter } from 'vs/base/browser/event'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { localize } from 'vs/nls'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; + + +export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void { + if (isString(tooltip)) { + htmlElement.title = tooltip; + } else if (tooltip?.markdownNotSupportedFallback) { + htmlElement.title = tooltip.markdownNotSupportedFallback; + } else { + htmlElement.removeAttribute('title'); + } +} + +export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined { + if (!markdownTooltip) { + return undefined; + } + + const tooltip = getTooltipForCustom(markdownTooltip); + + let hoverOptions: IHoverDelegateOptions | undefined; + let mouseX: number | undefined; + let isHovering = false; + let tokenSource: CancellationTokenSource; + let hoverDisposable: IDisposable | undefined; + + const mouseOverDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_OVER, true); + mouseOverDomEmitter.event((e: MouseEvent) => { + if (isHovering) { + return; + } + tokenSource = new CancellationTokenSource(); + function mouseLeaveOrDown(e: MouseEvent): void { + const isMouseDown = e.type === dom.EventType.MOUSE_DOWN; + if (isMouseDown) { + hoverDisposable?.dispose(); + hoverDisposable = undefined; + } + if (isMouseDown || (e).fromElement === htmlElement) { + isHovering = false; + hoverOptions = undefined; + tokenSource.dispose(true); + mouseLeaveDomEmitter.dispose(); + mouseDownDomEmitter.dispose(); + } + } + const mouseLeaveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_LEAVE, true); + mouseLeaveDomEmitter.event(mouseLeaveOrDown); + const mouseDownDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_DOWN, true); + mouseDownDomEmitter.event(mouseLeaveOrDown); + isHovering = true; + + function mouseMove(e: MouseEvent): void { + mouseX = e.x; + } + const mouseMoveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_MOVE, true); + mouseMoveDomEmitter.event(mouseMove); + setTimeout(async () => { + if (isHovering && tooltip) { + // Re-use the already computed hover options if they exist. + if (!hoverOptions) { + const target: IHoverDelegateTarget = { + targetElements: [htmlElement], + dispose: () => { } + }; + hoverOptions = { + text: localize('iconLabel.loading', "Loading..."), + target, + hoverPosition: HoverPosition.BELOW + }; + hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + + const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); + if (resolvedTooltip) { + hoverOptions = { + text: resolvedTooltip, + target, + showPointer: hoverDelegate.placement === 'element', + hoverPosition: HoverPosition.BELOW + }; + // awaiting the tooltip could take a while. Make sure we're still hovering. + hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + } else if (hoverDisposable) { + hoverDisposable.dispose(); + hoverDisposable = undefined; + } + } + + } + mouseMoveDomEmitter.dispose(); + }, hoverDelegate.delay); + }); + return mouseOverDomEmitter; +} + + +function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { + if (isString(markdownTooltip)) { + return async () => markdownTooltip; + } else if (isFunction(markdownTooltip.markdown)) { + return markdownTooltip.markdown; + } else { + const markdown = markdownTooltip.markdown; + return async () => markdown; + } +} + +function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { + if (hoverOptions && isHovering) { + if (mouseX !== undefined && (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse')) { + (hoverOptions.target).x = mouseX + 10; + } + return hoverDelegate.showHover(hoverOptions); + } + return undefined; +} diff --git a/lib/vscode/src/vs/base/browser/ui/list/listView.ts b/lib/vscode/src/vs/base/browser/ui/list/listView.ts index 6a2e9867dde2..580812f11975 100644 --- a/lib/vscode/src/vs/base/browser/ui/list/listView.ts +++ b/lib/vscode/src/vs/base/browser/ui/list/listView.ts @@ -898,7 +898,7 @@ export class ListView implements ISpliceable, IDisposable { @memoize get onMouseOver(): Event> { return Event.map(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); } @memoize get onMouseMove(): Event> { return Event.map(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); } @memoize get onMouseOut(): Event> { return Event.map(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); } - @memoize get onContextMenu(): Event> { return Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); } + @memoize get onContextMenu(): Event | IListGestureEvent> { return Event.any(Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)), Event.map(domEvent(this.domNode, TouchEventType.Contextmenu) as Event, e => this.toGestureEvent(e))); } @memoize get onTouchStart(): Event> { return Event.map(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)); } @memoize get onTap(): Event> { return Event.map(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e as GestureEvent)); } diff --git a/lib/vscode/src/vs/base/browser/ui/list/listWidget.ts b/lib/vscode/src/vs/base/browser/ui/list/listWidget.ts index e66c6bbcc974..93906052348d 100644 --- a/lib/vscode/src/vs/base/browser/ui/list/listWidget.ts +++ b/lib/vscode/src/vs/base/browser/ui/list/listWidget.ts @@ -660,9 +660,15 @@ export class MouseController implements IDisposable { private changeSelection(e: IListMouseEvent | IListTouchEvent): void { const focus = e.index!; - const anchor = this.list.getAnchor(); + let anchor = this.list.getAnchor(); + + if (this.isSelectionRangeChangeEvent(e)) { + if (typeof anchor === 'undefined') { + const currentFocus = this.list.getFocus()[0]; + anchor = currentFocus ?? focus; + this.list.setAnchor(anchor); + } - if (this.isSelectionRangeChangeEvent(e) && typeof anchor === 'number') { const min = Math.min(anchor, focus); const max = Math.max(anchor, focus); const rangeSelection = range(min, max + 1); @@ -1201,7 +1207,7 @@ export class List implements ISpliceable, IThemable, IDisposable { const fromMouse = Event.chain(this.view.onContextMenu) .filter(_ => !didJustPressContextMenuKey) - .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent })) + .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent })) .event; return Event.any>(fromKeyDown, fromKeyUp, fromMouse); diff --git a/lib/vscode/src/vs/base/browser/ui/sash/sash.ts b/lib/vscode/src/vs/base/browser/ui/sash/sash.ts index c08459895d6e..85f2ed8c4d09 100644 --- a/lib/vscode/src/vs/base/browser/ui/sash/sash.ts +++ b/lib/vscode/src/vs/base/browser/ui/sash/sash.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./sash'; -import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; -import * as types from 'vs/base/common/types'; -import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; import { Event, Emitter } from 'vs/base/common/event'; -import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $ } from 'vs/base/browser/dom'; -import { domEvent } from 'vs/base/browser/event'; +import { getElementsByTagName, EventHelper, createStyleSheet, append, $, EventLike } from 'vs/base/browser/dom'; +import { DomEmitter } from 'vs/base/browser/event'; import { Delayer } from 'vs/base/common/async'; +import { memoize } from 'vs/base/common/decorators'; let DEBUG = false; // DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this @@ -88,6 +87,78 @@ export function setGlobalHoverDelay(size: number): void { onDidChangeHoverDelay.fire(size); } +interface PointerEvent extends EventLike { + readonly pageX: number; + readonly pageY: number; + readonly altKey: boolean; + readonly target: EventTarget | null; +} + +interface IPointerEventFactory { + readonly onPointerMove: Event; + readonly onPointerUp: Event; + dispose(): void; +} + +class MouseEventFactory implements IPointerEventFactory { + + private disposables = new DisposableStore(); + + @memoize + get onPointerMove(): Event { + return this.disposables.add(new DomEmitter(window, 'mousemove')).event; + } + + @memoize + get onPointerUp(): Event { + return this.disposables.add(new DomEmitter(window, 'mouseup')).event; + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class GestureEventFactory implements IPointerEventFactory { + + private disposables = new DisposableStore(); + + @memoize + get onPointerMove(): Event { + return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event; + } + + @memoize + get onPointerUp(): Event { + return this.disposables.add(new DomEmitter(this.el, EventType.End)).event; + } + + constructor(private el: HTMLElement) { } + + dispose(): void { + this.disposables.dispose(); + } +} + +class OrthogonalPointerEventFactory implements IPointerEventFactory { + + @memoize + get onPointerMove(): Event { + return this.factory.onPointerMove; + } + + @memoize + get onPointerUp(): Event { + return this.factory.onPointerUp; + } + + constructor(private factory: IPointerEventFactory) { } + + dispose(): void { + // noop + } +} + export class Sash extends Disposable { private el: HTMLElement; @@ -146,9 +217,9 @@ export class Sash extends Disposable { if (state !== SashState.Disabled) { this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start')); this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove())); - domEvent(this._orthogonalStartDragHandle, 'mouseenter') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseenter')).event (() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables); - domEvent(this._orthogonalStartDragHandle, 'mouseleave') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseleave')).event (() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables); } }; @@ -176,9 +247,9 @@ export class Sash extends Disposable { if (state !== SashState.Disabled) { this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end')); this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove())); - domEvent(this._orthogonalEndDragHandle, 'mouseenter') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseenter')).event (() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables); - domEvent(this._orthogonalEndDragHandle, 'mouseleave') + this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseleave')).event (() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables); } }; @@ -205,13 +276,28 @@ export class Sash extends Disposable { this.el.classList.add('mac'); } - this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this)); - this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this)); - this._register(domEvent(this.el, 'mouseenter')(() => Sash.onMouseEnter(this))); - this._register(domEvent(this.el, 'mouseleave')(() => Sash.onMouseLeave(this))); + const onMouseDown = this._register(new DomEmitter(this.el, 'mousedown')).event; + this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this)); + const onMouseDoubleClick = this._register(new DomEmitter(this.el, 'dblclick')).event; + this._register(onMouseDoubleClick(this.onPointerDoublePress, this)); + const onMouseEnter = this._register(new DomEmitter(this.el, 'mouseenter')).event; + this._register(onMouseEnter(() => Sash.onMouseEnter(this))); + const onMouseLeave = this._register(new DomEmitter(this.el, 'mouseleave')).event; + this._register(onMouseLeave(() => Sash.onMouseLeave(this))); this._register(Gesture.addTarget(this.el)); - this._register(domEvent(this.el, EventType.Start)(e => this.onTouchStart(e as GestureEvent), this)); + + const onTouchStart = Event.map(this._register(new DomEmitter(this.el, EventType.Start)).event, e => ({ ...e, target: e.initialTarget ?? null })); + this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this.el)), this)); + const onTap = this._register(new DomEmitter(this.el, EventType.Tap)).event; + const onDoubleTap = Event.map( + Event.filter( + Event.debounce(onTap, (res, event) => ({ event, count: (res?.count ?? 0) + 1 }), 250), + ({ count }) => count === 2 + ), + ({ event }) => ({ ...event, target: event.initialTarget ?? null }) + ); + this._register(onDoubleTap(this.onPointerDoublePress, this)); if (typeof options.size === 'number') { this.size = options.size; @@ -252,24 +338,24 @@ export class Sash extends Disposable { this.layout(); } - private onMouseDown(e: MouseEvent): void { - EventHelper.stop(e, false); + private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void { + EventHelper.stop(event); let isMultisashResize = false; - if (!(e as any).__orthogonalSashEvent) { - const orthogonalSash = this.getOrthogonalSash(e); + if (!(event as any).__orthogonalSashEvent) { + const orthogonalSash = this.getOrthogonalSash(event); if (orthogonalSash) { isMultisashResize = true; - (e as any).__orthogonalSashEvent = true; - orthogonalSash.onMouseDown(e); + (event as any).__orthogonalSashEvent = true; + orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory)); } } - if (this.linkedSash && !(e as any).__linkedSashEvent) { - (e as any).__linkedSashEvent = true; - this.linkedSash.onMouseDown(e); + if (this.linkedSash && !(event as any).__linkedSashEvent) { + (event as any).__linkedSashEvent = true; + this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory)); } if (!this.state) { @@ -287,10 +373,9 @@ export class Sash extends Disposable { iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash } - const mouseDownEvent = new StandardMouseEvent(e); - const startX = mouseDownEvent.posx; - const startY = mouseDownEvent.posy; - const altKey = mouseDownEvent.altKey; + const startX = event.pageX; + const startY = event.pageY; + const altKey = event.altKey; const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey }; this.el.classList.add('active'); @@ -332,15 +417,14 @@ export class Sash extends Disposable { this.onDidEnablementChange(updateStyle, null, disposables); } - const onMouseMove = (e: MouseEvent) => { + const onPointerMove = (e: PointerEvent) => { EventHelper.stop(e, false); - const mouseMoveEvent = new StandardMouseEvent(e); - const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey }; + const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey }; this._onDidChange.fire(event); }; - const onMouseUp = (e: MouseEvent) => { + const onPointerUp = (e: PointerEvent) => { EventHelper.stop(e, false); this.el.removeChild(style); @@ -355,11 +439,12 @@ export class Sash extends Disposable { } }; - domEvent(window, 'mousemove')(onMouseMove, null, disposables); - domEvent(window, 'mouseup')(onMouseUp, null, disposables); + pointerEventFactory.onPointerMove(onPointerMove, null, disposables); + pointerEventFactory.onPointerUp(onPointerUp, null, disposables); + disposables.add(pointerEventFactory); } - private onMouseDoubleClick(e: MouseEvent): void { + private onPointerDoublePress(e: MouseEvent): void { const orthogonalSash = this.getOrthogonalSash(e); if (orthogonalSash) { @@ -373,41 +458,6 @@ export class Sash extends Disposable { this._onDidReset.fire(); } - private onTouchStart(event: GestureEvent): void { - EventHelper.stop(event); - - const listeners: IDisposable[] = []; - - const startX = event.pageX; - const startY = event.pageY; - const altKey = event.altKey; - - this._onDidStart.fire({ - startX: startX, - currentX: startX, - startY: startY, - currentY: startY, - altKey - }); - - listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => { - if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) { - this._onDidChange.fire({ - startX: startX, - currentX: event.pageX, - startY: startY, - currentY: event.pageY, - altKey - }); - } - })); - - listeners.push(addDisposableListener(this.el, EventType.End, () => { - this._onDidEnd.fire(); - dispose(listeners); - })); - } - private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void { if (sash.el.classList.contains('active')) { sash.hoverDelayer.cancel(); @@ -476,7 +526,7 @@ export class Sash extends Disposable { return this.hidden; } - private getOrthogonalSash(e: MouseEvent): Sash | undefined { + private getOrthogonalSash(e: PointerEvent): Sash | undefined { if (!e.target || !(e.target instanceof HTMLElement)) { return undefined; } diff --git a/lib/vscode/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/lib/vscode/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 9f60bfb43ef3..a2447f6db793 100644 --- a/lib/vscode/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/lib/vscode/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -329,7 +329,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } if (this.styles.listFocusForeground) { - content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`); + content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`); } if (this.styles.decoratorRightForeground) { diff --git a/lib/vscode/src/vs/base/browser/ui/splitview/paneview.ts b/lib/vscode/src/vs/base/browser/ui/splitview/paneview.ts index 06e8b064d49a..a84c6f5837e4 100644 --- a/lib/vscode/src/vs/base/browser/ui/splitview/paneview.ts +++ b/lib/vscode/src/vs/base/browser/ui/splitview/paneview.ts @@ -16,6 +16,7 @@ import { isFirefox } from 'vs/base/browser/browser'; import { DataTransfers } from 'vs/base/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { localize } from 'vs/nls'; +import { ScrollEvent } from 'vs/base/common/scrollable'; export interface IPaneOptions { minimumBodySize?: number; @@ -444,6 +445,7 @@ export class PaneView extends Disposable { orientation: Orientation; readonly onDidSashChange: Event; + readonly onDidScroll: Event; constructor(container: HTMLElement, options: IPaneViewOptions = {}) { super(); @@ -453,6 +455,7 @@ export class PaneView extends Disposable { this.element = append(container, $('.monaco-pane-view')); this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation })); this.onDidSashChange = this.splitview.onDidSashChange; + this.onDidScroll = this.splitview.onDidScroll; } addPane(pane: Pane, size: number, index = this.splitview.length): void { diff --git a/lib/vscode/src/vs/base/browser/ui/splitview/splitview.ts b/lib/vscode/src/vs/base/browser/ui/splitview/splitview.ts index 8434bc1c528c..5996365a70f0 100644 --- a/lib/vscode/src/vs/base/browser/ui/splitview/splitview.ts +++ b/lib/vscode/src/vs/base/browser/ui/splitview/splitview.ts @@ -14,7 +14,7 @@ import { Color } from 'vs/base/common/color'; import { domEvent } from 'vs/base/browser/event'; import { $, append, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; export interface ISplitViewStyles { @@ -237,6 +237,8 @@ export class SplitView extends Disposable { private _onDidSashReset = this._register(new Emitter()); readonly onDidSashReset = this._onDidSashReset.event; + readonly onDidScroll: Event; + get length(): number { return this.viewItems.length; } @@ -317,7 +319,8 @@ export class SplitView extends Disposable { horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden }, this.scrollable)); - this._register(this.scrollableElement.onScroll(e => { + this.onDidScroll = this.scrollableElement.onScroll; + this._register(this.onDidScroll(e => { this.viewContainer.scrollTop = e.scrollTop; this.viewContainer.scrollLeft = e.scrollLeft; })); diff --git a/lib/vscode/src/vs/base/common/actions.ts b/lib/vscode/src/vs/base/common/actions.ts index 171853e2fd6f..9125a2c58371 100644 --- a/lib/vscode/src/vs/base/common/actions.ts +++ b/lib/vscode/src/vs/base/common/actions.ts @@ -30,14 +30,14 @@ export interface IAction extends IDisposable { class: string | undefined; enabled: boolean; checked: boolean; - run(event?: unknown): Promise; + run(event?: unknown): unknown; } export interface IActionRunner extends IDisposable { readonly onDidRun: Event; readonly onBeforeRun: Event; - run(action: IAction, context?: unknown): Promise; + run(action: IAction, context?: unknown): unknown; } export interface IActionChangeEvent { @@ -59,9 +59,9 @@ export class Action extends Disposable implements IAction { protected _cssClass: string | undefined; protected _enabled: boolean = true; protected _checked: boolean = false; - protected readonly _actionCallback?: (event?: unknown) => Promise; + protected readonly _actionCallback?: (event?: unknown) => unknown; - constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => Promise) { + constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => unknown) { super(); this._id = id; this._label = label; diff --git a/lib/vscode/src/vs/base/common/arrays.ts b/lib/vscode/src/vs/base/common/arrays.ts index fceca7212f64..8d72d50d9ffe 100644 --- a/lib/vscode/src/vs/base/common/arrays.ts +++ b/lib/vscode/src/vs/base/common/arrays.ts @@ -554,3 +554,37 @@ export function mapFind(array: Iterable, mapFn: (value: T) => R | undef return undefined; } + +/** + * Like Math.min with a delegate, and returns the winning index + */ +export function minIndex(array: readonly T[], fn: (value: T) => number): number { + let minValue = Number.MAX_SAFE_INTEGER; + let minIdx = 0; + array.forEach((value, i) => { + const thisValue = fn(value); + if (thisValue < minValue) { + minValue = thisValue; + minIdx = i; + } + }); + + return minIdx; +} + +/** + * Like Math.max with a delegate, and returns the winning index + */ +export function maxIndex(array: readonly T[], fn: (value: T) => number): number { + let minValue = Number.MIN_SAFE_INTEGER; + let maxIdx = 0; + array.forEach((value, i) => { + const thisValue = fn(value); + if (thisValue > minValue) { + minValue = thisValue; + maxIdx = i; + } + }); + + return maxIdx; +} diff --git a/lib/vscode/src/vs/base/common/async.ts b/lib/vscode/src/vs/base/common/async.ts index 7090d5add78d..fba360da12cd 100644 --- a/lib/vscode/src/vs/base/common/async.ts +++ b/lib/vscode/src/vs/base/common/async.ts @@ -536,7 +536,6 @@ export class Limiter { get size(): number { return this._size; - // return this.runningPromises + this.outstandingPromises.length; } queue(factory: ITask>): Promise { diff --git a/lib/vscode/src/vs/base/common/codicons.ts b/lib/vscode/src/vs/base/common/codicons.ts index fc38ba8ee14c..da86ec14cce7 100644 --- a/lib/vscode/src/vs/base/common/codicons.ts +++ b/lib/vscode/src/vs/base/common/codicons.ts @@ -573,6 +573,7 @@ export namespace Codicon { export const filterFilled = new Codicon('filter-filled', { fontCharacter: '\\ebce' }); export const wand = new Codicon('wand', { fontCharacter: '\\ebcf' }); export const debugLineByLine = new Codicon('debug-line-by-line', { fontCharacter: '\\ebd0' }); + export const inspect = new Codicon('inspect', { fontCharacter: '\\ebd1' }); export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition); } diff --git a/lib/vscode/src/vs/base/common/collections.ts b/lib/vscode/src/vs/base/common/collections.ts index cc037ecfd717..24ed68bae5f9 100644 --- a/lib/vscode/src/vs/base/common/collections.ts +++ b/lib/vscode/src/vs/base/common/collections.ts @@ -53,8 +53,8 @@ export function forEach(from: IStringDictionary | INumberDictionary, ca * Groups the collection into a dictionary based on the provided * group function. */ -export function groupBy(data: T[], groupFn: (element: T) => string): IStringDictionary { - const result: IStringDictionary = Object.create(null); +export function groupBy(data: V[], groupFn: (element: V) => K): Record { + const result: Record = Object.create(null); for (const element of data) { const key = groupFn(element); let target = result[key]; @@ -66,24 +66,6 @@ export function groupBy(data: T[], groupFn: (element: T) => string): IStringD return result; } -/** - * Groups the collection into a dictionary based on the provided - * group function. - */ -export function groupByNumber(data: T[], groupFn: (element: T) => number): Map { - const result = new Map(); - for (const element of data) { - const key = groupFn(element); - let target = result.get(key); - if (!target) { - target = []; - result.set(key, target); - } - target.push(element); - } - return result; -} - export function fromMap(original: Map): IStringDictionary { const result: IStringDictionary = Object.create(null); if (original) { diff --git a/lib/vscode/src/vs/base/common/color.ts b/lib/vscode/src/vs/base/common/color.ts index eb5fd3b079a9..abfbad67c9e1 100644 --- a/lib/vscode/src/vs/base/common/color.ts +++ b/lib/vscode/src/vs/base/common/color.ts @@ -523,7 +523,7 @@ export namespace Color { /** * The default format will use HEX if opaque and RGBA otherwise. */ - export function format(color: Color): string | null { + export function format(color: Color): string { if (color.isOpaque()) { return Color.Format.CSS.formatHex(color); } diff --git a/lib/vscode/src/vs/base/common/comparers.ts b/lib/vscode/src/vs/base/common/comparers.ts index 7b45ce5abb6d..fe3c534467d4 100644 --- a/lib/vscode/src/vs/base/common/comparers.ts +++ b/lib/vscode/src/vs/base/common/comparers.ts @@ -6,11 +6,11 @@ import { sep } from 'vs/base/common/path'; import { IdleValue } from 'vs/base/common/async'; -// When comparing large numbers of strings, such as in sorting large arrays, is better for -// performance to create an Intl.Collator object and use the function provided by its compare -// property than it is to use String.prototype.localeCompare() +// When comparing large numbers of strings it's better for performance to create an +// Intl.Collator object and use the function provided by its compare property +// than it is to use String.prototype.localeCompare() -// A collator with numeric sorting enabled, and no sensitivity to case or to accents +// A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics. const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); return { @@ -28,19 +28,20 @@ const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new }); // A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case. -const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { +const intlFileNameCollatorNumericCaseInsensitive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' }); return { collator: collator }; -});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */ +}); + +/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */ export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { const a = one || ''; const b = other || ''; const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b); - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. + // Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate. if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) { return a < b ? -1 : 1; } @@ -48,16 +49,45 @@ export function compareFileNames(one: string | null, other: string | null, caseS return result; } -/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */ +/** Compares full filenames without grouping by case. */ export function compareFileNamesDefault(one: string | null, other: string | null): number { const collatorNumeric = intlFileNameCollatorNumeric.value.collator; one = one || ''; other = other || ''; - // Compare the entire filename - both name and extension - and disambiguate by length if needed return compareAndDisambiguateByLength(collatorNumeric, one, other); } +/** Compares full filenames grouping uppercase names before lowercase. */ +export function compareFileNamesUpper(one: string | null, other: string | null) { + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + one = one || ''; + other = other || ''; + + return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares full filenames grouping lowercase names before uppercase. */ +export function compareFileNamesLower(one: string | null, other: string | null) { + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + one = one || ''; + other = other || ''; + + return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares full filenames by unicode value. */ +export function compareFileNamesUnicode(one: string | null, other: string | null) { + one = one || ''; + other = other || ''; + + if (one === other) { + return 0; + } + + return one < other ? -1 : 1; +} + export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); @@ -78,6 +108,7 @@ export function noIntlCompareFileNames(one: string | null, other: string | null, return oneExtension < otherExtension ? -1 : 1; } +/** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */ export function compareFileExtensions(one: string | null, other: string | null): number { const [oneName, oneExtension] = extractNameAndExtension(one); const [otherName, otherExtension] = extractNameAndExtension(other); @@ -85,8 +116,7 @@ export function compareFileExtensions(one: string | null, other: string | null): let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension); if (result === 0) { - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. + // Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate. if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) { return oneExtension < otherExtension ? -1 : 1; } @@ -102,24 +132,65 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -/** Compares filenames by extenson, then by full filename */ +/** Compares filenames by extenson, then by full filename. Mixes uppercase and lowercase names together. */ export function compareFileExtensionsDefault(one: string | null, other: string | null): number { one = one || ''; other = other || ''; const oneExtension = extractExtension(one); const otherExtension = extractExtension(other); const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator; - // Check for extension differences, ignoring differences in case and comparing numbers numerically. - result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); - if (result !== 0) { - return result; + return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) || + compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */ +export function compareFileExtensionsUpper(one: string | null, other: string | null): number { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one); + const otherExtension = extractExtension(other); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator; + + return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) || + compareCaseUpperFirst(one, other) || + compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */ +export function compareFileExtensionsLower(one: string | null, other: string | null): number { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one); + const otherExtension = extractExtension(other); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator; + + return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) || + compareCaseLowerFirst(one, other) || + compareAndDisambiguateByLength(collatorNumeric, one, other); +} + +/** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */ +export function compareFileExtensionsUnicode(one: string | null, other: string | null) { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one).toLowerCase(); + const otherExtension = extractExtension(other).toLowerCase(); + + // Check for extension differences + if (oneExtension !== otherExtension) { + return oneExtension < otherExtension ? -1 : 1; } - // Compare full filenames - return compareAndDisambiguateByLength(collatorNumeric, one, other); + // Check for full filename differences. + if (one !== other) { + return one < other ? -1 : 1; + } + + return 0; } const FileNameMatch = /^(.*?)(\.([^.]*))?$/; @@ -130,7 +201,7 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; - // if the dotfilesAsNames option is selected, treat an empty filename with an extension, + // if the dotfilesAsNames option is selected, treat an empty filename with an extension // or a filename that starts with a dot, as a dotfile name if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) { result = [result[0] + '.' + result[1], '']; @@ -162,6 +233,54 @@ function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, ot return 0; } +/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */ +function startsWithLower(string: string) { + const character = string.charAt(0); + + return (character.toLocaleUpperCase() !== character) ? true : false; +} + +/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */ +function startsWithUpper(string: string) { + const character = string.charAt(0); + + return (character.toLocaleLowerCase() !== character) ? true : false; +} + +/** + * Compares the case of the provided strings - lowercase before uppercase + * + * @returns + * ```text + * -1 if one is lowercase and other is uppercase + * 1 if one is uppercase and other is lowercase + * 0 otherwise + * ``` + */ +function compareCaseLowerFirst(one: string, other: string): number { + if (startsWithLower(one) && startsWithUpper(other)) { + return -1; + } + return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0; +} + +/** + * Compares the case of the provided strings - uppercase before lowercase + * + * @returns + * ```text + * -1 if one is uppercase and other is lowercase + * 1 if one is lowercase and other is uppercase + * 0 otherwise + * ``` + */ +function compareCaseUpperFirst(one: string, other: string): number { + if (startsWithUpper(one) && startsWithLower(other)) { + return -1; + } + return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0; +} + function comparePathComponents(one: string, other: string, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); diff --git a/lib/vscode/src/vs/base/common/decorators.ts b/lib/vscode/src/vs/base/common/decorators.ts index f9e39b6b941d..5eb4fa7ad833 100644 --- a/lib/vscode/src/vs/base/common/decorators.ts +++ b/lib/vscode/src/vs/base/common/decorators.ts @@ -24,64 +24,39 @@ export function createDecorator(mapFn: (fn: Function, key: string) => Function): }; } -let memoizeId = 0; -export function createMemoizer() { - const memoizeKeyPrefix = `$memoize${memoizeId++}`; - let self: any = undefined; +export function memoize(_target: any, key: string, descriptor: any) { + let fnKey: string | null = null; + let fn: Function | null = null; - const result = function memoize(target: any, key: string, descriptor: any) { - let fnKey: string | null = null; - let fn: Function | null = null; - - if (typeof descriptor.value === 'function') { - fnKey = 'value'; - fn = descriptor.value; + if (typeof descriptor.value === 'function') { + fnKey = 'value'; + fn = descriptor.value; - if (fn!.length !== 0) { - console.warn('Memoize should only be used in functions with zero parameters'); - } - } else if (typeof descriptor.get === 'function') { - fnKey = 'get'; - fn = descriptor.get; + if (fn!.length !== 0) { + console.warn('Memoize should only be used in functions with zero parameters'); } - - if (!fn) { - throw new Error('not supported'); + } else if (typeof descriptor.get === 'function') { + fnKey = 'get'; + fn = descriptor.get; + } + + if (!fn) { + throw new Error('not supported'); + } + + const memoizeKey = `$memoize$${key}`; + descriptor[fnKey!] = function (...args: any[]) { + if (!this.hasOwnProperty(memoizeKey)) { + Object.defineProperty(this, memoizeKey, { + configurable: false, + enumerable: false, + writable: false, + value: fn!.apply(this, args) + }); } - const memoizeKey = `${memoizeKeyPrefix}:${key}`; - descriptor[fnKey!] = function (...args: any[]) { - self = this; - - if (!this.hasOwnProperty(memoizeKey)) { - Object.defineProperty(this, memoizeKey, { - configurable: true, - enumerable: false, - writable: true, - value: fn!.apply(this, args) - }); - } - - return this[memoizeKey]; - }; + return this[memoizeKey]; }; - - result.clear = () => { - if (typeof self === 'undefined') { - return; - } - Object.getOwnPropertyNames(self).forEach(property => { - if (property.indexOf(memoizeKeyPrefix) === 0) { - delete self[property]; - } - }); - }; - - return result; -} - -export function memoize(target: any, key: string, descriptor: any) { - return createMemoizer()(target, key, descriptor); } export interface IDebounceReducer { diff --git a/lib/vscode/src/vs/base/common/event.ts b/lib/vscode/src/vs/base/common/event.ts index 8b4efda9d448..d992d0abe8a7 100644 --- a/lib/vscode/src/vs/base/common/event.ts +++ b/lib/vscode/src/vs/base/common/event.ts @@ -68,6 +68,7 @@ export namespace Event { * Given an event and a `filter` function, returns another event which emits those * elements for which the `filter` function returns `true`. */ + export function filter(event: Event, filter: (e: T | U) => e is T): Event; export function filter(event: Event, filter: (e: T) => boolean): Event; export function filter(event: Event, filter: (e: T | R) => e is R): Event; export function filter(event: Event, filter: (e: T) => boolean): Event { @@ -188,18 +189,29 @@ export namespace Event { * Given an event, it returns another event which fires only when the event * element changes. */ - export function latch(event: Event): Event { + export function latch(event: Event, equals: (a: T, b: T) => boolean = (a, b) => a === b): Event { let firstCall = true; let cache: T; return filter(event, value => { - const shouldEmit = firstCall || value !== cache; + const shouldEmit = firstCall || !equals(value, cache); firstCall = false; cache = value; return shouldEmit; }); } + /** + * Given an event, it returns another event which fires only when the event + * element changes. + */ + export function split(event: Event, isT: (e: T | U) => e is T): [Event, Event] { + return [ + Event.filter(event, isT), + Event.filter(event, e => !isT(e)) as Event, + ]; + } + /** * Buffers the provided event until a first listener comes * along, at which point fire all the events at once and @@ -633,10 +645,13 @@ export class Emitter { } dispose() { - this._listeners?.clear(); - this._deliveryQueue?.clear(); - this._leakageMon?.dispose(); - this._disposed = true; + if (!this._disposed) { + this._disposed = true; + this._listeners?.clear(); + this._deliveryQueue?.clear(); + this._options?.onLastListenerRemove?.(); + this._leakageMon?.dispose(); + } } } diff --git a/lib/vscode/src/vs/base/common/extpath.ts b/lib/vscode/src/vs/base/common/extpath.ts index c6d8b39aa5f2..3d7d387c763a 100644 --- a/lib/vscode/src/vs/base/common/extpath.ts +++ b/lib/vscode/src/vs/base/common/extpath.ts @@ -22,6 +22,23 @@ export function toSlashes(osPath: string) { return osPath.replace(/[\\/]/g, posix.sep); } +/** + * Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path: + * - turns backward slashes into forward slashes + * - makes it absolute if it starts with a drive letter + * This should only be done for OS paths from Windows (or user provided paths potentially from Windows). + * Using it on a Linux or MaxOS path might change it. + */ +export function toPosixPath(osPath: string) { + if (osPath.indexOf('/') === -1) { + osPath = toSlashes(osPath); + } + if (/^[a-zA-Z]:(\/|$)/.test(osPath)) { // starts with a drive letter + osPath = '/' + osPath; + } + return osPath; +} + /** * Computes the _root_ this path, like `getRoot('c:\files') === c:\`, * `getRoot('files:///files/path') === files:///`, diff --git a/lib/vscode/src/vs/base/common/htmlContent.ts b/lib/vscode/src/vs/base/common/htmlContent.ts index 321fff44c961..50f9a858c90d 100644 --- a/lib/vscode/src/vs/base/common/htmlContent.ts +++ b/lib/vscode/src/vs/base/common/htmlContent.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; import { UriComponents } from 'vs/base/common/uri'; import { escapeIcons } from 'vs/base/common/iconLabels'; import { illegalArgument } from 'vs/base/common/errors'; @@ -90,21 +89,7 @@ export function isMarkdownString(thing: any): thing is IMarkdownString { return false; } -export function markedStringsEquals(a: IMarkdownString | IMarkdownString[], b: IMarkdownString | IMarkdownString[]): boolean { - if (!a && !b) { - return true; - } else if (!a || !b) { - return false; - } else if (Array.isArray(a) && Array.isArray(b)) { - return equals(a, b, markdownStringEqual); - } else if (isMarkdownString(a) && isMarkdownString(b)) { - return markdownStringEqual(a, b); - } else { - return false; - } -} - -function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { +export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { if (a === b) { return true; } else if (!a || !b) { diff --git a/lib/vscode/src/vs/base/common/ipc.d.ts b/lib/vscode/src/vs/base/common/ipc.d.ts new file mode 120000 index 000000000000..34e6f8130162 --- /dev/null +++ b/lib/vscode/src/vs/base/common/ipc.d.ts @@ -0,0 +1 @@ +../../../../../../typings/ipc.d.ts \ No newline at end of file diff --git a/lib/vscode/src/vs/base/common/iterator.ts b/lib/vscode/src/vs/base/common/iterator.ts index 8b60c8757dc2..f7d81bf5bbc3 100644 --- a/lib/vscode/src/vs/base/common/iterator.ts +++ b/lib/vscode/src/vs/base/common/iterator.ts @@ -61,9 +61,10 @@ export namespace Iterable { } } - export function* map(iterable: Iterable, fn: (t: T) => R): Iterable { + export function* map(iterable: Iterable, fn: (t: T, index: number) => R): Iterable { + let index = 0; for (const element of iterable) { - yield fn(element); + yield fn(element, index++); } } diff --git a/lib/vscode/src/vs/base/common/linkedList.ts b/lib/vscode/src/vs/base/common/linkedList.ts index ddeb494cccc7..6a3d9d7af6bd 100644 --- a/lib/vscode/src/vs/base/common/linkedList.ts +++ b/lib/vscode/src/vs/base/common/linkedList.ts @@ -33,6 +33,14 @@ export class LinkedList { } clear(): void { + let node = this._first; + while (node !== Node.Undefined) { + const next = node.next; + node.prev = Node.Undefined; + node.next = Node.Undefined; + node = next; + } + this._first = Node.Undefined; this._last = Node.Undefined; this._size = 0; diff --git a/lib/vscode/src/vs/base/common/map.ts b/lib/vscode/src/vs/base/common/map.ts index 36d543ee934a..a552d658aac9 100644 --- a/lib/vscode/src/vs/base/common/map.ts +++ b/lib/vscode/src/vs/base/common/map.ts @@ -497,10 +497,14 @@ export class TernarySearchTree { yield* this._entries(this._root); } - private *_entries(node: TernarySearchTreeNode | undefined): IterableIterator<[K, V]> { + private *_entries(node: TernarySearchTreeNode | undefined, i: number = 0): IterableIterator<[K, V]> { + if (i > 5000) { + console.log('potential CYCLE detected', new Error().stack); + return; + } if (node) { // left - yield* this._entries(node.left); + yield* this._entries(node.left, i++); // node if (node.value) { @@ -508,10 +512,10 @@ export class TernarySearchTree { yield [node.key, node.value]; } // mid - yield* this._entries(node.mid); + yield* this._entries(node.mid, i++); // right - yield* this._entries(node.right); + yield* this._entries(node.right, i++); } } } diff --git a/lib/vscode/src/vs/base/common/mime.ts b/lib/vscode/src/vs/base/common/mime.ts index dcaca6245b6f..368d77953e68 100644 --- a/lib/vscode/src/vs/base/common/mime.ts +++ b/lib/vscode/src/vs/base/common/mime.ts @@ -316,3 +316,20 @@ export function getExtensionForMimeType(mimeType: string): string | undefined { return undefined; } + +const _simplePattern = /^(.+)\/(.+?)(;.+)?$/; + +export function normalizeMimeType(mimeType: string): string; +export function normalizeMimeType(mimeType: string, strict: true): string | undefined; +export function normalizeMimeType(mimeType: string, strict?: true): string | undefined { + + const match = _simplePattern.exec(mimeType); + if (!match) { + return strict + ? undefined + : mimeType; + } + // https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 + // media and subtype must ALWAYS be lowercase, parameter not + return `${match[1].toLowerCase()}/${match[2].toLowerCase()}${match[3] ?? ''}`; +} diff --git a/lib/vscode/src/vs/base/common/network.ts b/lib/vscode/src/vs/base/common/network.ts index 5833339b33e5..c990c88e095f 100644 --- a/lib/vscode/src/vs/base/common/network.ts +++ b/lib/vscode/src/vs/base/common/network.ts @@ -61,6 +61,7 @@ export namespace Schemas { export const vscodeNotebookCell = 'vscode-notebook-cell'; export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata'; + export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output'; export const vscodeSettings = 'vscode-settings'; diff --git a/lib/vscode/src/vs/base/common/objects.ts b/lib/vscode/src/vs/base/common/objects.ts index 2abc22e51e18..0b032d6a219c 100644 --- a/lib/vscode/src/vs/base/common/objects.ts +++ b/lib/vscode/src/vs/base/common/objects.ts @@ -226,3 +226,13 @@ export function getCaseInsensitive(target: obj, key: string): any { const equivalentKey = Object.keys(target).find(k => k.toLowerCase() === lowercaseKey); return equivalentKey ? target[equivalentKey] : target[key]; } + +export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj { + const result = Object.create(null); + for (const key of Object.keys(obj)) { + if (predicate(key, obj[key])) { + result[key] = obj[key]; + } + } + return result; +} diff --git a/lib/vscode/src/vs/base/common/platform.ts b/lib/vscode/src/vs/base/common/platform.ts index 2bdfc7e6c7e7..2453c8fd24f4 100644 --- a/lib/vscode/src/vs/base/common/platform.ts +++ b/lib/vscode/src/vs/base/common/platform.ts @@ -53,7 +53,7 @@ declare const self: unknown; export const globals: any = (typeof self === 'object' ? self : typeof global === 'object' ? global : {}); let nodeProcess: INodeProcess | undefined = undefined; -if (typeof globals.vscode !== 'undefined') { +if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') { // Native environment (sandboxed) nodeProcess = globals.vscode.process; } else if (typeof process !== 'undefined') { diff --git a/lib/vscode/src/vs/base/common/ports.ts b/lib/vscode/src/vs/base/common/ports.ts new file mode 100644 index 000000000000..5ec75530a871 --- /dev/null +++ b/lib/vscode/src/vs/base/common/ports.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * @returns Returns a random port between 1025 and 65535. + */ +export function randomPort(): number { + const min = 1025; + const max = 65535; + return min + Math.floor((max - min) * Math.random()); +} diff --git a/lib/vscode/src/vs/base/common/process.ts b/lib/vscode/src/vs/base/common/process.ts index 2df6dd5df2f1..ec90453aa83e 100644 --- a/lib/vscode/src/vs/base/common/process.ts +++ b/lib/vscode/src/vs/base/common/process.ts @@ -9,7 +9,7 @@ let safeProcess: INodeProcess & { nextTick: (callback: (...args: any[]) => void) declare const process: INodeProcess; // Native sandbox environment -if (typeof globals.vscode !== 'undefined') { +if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') { const sandboxProcess: INodeProcess = globals.vscode.process; safeProcess = { get platform() { return sandboxProcess.platform; }, @@ -38,7 +38,7 @@ else { nextTick(callback: (...args: any[]) => void): void { return setImmediate(callback); }, // Unsupported - get env() { return Object.create(null); }, + get env() { return {}; }, cwd() { return '/'; } }; } diff --git a/lib/vscode/src/vs/base/common/product.ts b/lib/vscode/src/vs/base/common/product.ts index 9bfd5ae6a42f..8ede7fbdfd6b 100644 --- a/lib/vscode/src/vs/base/common/product.ts +++ b/lib/vscode/src/vs/base/common/product.ts @@ -25,6 +25,11 @@ export type ExtensionUntrustedWorkspaceSupport = { readonly override?: boolean | 'limited' }; +export type ExtensionVirtualWorkspaceSupport = { + readonly default?: boolean, + readonly override?: boolean +}; + export interface IProductConfiguration { // NOTE@coder: add codeServerVersion readonly codeServerVersion?: string; @@ -77,6 +82,7 @@ export interface IProductConfiguration { readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; }; readonly extensionKeywords?: { [extension: string]: readonly string[]; }; readonly keymapExtensionTips?: readonly string[]; + readonly languageExtensionTips?: readonly string[]; readonly trustedExtensionUrlPublicKeys?: { [id: string]: string[]; }; readonly crashReporter?: { @@ -125,7 +131,7 @@ export interface IProductConfiguration { readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[]; }; readonly extensionAllowedProposedApi?: readonly string[]; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; - readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: { default?: boolean, override?: boolean } }; + readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; @@ -133,6 +139,8 @@ export interface IProductConfiguration { readonly 'configurationSync.store'?: ConfigurationSyncStore; readonly darwinUniversalAssetId?: string; + + readonly webviewContentExternalBaseUrlTemplate?: string; } export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; diff --git a/lib/vscode/src/vs/base/common/resources.ts b/lib/vscode/src/vs/base/common/resources.ts index f516eec5304d..5a40b0cc4247 100644 --- a/lib/vscode/src/vs/base/common/resources.ts +++ b/lib/vscode/src/vs/base/common/resources.ts @@ -50,7 +50,7 @@ export interface IExtUri { /** * Creates a key from a resource URI to be used to resource comparison and for resource maps. - * @see ResourceMap + * @see {@link ResourceMap} * @param uri Uri * @param ignoreFragment Ignore the fragment (defaults to `false`) */ @@ -264,12 +264,7 @@ export class ExtUri implements IExtUri { path: newURI.path }); } - if (path.indexOf('/') === -1) { // no slashes? it's likely a Windows path - path = extpath.toSlashes(path); - if (/^[a-zA-Z]:(\/|$)/.test(path)) { // starts with a drive letter - path = '/' + path; - } - } + path = extpath.toPosixPath(path); // we allow path to be a windows path return base.with({ path: paths.posix.resolve(base.path, path) }); diff --git a/lib/vscode/src/vs/base/common/strings.ts b/lib/vscode/src/vs/base/common/strings.ts index e560d16925ba..67d4c7f16a32 100644 --- a/lib/vscode/src/vs/base/common/strings.ts +++ b/lib/vscode/src/vs/base/common/strings.ts @@ -1082,3 +1082,81 @@ function getGraphemeBreakRawData(): number[] { } //#endregion + +/** + * Computes the offset after performing a left delete on the given string, + * while considering unicode grapheme/emoji rules. +*/ +export function getLeftDeleteOffset(offset: number, str: string): number { + if (offset === 0) { + return 0; + } + + // Try to delete emoji part. + const emojiOffset = getOffsetBeforeLastEmojiComponent(offset, str); + if (emojiOffset !== undefined) { + return emojiOffset; + } + + // Otherwise, just skip a single code point. + const codePoint = getPrevCodePoint(str, offset); + offset -= getUTF16Length(codePoint); + return offset; +} + +function getOffsetBeforeLastEmojiComponent(offset: number, str: string): number | undefined { + // See https://www.unicode.org/reports/tr51/tr51-14.html#EBNF_and_Regex for the + // structure of emojis. + let codePoint = getPrevCodePoint(str, offset); + offset -= getUTF16Length(codePoint); + + // Skip modifiers + while ((isEmojiModifier(codePoint) || codePoint === CodePoint.emojiVariantSelector || codePoint === CodePoint.enclosingKeyCap)) { + if (offset === 0) { + // Cannot skip modifier, no preceding emoji base. + return undefined; + } + codePoint = getPrevCodePoint(str, offset); + offset -= getUTF16Length(codePoint); + } + + // Expect base emoji + if (!isEmojiImprecise(codePoint)) { + // Unexpected code point, not a valid emoji. + return undefined; + } + + if (offset >= 0) { + // Skip optional ZWJ code points that combine multiple emojis. + // In theory, we should check if that ZWJ actually combines multiple emojis + // to prevent deleting ZWJs in situations we didn't account for. + const optionalZwjCodePoint = getPrevCodePoint(str, offset); + if (optionalZwjCodePoint === CodePoint.zwj) { + offset -= getUTF16Length(optionalZwjCodePoint); + } + } + + return offset; +} + +function getUTF16Length(codePoint: number) { + return codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; +} + +function isEmojiModifier(codePoint: number): boolean { + return 0x1F3FB <= codePoint && codePoint <= 0x1F3FF; +} + +const enum CodePoint { + zwj = 0x200D, + + /** + * Variation Selector-16 (VS16) + */ + emojiVariantSelector = 0xFE0F, + + /** + * Combining Enclosing Keycap + */ + enclosingKeyCap = 0x20E3, +} diff --git a/lib/vscode/src/vs/base/common/uri.ts b/lib/vscode/src/vs/base/common/uri.ts index c2b0b02eb867..da8c4de410d9 100644 --- a/lib/vscode/src/vs/base/common/uri.ts +++ b/lib/vscode/src/vs/base/common/uri.ts @@ -327,13 +327,15 @@ export class URI implements UriComponents { } static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { - return new Uri( + const result = new Uri( components.scheme, components.authority, components.path, components.query, components.fragment, ); + _validateUri(result, true); + return result; } /** diff --git a/lib/vscode/src/vs/base/common/uriIpc.ts b/lib/vscode/src/vs/base/common/uriIpc.ts index 29b2f9dfc2b7..ef2291d49b13 100644 --- a/lib/vscode/src/vs/base/common/uriIpc.ts +++ b/lib/vscode/src/vs/base/common/uriIpc.ts @@ -5,7 +5,6 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { MarshalledObject } from 'vs/base/common/marshalling'; -import { Schemas } from './network'; export interface IURITransformer { transformIncoming(uri: UriComponents): UriComponents; @@ -32,35 +31,29 @@ function toJSON(uri: URI): UriComponents { export class URITransformer implements IURITransformer { - constructor(private readonly remoteAuthority: string) { + private readonly _uriTransformer: IRawURITransformer; + + constructor(uriTransformer: IRawURITransformer) { + this._uriTransformer = uriTransformer; } - // NOTE@coder: Coming in from the browser it'll be vscode-remote so it needs - // to be transformed into file. public transformIncoming(uri: UriComponents): UriComponents { - return uri.scheme === Schemas.vscodeRemote - ? toJSON(URI.file(uri.path)) - : uri; + const result = this._uriTransformer.transformIncoming(uri); + return (result === uri ? uri : toJSON(URI.from(result))); } - // NOTE@coder: Going out to the browser it'll be file so it needs to be - // transformed into vscode-remote. public transformOutgoing(uri: UriComponents): UriComponents { - return uri.scheme === Schemas.file - ? toJSON(URI.from({ authority: this.remoteAuthority, scheme: Schemas.vscodeRemote, path: uri.path })) - : uri; + const result = this._uriTransformer.transformOutgoing(uri); + return (result === uri ? uri : toJSON(URI.from(result))); } public transformOutgoingURI(uri: URI): URI { - return uri.scheme === Schemas.file - ? URI.from({ authority: this.remoteAuthority, scheme: Schemas.vscodeRemote, path:uri.path }) - : uri; + const result = this._uriTransformer.transformOutgoing(uri); + return (result === uri ? uri : URI.from(result)); } public transformOutgoingScheme(scheme: string): string { - return scheme === Schemas.file - ? Schemas.vscodeRemote - : scheme; + return this._uriTransformer.transformOutgoingScheme(scheme); } } @@ -159,4 +152,4 @@ export function transformAndReviveIncomingURIs(obj: T, transformer: IURITrans return obj; } return result; -} +} \ No newline at end of file diff --git a/lib/vscode/src/vs/server/common/util.ts b/lib/vscode/src/vs/base/common/util.ts similarity index 100% rename from lib/vscode/src/vs/server/common/util.ts rename to lib/vscode/src/vs/base/common/util.ts diff --git a/lib/vscode/src/vs/base/node/extpath.ts b/lib/vscode/src/vs/base/node/extpath.ts index eaf62cd8e125..325c0d2d1dc3 100644 --- a/lib/vscode/src/vs/base/node/extpath.ts +++ b/lib/vscode/src/vs/base/node/extpath.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { promisify } from 'util'; import { rtrim } from 'vs/base/common/strings'; import { sep, join, normalize, dirname, basename } from 'vs/base/common/path'; -import { readdirSync } from 'vs/base/node/pfs'; +import { Promises, readdirSync } from 'vs/base/node/pfs'; /** * Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 @@ -57,7 +56,7 @@ export async function realpath(path: string): Promise { // calls `fs.native.realpath` which will result in subst // drives to be resolved to their target on Windows // https://github.com/microsoft/vscode/issues/118562 - return await promisify(fs.realpath)(path); + return await Promises.realpath(path); } catch (error) { // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization @@ -67,7 +66,7 @@ export async function realpath(path: string): Promise { // to not resolve links but to simply see if the path is read accessible or not. const normalizedPath = normalizePath(path); - await fs.promises.access(normalizedPath, fs.constants.R_OK); + await Promises.access(normalizedPath, fs.constants.R_OK); return normalizedPath; } diff --git a/lib/vscode/src/vs/base/node/pfs.ts b/lib/vscode/src/vs/base/node/pfs.ts index 30d25a8cc338..40a81246b022 100644 --- a/lib/vscode/src/vs/base/node/pfs.ts +++ b/lib/vscode/src/vs/base/node/pfs.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; +import { promisify } from 'util'; import { join } from 'vs/base/common/path'; import { ResourceQueue } from 'vs/base/common/async'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -56,7 +57,7 @@ async function rimrafMove(path: string): Promise { try { const pathInTemp = join(tmpdir(), generateUuid()); try { - await fs.promises.rename(path, pathInTemp); + await Promises.rename(path, pathInTemp); } catch (error) { return rimrafUnlink(path); // if rename fails, delete without tmp dir } @@ -71,7 +72,7 @@ async function rimrafMove(path: string): Promise { } async function rimrafUnlink(path: string): Promise { - return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 }); + return Promises.rmdir(path, { recursive: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -102,12 +103,12 @@ export interface IDirent { export async function readdir(path: string): Promise; export async function readdir(path: string, options: { withFileTypes: true }): Promise; export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { - return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : Promises.readdir(path))); } async function safeReaddirWithFileTypes(path: string): Promise { try { - return await fs.promises.readdir(path, { withFileTypes: true }); + return await Promises.readdir(path, { withFileTypes: true }); } catch (error) { console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } @@ -126,7 +127,7 @@ async function safeReaddirWithFileTypes(path: string): Promise { let isSymbolicLink = false; try { - const lstat = await fs.promises.lstat(join(path, child)); + const lstat = await Promises.lstat(join(path, child)); isFile = lstat.isFile(); isDirectory = lstat.isDirectory(); @@ -251,7 +252,7 @@ export namespace SymlinkSupport { // First stat the link let lstats: fs.Stats | undefined; try { - lstats = await fs.promises.lstat(path); + lstats = await Promises.lstat(path); // Return early if the stat is not a symbolic link at all if (!lstats.isSymbolicLink()) { @@ -264,7 +265,7 @@ export namespace SymlinkSupport { // If the stat is a symbolic link or failed to stat, use fs.stat() // which for symbolic links will stat the target they point to try { - const stats = await fs.promises.stat(path); + const stats = await Promises.stat(path); return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { @@ -279,7 +280,7 @@ export namespace SymlinkSupport { // are not supported (https://github.com/nodejs/node/issues/36790) if (isWindows && error.code === 'EACCES') { try { - const stats = await fs.promises.stat(await fs.promises.readlink(path)); + const stats = await Promises.stat(await Promises.readlink(path)); return { stat: stats, symbolicLink: { dangling: false } }; } catch (error) { @@ -488,24 +489,19 @@ export async function move(source: string, target: string): Promise { // as well because conceptually it is a change of a similar category. async function updateMtime(path: string): Promise { try { - const stat = await fs.promises.lstat(path); + const stat = await Promises.lstat(path); if (stat.isDirectory() || stat.isSymbolicLink()) { return; // only for files } - const fh = await fs.promises.open(path, 'a'); - try { - await fh.utimes(stat.atime, new Date()); - } finally { - await fh.close(); - } + await Promises.utimes(path, stat.atime, new Date()); } catch (error) { // Ignore any error } } try { - await fs.promises.rename(source, target); + await Promises.rename(source, target); await updateMtime(target); } catch (error) { @@ -594,7 +590,7 @@ async function doCopy(source: string, target: string, payload: ICopyPayload): Pr async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise { // Create folder - await fs.promises.mkdir(target, { recursive: true, mode }); + await Promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = await readdir(source); @@ -606,16 +602,16 @@ async function doCopyDirectory(source: string, target: string, mode: number, pay async function doCopyFile(source: string, target: string, mode: number): Promise { // Copy file - await fs.promises.copyFile(source, target); + await Promises.copyFile(source, target); // restore mode (https://github.com/nodejs/node/issues/1104) - await fs.promises.chmod(target, mode); + await Promises.chmod(target, mode); } async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise { // Figure out link target - let linkTarget = await fs.promises.readlink(source); + let linkTarget = await Promises.readlink(source); // Special case: the symlink points to a target that is // actually within the path that is being copied. In that @@ -626,7 +622,7 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo } // Create symlink - await fs.promises.symlink(linkTarget, target); + await Promises.symlink(linkTarget, target); } //#endregion @@ -635,7 +631,7 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo export async function exists(path: string): Promise { try { - await fs.promises.access(path); + await Promises.access(path); return true; } catch { @@ -644,3 +640,56 @@ export async function exists(path: string): Promise { } //#endregion + +//#region Promise based fs methods + +/** + * Note: prefer this namespace over the `fs.promises` API + * to enable `graceful-fs` to function properly. Given issue + * https://github.com/isaacs/node-graceful-fs/issues/160 it + * is evident that the module only takes care of the non-promise + * based fs methods. + * + * Another reason is `realpath` being entirely different in + * the promise based implementation compared to the other + * one (https://github.com/microsoft/vscode/issues/118562) + */ +export namespace Promises { + export const access = promisify(fs.access); + + export const stat = promisify(fs.stat); + export const lstat = promisify(fs.lstat); + export const utimes = promisify(fs.utimes); + + export const read = promisify(fs.read); + export const readFile = promisify(fs.readFile); + + export const write = promisify(fs.write); + export const writeFile = promisify(fs.writeFile); + + export const appendFile = promisify(fs.appendFile); + + export const fdatasync = promisify(fs.fdatasync); + export const truncate = promisify(fs.truncate); + + export const rename = promisify(fs.rename); + export const copyFile = promisify(fs.copyFile); + + export const open = promisify(fs.open); + export const close = promisify(fs.close); + + export const symlink = promisify(fs.symlink); + export const readlink = promisify(fs.readlink); + + export const chmod = promisify(fs.chmod); + + export const readdir = promisify(fs.readdir); + export const mkdir = promisify(fs.mkdir); + + export const unlink = promisify(fs.unlink); + export const rmdir = promisify(fs.rmdir); + + export const realpath = promisify(fs.realpath); +} + +//#endregion diff --git a/lib/vscode/src/vs/base/node/ports.ts b/lib/vscode/src/vs/base/node/ports.ts index ba8297dc46ff..f6d27bb62154 100644 --- a/lib/vscode/src/vs/base/node/ports.ts +++ b/lib/vscode/src/vs/base/node/ports.ts @@ -5,15 +5,6 @@ import * as net from 'net'; -/** - * @returns Returns a random port between 1025 and 65535. - */ -export function randomPort(): number { - const min = 1025; - const max = 65535; - return min + Math.floor((max - min) * Math.random()); -} - /** * Given a start point and a max number of retries, will find a port that * is openable. Will return 0 in case no free port can be found. diff --git a/lib/vscode/src/vs/base/node/processes.ts b/lib/vscode/src/vs/base/node/processes.ts index 363dc229f627..f1b1b3982122 100644 --- a/lib/vscode/src/vs/base/node/processes.ts +++ b/lib/vscode/src/vs/base/node/processes.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as cp from 'child_process'; import * as nls from 'vs/nls'; @@ -458,7 +457,7 @@ export namespace win32 { async function fileExists(path: string): Promise { if (await pfs.exists(path)) { - return !((await fs.promises.stat(path)).isDirectory()); + return !((await pfs.Promises.stat(path)).isDirectory()); } return false; } diff --git a/lib/vscode/src/vs/base/node/zip.ts b/lib/vscode/src/vs/base/node/zip.ts index 0d74501d9977..3c289c297d12 100644 --- a/lib/vscode/src/vs/base/node/zip.ts +++ b/lib/vscode/src/vs/base/node/zip.ts @@ -5,10 +5,10 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import { promises, createWriteStream, WriteStream } from 'fs'; +import { createWriteStream, WriteStream } from 'fs'; import { Readable } from 'stream'; import { Sequencer, createCancelablePromise } from 'vs/base/common/async'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises, rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -86,7 +86,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa } }); - return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { + return Promise.resolve(Promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { if (token.isCancellationRequested) { return; } @@ -149,7 +149,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok // directory file names end with '/' if (/\/$/.test(fileName)) { const targetFileName = path.join(targetPath, fileName); - last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); + last = createCancelablePromise(token => Promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); return; } diff --git a/lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts b/lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts index 0e88c4481c67..7d9459c1c240 100644 --- a/lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts @@ -743,15 +743,15 @@ export class PersistentProtocol implements IMessagePassingProtocol { }, Math.max(ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg, 0) + 5); } - // NOTE@coder: Set the socket without initiating a reconnect. - public setSocket(socket: ISocket): void { - this._socket = socket; - } - public getSocket(): ISocket { return this._socket; } + // NOTE@coder: add setSocket + public setSocket(socket: ISocket) { + this._socket = socket; + } + public getMillisSinceLastIncomingData(): number { return Date.now() - this._socketReader.lastReadTime; } diff --git a/lib/vscode/src/vs/base/parts/ipc/common/ipc.ts b/lib/vscode/src/vs/base/parts/ipc/common/ipc.ts index 0757b52ab813..c3a249d5c91f 100644 --- a/lib/vscode/src/vs/base/parts/ipc/common/ipc.ts +++ b/lib/vscode/src/vs/base/parts/ipc/common/ipc.ts @@ -13,6 +13,7 @@ import { getRandomElement } from 'vs/base/common/arrays'; import { isFunction, isUndefinedOrNull } from 'vs/base/common/types'; import { revive } from 'vs/base/common/marshalling'; import * as strings from 'vs/base/common/strings'; +import { memoize } from 'vs/base/common/decorators'; /** * An `IChannel` is an abstraction over a collection of commands. @@ -723,11 +724,16 @@ export class ChannelClient implements IChannelClient, IDisposable { } } + @memoize + get onDidInitializePromise(): Promise { + return Event.toPromise(this.onDidInitialize); + } + private whenInitialized(): Promise { if (this.state === State.Idle) { return Promise.resolve(); } else { - return Event.toPromise(this.onDidInitialize); + return this.onDidInitializePromise; } } diff --git a/lib/vscode/src/vs/base/parts/quickinput/browser/media/quickInput.css b/lib/vscode/src/vs/base/parts/quickinput/browser/media/quickInput.css index 297f763a76a0..651e43b00bff 100644 --- a/lib/vscode/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/lib/vscode/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -170,7 +170,7 @@ border-top-style: solid; } -.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border { +.quick-input-list .monaco-list-row[data-index="0"] .quick-input-list-entry.quick-input-list-separator-border { border-top-style: none; } @@ -276,3 +276,14 @@ .quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label { display: flex; } + +/* focused items in quick pick */ +.quick-input-list .monaco-list-row.focused .monaco-keybinding-key, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator, +.quick-input-list .monaco-list-row.focused .quick-input-list-rows .quick-input-list-row .codicon, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .codicon { + color: inherit +} +.quick-input-list .monaco-list-row.focused .monaco-keybinding-key { + background: none; +} diff --git a/lib/vscode/src/vs/base/parts/quickinput/browser/quickInput.ts b/lib/vscode/src/vs/base/parts/quickinput/browser/quickInput.ts index 7aed5e202bcf..2cb5092c9015 100644 --- a/lib/vscode/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/lib/vscode/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/quickInput'; -import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS, ItemActivation, QuickInputHideReason, IQuickInputHideEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickDidAcceptEvent, NO_KEY_MODS, ItemActivation, QuickInputHideReason, IQuickInputHideEvent, IQuickPickWillAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList, QuickInputListFocus } from './quickInputList'; @@ -29,7 +29,6 @@ import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { Color } from 'vs/base/common/color'; import { registerCodicon, Codicon } from 'vs/base/common/codicons'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { escape } from 'vs/base/common/strings'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { isString } from 'vs/base/common/types'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; @@ -359,7 +358,7 @@ class QuickInput extends Disposable implements IQuickInput { const validationMessage = this.validationMessage || this.noValidationMessage; if (this._lastValidationMessage !== validationMessage) { this._lastValidationMessage = validationMessage; - dom.reset(this.ui.message, ...renderLabelWithIcons(escape(validationMessage))); + dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage)); } if (this._lastSeverity !== this.severity) { this._lastSeverity = this.severity; @@ -428,7 +427,8 @@ class QuickPick extends QuickInput implements IQuickPi private _ariaLabel: string | undefined; private _placeholder: string | undefined; private readonly onDidChangeValueEmitter = this._register(new Emitter()); - private readonly onDidAcceptEmitter = this._register(new Emitter()); + private readonly onWillAcceptEmitter = this._register(new Emitter()); + private readonly onDidAcceptEmitter = this._register(new Emitter()); private readonly onDidCustomEmitter = this._register(new Emitter()); private _items: Array = []; private itemsUpdated = false; @@ -473,8 +473,11 @@ class QuickPick extends QuickInput implements IQuickPi } set value(value: string) { - this._value = value || ''; - this.update(); + if (this._value !== value) { + this._value = value || ''; + this.update(); + this.onDidChangeValueEmitter.fire(this._value); + } } filterValue = (value: string) => value; @@ -499,6 +502,7 @@ class QuickPick extends QuickInput implements IQuickPi onDidChangeValue = this.onDidChangeValueEmitter.event; + onWillAccept = this.onWillAcceptEmitter.event; onDidAccept = this.onDidAcceptEmitter.event; onDidCustom = this.onDidCustomEmitter.event; @@ -761,7 +765,7 @@ class QuickPick extends QuickInput implements IQuickPi if (this.activeItems[0]) { this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.onDidAcceptEmitter.fire({ inBackground: true }); + this.handleAccept(true); } break; @@ -784,7 +788,7 @@ class QuickPick extends QuickInput implements IQuickPi this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); } - this.onDidAcceptEmitter.fire({ inBackground: false }); + this.handleAccept(false); })); this.visibleDisposables.add(this.ui.onDidCustom(() => { this.onDidCustomEmitter.fire(); @@ -812,7 +816,7 @@ class QuickPick extends QuickInput implements IQuickPi this._selectedItems = selectedItems as T[]; this.onDidChangeSelectionEmitter.fire(selectedItems as T[]); if (selectedItems.length) { - this.onDidAcceptEmitter.fire({ inBackground: event instanceof MouseEvent && event.button === 1 /* mouse middle click */ }); + this.handleAccept(event instanceof MouseEvent && event.button === 1 /* mouse middle click */); } })); this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { @@ -832,6 +836,18 @@ class QuickPick extends QuickInput implements IQuickPi super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.) } + private handleAccept(inBackground: boolean): void { + + // Figure out veto via `onWillAccept` event + let veto = false; + this.onWillAcceptEmitter.fire({ veto: () => veto = true }); + + // Continue with `onDidAccpet` if no veto + if (!veto) { + this.onDidAcceptEmitter.fire({ inBackground }); + } + } + private registerQuickNavigation() { return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => { if (this.canSelectMany || !this._quickNavigate) { @@ -876,7 +892,7 @@ class QuickPick extends QuickInput implements IQuickPi if (this.activeItems[0]) { this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.onDidAcceptEmitter.fire({ inBackground: false }); + this.handleAccept(false); } // Unset quick navigate after press. It is only valid once // and should not result in any behaviour change afterwards diff --git a/lib/vscode/src/vs/base/parts/quickinput/common/quickInput.ts b/lib/vscode/src/vs/base/parts/quickinput/common/quickInput.ts index 56aed1582031..7454b45a6d7c 100644 --- a/lib/vscode/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/lib/vscode/src/vs/base/parts/quickinput/common/quickInput.ts @@ -207,7 +207,17 @@ export interface IQuickInput extends IDisposable { hide(): void; } -export interface IQuickPickAcceptEvent { +export interface IQuickPickWillAcceptEvent { + + /** + * Allows to disable the default accept handling + * of the picker. If `veto` is called, the picker + * will not trigger the `onDidAccept` event. + */ + veto(): void; +} + +export interface IQuickPickDidAcceptEvent { /** * Signals if the picker item is to be accepted @@ -239,7 +249,8 @@ export interface IQuickPick extends IQuickInput { readonly onDidChangeValue: Event; - readonly onDidAccept: Event; + readonly onWillAccept: Event; + readonly onDidAccept: Event; /** * If enabled, will fire the `onDidAccept` event when diff --git a/lib/vscode/src/vs/base/parts/sandbox/common/electronTypes.ts b/lib/vscode/src/vs/base/parts/sandbox/common/electronTypes.ts index 5c36ba595531..01622e6bbab1 100644 --- a/lib/vscode/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/lib/vscode/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -13,6 +13,10 @@ export interface MessageBoxOptions { + /** + * Content of the message box. + */ + message: string; /** * Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows, * `"question"` displays the same icon as `"info"`, unless you set an icon using @@ -34,10 +38,6 @@ export interface MessageBoxOptions { * Title of the message box, some platforms will not show it. */ title?: string; - /** - * Content of the message box. - */ - message: string; /** * Extra information of the message. */ diff --git a/lib/vscode/src/vs/base/parts/sandbox/electron-browser/preload.js b/lib/vscode/src/vs/base/parts/sandbox/electron-browser/preload.js index bae46cc3d958..67857146ad4b 100644 --- a/lib/vscode/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/lib/vscode/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -112,12 +112,6 @@ ipcRenderer.invoke('vscode:fetchShellEnv') ]); - if (!process.env['VSCODE_SKIP_PROCESS_ENV_PATCHING'] /* TODO@bpasero for https://github.com/microsoft/vscode/issues/108804 */) { - // Assign all keys of the shell environment to our process environment - // But make sure that the user environment wins in the end over shell environment - Object.assign(process.env, shellEnv, userEnv); - } - return { ...process.env, ...shellEnv, ...userEnv }; })(); diff --git a/lib/vscode/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/lib/vscode/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index e03f9e136739..cd17ec0006eb 100644 --- a/lib/vscode/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/lib/vscode/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -36,6 +36,9 @@ export interface IpcRendererEvent extends Event { } export interface IpcRenderer { + + // Docs: https://electronjs.org/docs/api/ipc-renderer + /** * Listens to `channel`, when a new message arrives `listener` would be called with * `listener(event, args...)`. @@ -58,9 +61,13 @@ export interface IpcRenderer { * Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an * exception. * - * > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special - * Electron objects is deprecated, and will begin throwing an exception starting - * with Electron 9. + * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special + * Electron objects will throw an exception. + * + * Since the main process does not have support for DOM objects such as + * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over + * Electron's IPC to the main process, as the main process would have no way to + * decode them. Attempting to send such objects over IPC will result in an error. * * The main process handles it by listening for `channel` with the `ipcMain` * module. @@ -81,9 +88,13 @@ export interface IpcRenderer { * included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw * an exception. * - * > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special - * Electron objects is deprecated, and will begin throwing an exception starting - * with Electron 9. + * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special + * Electron objects will throw an exception. + * + * Since the main process does not have support for DOM objects such as + * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over + * Electron's IPC to the main process, as the main process would have no way to + * decode them. Attempting to send such objects over IPC will result in an error. * * The main process should listen for `channel` with `ipcMain.handle()`. * @@ -111,7 +122,7 @@ export interface IpcRenderer { // * For more information on using `MessagePort` and `MessageChannel`, see the MDN // * documentation. // */ - // postMessage(channel: string, message: any): void; + // postMessage(channel: string, message: any, transfer?: MessagePort[]): void; } export interface WebFrame { @@ -119,6 +130,11 @@ export interface WebFrame { * Changes the zoom level to the specified level. The original size is 0 and each * increment above or below represents zooming 20% larger or smaller to default * limits of 300% and 50% of original size, respectively. + * + * > **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that + * the zoom level for a specific domain propagates across all instances of windows + * with the same domain. Differentiating the window URLs will make zoom work + * per-window. */ setZoomLevel(level: number): void; } @@ -207,7 +223,7 @@ export interface CrashReporterStartOptions { rateLimit?: boolean; /** * If true, crash reports will be compressed and uploaded with `Content-Encoding: - * gzip`. Default is `false`. + * gzip`. Default is `true`. */ compress?: boolean; /** diff --git a/lib/vscode/src/vs/base/parts/storage/node/storage.ts b/lib/vscode/src/vs/base/parts/storage/node/storage.ts index e497862ddafe..e79d41f9e0da 100644 --- a/lib/vscode/src/vs/base/parts/storage/node/storage.ts +++ b/lib/vscode/src/vs/base/parts/storage/node/storage.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import type { Database, Statement } from 'vscode-sqlite3'; -import { promises } from 'fs'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; import { basename } from 'vs/base/common/path'; -import { copy } from 'vs/base/node/pfs'; +import { copy, Promises } from 'vs/base/node/pfs'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; interface IDatabaseConnection { @@ -187,7 +186,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // Delete the existing DB. If the path does not exist or fails to // be deleted, we do not try to recover anymore because we assume // that the path is no longer writeable for us. - return promises.unlink(this.path).then(() => { + return Promises.unlink(this.path).then(() => { // Re-open the DB fresh return this.doConnect(this.path).then(recoveryConnection => { @@ -273,9 +272,9 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // folder is really not writeable for us. // try { - await promises.unlink(path); + await Promises.unlink(path); try { - await promises.rename(this.toBackupPath(path), path); + await Promises.rename(this.toBackupPath(path), path); } catch (error) { // ignore } diff --git a/lib/vscode/src/vs/base/parts/storage/test/node/storage.test.ts b/lib/vscode/src/vs/base/parts/storage/test/node/storage.test.ts index fd963590dc43..07846cf4a4f6 100644 --- a/lib/vscode/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/lib/vscode/src/vs/base/parts/storage/test/node/storage.test.ts @@ -7,9 +7,8 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/pa import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { strictEqual, ok } from 'assert'; -import { writeFile, exists, rimraf } from 'vs/base/node/pfs'; +import { writeFile, exists, rimraf, Promises } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; @@ -23,7 +22,7 @@ flakySuite('Storage Library', function () { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(function () { @@ -296,7 +295,7 @@ flakySuite('SQLite Storage Library', function () { setup(function () { testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return promises.mkdir(testdir, { recursive: true }); + return Promises.mkdir(testdir, { recursive: true }); }); teardown(function () { @@ -477,7 +476,7 @@ flakySuite('SQLite Storage Library', function () { // on shutdown. await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */); - await promises.unlink(backupPath); // also test that the recovery DB is backed up properly + await Promises.unlink(backupPath); // also test that the recovery DB is backed up properly let recoveryCalled = false; await storage.close(() => { diff --git a/lib/vscode/src/vs/base/test/browser/comparers.test.ts b/lib/vscode/src/vs/base/test/browser/comparers.test.ts index ef5856b76c65..df66a081334f 100644 --- a/lib/vscode/src/vs/base/test/browser/comparers.test.ts +++ b/lib/vscode/src/vs/base/test/browser/comparers.test.ts @@ -3,13 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; +import { + compareFileNames, + compareFileExtensions, + compareFileNamesDefault, + compareFileExtensionsDefault, + compareFileNamesUpper, + compareFileExtensionsUpper, + compareFileNamesLower, + compareFileExtensionsLower, + compareFileNamesUnicode, + compareFileExtensionsUnicode, +} from 'vs/base/common/comparers'; import * as assert from 'assert'; const compareLocale = (a: string, b: string) => a.localeCompare(b); const compareLocaleNumeric = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }); - suite('Comparers', () => { test('compareFileNames', () => { @@ -23,11 +33,11 @@ suite('Comparers', () => { assert(compareFileNames(null, 'abc') < 0, 'null should be come before real values'); assert(compareFileNames('', '') === 0, 'empty should be equal'); assert(compareFileNames('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNames('z', 'A') > 0, 'z comes is after A regardless of case'); - assert(compareFileNames('Z', 'a') > 0, 'Z comes after a regardless of case'); + assert(compareFileNames('z', 'A') > 0, 'z comes after A'); + assert(compareFileNames('Z', 'a') > 0, 'Z comes after a'); // name plus extension comparisons - assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'compares the whole name all at once by locale'); assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); // dotfile comparisons @@ -35,7 +45,7 @@ suite('Comparers', () => { assert(compareFileNames('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); assert(compareFileNames('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); assert(compareFileNames('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); - assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); // dotfile vs non-dotfile comparisons assert(compareFileNames(null, '.abc') < 0, 'null should come before dotfiles'); @@ -51,14 +61,16 @@ suite('Comparers', () => { assert(compareFileNames('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); assert(compareFileNames('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNames('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted in name order'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNames), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileNamesDefault // // name-only comparisons - assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter does not sort by locale'); - assert(compareFileNames('รข', 'ร‚') !== compareLocale('รข', 'ร‚'), 'the same accented letter does not sort by locale'); + assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter sorts in unicode order, not by locale'); + assert(compareFileNames('รข', 'ร‚') !== compareLocale('รข', 'ร‚'), 'the same accented letter sorts in unicode order, not by locale'); assert.notDeepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); assert.notDeepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileNames), ['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order'); @@ -67,6 +79,7 @@ suite('Comparers', () => { assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); assert(compareFileNames('art01', 'Art01') !== 'art01'.localeCompare('Art01', undefined, { numeric: true }), 'a numerically equivalent word of a different case does not compare numerically based on locale'); + assert(compareFileNames('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in full filename unicode order'); }); @@ -89,9 +102,6 @@ suite('Comparers', () => { assert(compareFileExtensions('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); assert(compareFileExtensions('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extensions even if filenames compare differently'); - assert(compareFileExtensions('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); - assert(compareFileExtensions('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); - assert(compareFileExtensions('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); // dotfile comparisons assert(compareFileExtensions('.abc', '.abc') === 0, 'equal dotfiles should be equal'); @@ -113,8 +123,8 @@ suite('Comparers', () => { assert(compareFileExtensions('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); assert(compareFileExtensions('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, names should be compared'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensions), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results from compareFileExtensionsDefault @@ -127,8 +137,9 @@ suite('Comparers', () => { assert.notDeepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileExtensions), ['email', 'Email', 'รฉmail', 'ร‰mail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents do not sort in locale order'); // name plus extension comparisons - assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale'); - assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale'); + assert(compareFileExtensions('a.MD', 'a.md') < 0, 'case differences in extensions sort in unicode order'); + assert(compareFileExtensions('a.md', 'A.md') > 0, 'case differences in names sort in unicode order'); + assert(compareFileExtensions('a.md', 'b.MD') > 0, 'when extensions are the same except for case, the files sort by extension'); assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); // dotfile comparisons @@ -144,6 +155,8 @@ suite('Comparers', () => { assert(compareFileExtensions('art01', 'Art01') !== compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case does not compare by locale'); assert(compareFileExtensions('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); assert(compareFileExtensions('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order'); + assert(compareFileExtensions('a.ext1', 'b.Ext1') > 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted in extension unicode order'); + assert(compareFileExtensions('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in extension unicode order'); }); @@ -158,8 +171,8 @@ suite('Comparers', () => { assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values'); assert(compareFileNamesDefault('', '') === 0, 'empty should be equal'); assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case'); - assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a regardless of case'); + assert(compareFileNamesDefault('z', 'A') > 0, 'z comes after A'); + assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a'); // name plus extension comparisons assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal'); @@ -173,7 +186,7 @@ suite('Comparers', () => { assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); - assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); // dotfile vs non-dotfile comparisons assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles'); @@ -189,6 +202,8 @@ suite('Comparers', () => { assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); assert(compareFileNamesDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesDefault('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are compared by full filename'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesDefault), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileNames @@ -203,7 +218,7 @@ suite('Comparers', () => { assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); - + assert(compareFileNamesDefault('a.ext1', 'a.Ext1') === compareLocale('ext1', 'Ext1'), 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in extension locale order'); }); test('compareFileExtensionsDefault', () => { @@ -225,8 +240,6 @@ suite('Comparers', () => { assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); - assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); - assert(compareFileExtensionsDefault('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); // dotfile comparisons assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal'); @@ -248,8 +261,8 @@ suite('Comparers', () => { assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsDefault), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileExtensions @@ -263,6 +276,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order'); // dotfile comparisons @@ -278,6 +292,418 @@ suite('Comparers', () => { assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsDefault('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, full filenames should be compared'); + assert(compareFileExtensionsDefault('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'if extensions with numbers are equal except for case, full filenames are compared in locale order'); }); + + test('compareFileNamesUpper', () => { + + // + // Comparisons with the same results as compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUpper(null, null) === 0, 'null should be equal'); + assert(compareFileNamesUpper(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesUpper('', '') === 0, 'empty should be equal'); + assert(compareFileNamesUpper('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesUpper('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileNamesUpper('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesUpper('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesUpper('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesUpper('aggregate.go', 'aggregate_repo.go') > 0, 'compares the full filename in locale order'); + + // dotfile comparisons + assert(compareFileNamesUpper('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesUpper('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesUpper('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesUpper(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesUpper('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesUpper('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesUpper('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesUpper('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesUpper('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesUpper('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesUpper('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesUpper('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesUpper('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesUpper('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesUpper('a.ext1', 'b.Ext1') < 0, 'different names with the equal extensions except for case are sorted by full filename'); + assert(compareFileNamesUpper('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'same names with equal and extensions except for case are sorted in full filename locale order'); + + // + // Comparisons with different results than compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUpper('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileNamesUpper('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileNamesUpper('รข', 'ร‚') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesUpper), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase first'); + assert.deepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileNamesUpper), ['Email', 'ร‰mail', 'email', 'รฉmail'], 'the same base characters with different case or accents sort uppercase first'); + + // numeric comparisons + assert(compareFileNamesUpper('art01', 'Art01') > 0, 'a numerically equivalent name of a different case compares uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesUpper), ['A2.txt', 'A100.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences group by case then compare by number'); + + }); + + test('compareFileExtensionsUpper', () => { + + // + // Comparisons with the same result as compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUpper(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsUpper(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsUpper('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsUpper('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsUpper('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileExtensionsUpper('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsUpper('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsUpper('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsUpper('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsUpper('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsUpper('aggregate.go', 'aggregate_repo.go') > 0, 'when extensions are equal, compares the full filename'); + + // dotfile comparisons + assert(compareFileExtensionsUpper('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsUpper('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsUpper(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsUpper('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsUpper('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsUpper('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsUpper('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsUpper('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUpper('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsUpper('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsUpper('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUpper('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsUpper('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUpper('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsUpper('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + assert(compareFileExtensionsUpper('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsUpper('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsUpper('a.ext1', 'b.Ext1') < 0, 'different names and extensions that are equal except for case are sorted in full filename order'); + assert(compareFileExtensionsUpper('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'b.Ext1'), 'same names and extensions that are equal except for case are sorted in full filename locale order'); + + // + // Comparisons with different results than compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUpper('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileExtensionsUpper('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileExtensionsUpper('รข', 'ร‚') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsUpper), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase names first'); + assert.deepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileExtensionsUpper), ['Email', 'ร‰mail', 'email', 'รฉmail'], 'the same base characters with different case or accents sort uppercase names first'); + + // name plus extension comparisons + assert(compareFileExtensionsUpper('a.md', 'A.md') > 0, 'case differences in names sort uppercase first'); + assert(compareFileExtensionsUpper('art01', 'Art01') > 0, 'a numerically equivalent word of a different case sorts uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsUpper), ['A2.txt', 'A100.txt', 'a10.txt', 'a20.txt',], 'filenames with number and case differences group by case then sort by number'); + + }); + + test('compareFileNamesLower', () => { + + // + // Comparisons with the same results as compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesLower(null, null) === 0, 'null should be equal'); + assert(compareFileNamesLower(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesLower('', '') === 0, 'empty should be equal'); + assert(compareFileNamesLower('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesLower('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons + assert(compareFileNamesLower('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesLower('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesLower('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesLower('aggregate.go', 'aggregate_repo.go') > 0, 'compares full filenames'); + + // dotfile comparisons + assert(compareFileNamesLower('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesLower('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesLower('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesLower(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesLower('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesLower('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesLower('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesLower('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesLower('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesLower('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesLower('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesLower('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesLower('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesLower('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesLower('a.ext1', 'b.Ext1') < 0, 'different names and extensions that are equal except for case are sorted in full filename order'); + assert(compareFileNamesLower('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'b.Ext1'), 'same names and extensions that are equal except for case are sorted in full filename locale order'); + + // + // Comparisons with different results than compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesLower('z', 'A') < 0, 'z comes before A'); + assert(compareFileNamesLower('a', 'A') < 0, 'the same letter sorts lowercase first'); + assert(compareFileNamesLower('รข', 'ร‚') < 0, 'the same accented letter sorts lowercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesLower), ['art', 'artichoke', 'Art', 'Artichoke'], 'names with the same root and different cases sort lowercase first'); + assert.deepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileNamesLower), ['email', 'รฉmail', 'Email', 'ร‰mail'], 'the same base characters with different case or accents sort lowercase first'); + + // numeric comparisons + assert(compareFileNamesLower('art01', 'Art01') < 0, 'a numerically equivalent name of a different case compares lowercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesLower), ['a10.txt', 'a20.txt', 'A2.txt', 'A100.txt'], 'filenames with number and case differences group by case then compare by number'); + + }); + + test('compareFileExtensionsLower', () => { + + // + // Comparisons with the same result as compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsLower(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsLower(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsLower('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsLower('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsLower('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons + assert(compareFileExtensionsLower('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsLower('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsLower('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsLower('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsLower('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + + // dotfile comparisons + assert(compareFileExtensionsLower('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsLower('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsLower(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsLower('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsLower('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsLower('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsLower('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsLower('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsLower('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsLower('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsLower('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsLower('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsLower('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsLower('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsLower('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsLower('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + assert(compareFileExtensionsLower('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsLower('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsLower('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsLower('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, full filenames should be compared'); + assert(compareFileExtensionsLower('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'if extensions with numbers are equal except for case, filenames are sorted in locale order'); + + // + // Comparisons with different results than compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsLower('z', 'A') < 0, 'z comes before A'); + assert(compareFileExtensionsLower('a', 'A') < 0, 'the same letter sorts lowercase first'); + assert(compareFileExtensionsLower('รข', 'ร‚') < 0, 'the same accented letter sorts lowercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsLower), ['art', 'artichoke', 'Art', 'Artichoke'], 'names with the same root and different cases sort lowercase names first'); + assert.deepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileExtensionsLower), ['email', 'รฉmail', 'Email', 'ร‰mail'], 'the same base characters with different case or accents sort lowercase names first'); + + // name plus extension comparisons + assert(compareFileExtensionsLower('a.md', 'A.md') < 0, 'case differences in names sort lowercase first'); + assert(compareFileExtensionsLower('art01', 'Art01') < 0, 'a numerically equivalent word of a different case sorts lowercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsLower), ['a10.txt', 'a20.txt', 'A2.txt', 'A100.txt'], 'filenames with number and case differences group by case then sort by number'); + assert(compareFileExtensionsLower('aggregate.go', 'aggregate_repo.go') > 0, 'when extensions are equal, compares full filenames'); + + }); + + test('compareFileNamesUnicode', () => { + + // + // Comparisons with the same results as compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUnicode(null, null) === 0, 'null should be equal'); + assert(compareFileNamesUnicode(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesUnicode('', '') === 0, 'empty should be equal'); + assert(compareFileNamesUnicode('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesUnicode('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileNamesUnicode('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesUnicode('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesUnicode('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesUnicode('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + + // dotfile comparisons + assert(compareFileNamesUnicode('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesUnicode('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesUnicode('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesUnicode('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesUnicode(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesUnicode('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesUnicode('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesUnicode('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesUnicode('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesUnicode('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesUnicode('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesUnicode('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesUnicode('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted by unicode full filename'); + assert(compareFileNamesUnicode('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted by unicode full filename'); + + // + // Comparisons with different results than compareFileNamesDefault + // + + // name-only comparisons + assert(compareFileNamesUnicode('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileNamesUnicode('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileNamesUnicode('รข', 'ร‚') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesUnicode), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase first'); + assert.deepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileNamesUnicode), ['Email', 'email', 'ร‰mail', 'รฉmail'], 'the same base characters with different case or accents sort in unicode order'); + + // name plus extension comparisons + assert(compareFileNamesUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'compares the whole name in unicode order, but dot comes before underscore'); + + // dotfile comparisons + assert(compareFileNamesUnicode('.aaa_env', '.aaa.env') > 0, 'an underscore in a dotfile name will sort after a dot'); + + // numeric comparisons + assert(compareFileNamesUnicode('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in unicode order even when they are multiple digits long'); + assert(compareFileNamesUnicode('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort in unicode order'); + assert(compareFileNamesUnicode('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between them are sorted in unicode order'); + assert(compareFileNamesUnicode('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileNamesUnicode('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); + assert(compareFileNamesUnicode('art01', 'Art01') > 0, 'a numerically equivalent name of a different case compares uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesUnicode), ['A100.txt', 'A2.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences sort in unicode order'); + + }); + + test('compareFileExtensionsUnicode', () => { + + // + // Comparisons with the same result as compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUnicode(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsUnicode(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsUnicode('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsUnicode('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsUnicode('z', 'A') > 0, 'z comes after A'); + + // name plus extension comparisons + assert(compareFileExtensionsUnicode('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsUnicode('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsUnicode('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsUnicode('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsUnicode('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsUnicode('a.MD', 'a.md') < 0, 'case differences in extensions sort in unicode order'); + + // dotfile comparisons + assert(compareFileExtensionsUnicode('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsUnicode('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsUnicode('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsUnicode('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsUnicode(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsUnicode('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsUnicode('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsUnicode('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsUnicode('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsUnicode('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsUnicode('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsUnicode('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUnicode('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsUnicode('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsUnicode('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared'); + + // + // Comparisons with different results than compareFileExtensionsDefault + // + + // name-only comparisons + assert(compareFileExtensionsUnicode('Z', 'a') < 0, 'Z comes before a'); + assert(compareFileExtensionsUnicode('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileExtensionsUnicode('รข', 'ร‚') > 0, 'the same accented letter sorts uppercase first'); + assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsUnicode), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase names first'); + assert.deepStrictEqual(['email', 'Email', 'รฉmail', 'ร‰mail'].sort(compareFileExtensionsUnicode), ['Email', 'email', 'ร‰mail', 'รฉmail'], 'the same base characters with different case or accents sort in unicode order'); + + // name plus extension comparisons + assert(compareFileExtensionsUnicode('a.MD', 'a.md') < 0, 'case differences in extensions sort by uppercase extension first'); + assert(compareFileExtensionsUnicode('a.md', 'A.md') > 0, 'case differences in names sort uppercase first'); + assert(compareFileExtensionsUnicode('art01', 'Art01') > 0, 'a numerically equivalent name of a different case sorts uppercase first'); + assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsUnicode), ['A100.txt', 'A2.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences sort in unicode order'); + assert(compareFileExtensionsUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, compares full filenames in unicode order'); + + // numeric comparisons + assert(compareFileExtensionsUnicode('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in unicode order'); + assert(compareFileExtensionsUnicode('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort in unicode order'); + assert(compareFileExtensionsUnicode('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between them sort in unicode order'); + assert(compareFileExtensionsUnicode('abc2.txt2', 'abc1.txt10') > 0, 'extensions with numbers should be in unicode order'); + assert(compareFileExtensionsUnicode('txt.abc2', 'txt.abc10') > 0, 'extensions with numbers should be in unicode order even when they are multiple digits long'); + assert(compareFileExtensionsUnicode('abc.txt01', 'abc.txt1') < 0, 'extensions with equal numbers should be in unicode order'); + assert(compareFileExtensionsUnicode('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileExtensionsUnicode('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order'); + assert(compareFileExtensionsUnicode('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, unicode full filenames should be compared'); + assert(compareFileExtensionsUnicode('a.ext1', 'a.Ext1') > 0, 'if extensions with numbers are equal except for case, unicode full filenames should be compared'); + + }); + }); diff --git a/lib/vscode/src/vs/base/test/common/arrays.test.ts b/lib/vscode/src/vs/base/test/common/arrays.test.ts index e30a841551c8..08afd92a2b46 100644 --- a/lib/vscode/src/vs/base/test/common/arrays.test.ts +++ b/lib/vscode/src/vs/base/test/common/arrays.test.ts @@ -295,4 +295,20 @@ suite('Arrays', () => { remove(); assert.strictEqual(array.length, 0); }); + + test('minIndex', () => { + const array = ['a', 'b', 'c']; + assert.strictEqual(arrays.minIndex(array, value => array.indexOf(value)), 0); + assert.strictEqual(arrays.minIndex(array, value => -array.indexOf(value)), 2); + assert.strictEqual(arrays.minIndex(array, _value => 0), 0); + assert.strictEqual(arrays.minIndex(array, value => value === 'b' ? 0 : 5), 1); + }); + + test('maxIndex', () => { + const array = ['a', 'b', 'c']; + assert.strictEqual(arrays.maxIndex(array, value => array.indexOf(value)), 2); + assert.strictEqual(arrays.maxIndex(array, value => -array.indexOf(value)), 0); + assert.strictEqual(arrays.maxIndex(array, _value => 0), 0); + assert.strictEqual(arrays.maxIndex(array, value => value === 'b' ? 5 : 0), 1); + }); }); diff --git a/lib/vscode/src/vs/base/test/common/collections.test.ts b/lib/vscode/src/vs/base/test/common/collections.test.ts index 881525a006bb..a86eeab2de04 100644 --- a/lib/vscode/src/vs/base/test/common/collections.test.ts +++ b/lib/vscode/src/vs/base/test/common/collections.test.ts @@ -53,26 +53,4 @@ suite('Collections', () => { assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2][0].value, value3); }); - - test('groupByNumber', () => { - - const group1 = 1, group2 = 2; - const value1 = 'a', value2 = 'b', value3 = 'c'; - let source = [ - { key: group1, value: value1 }, - { key: group1, value: value2 }, - { key: group2, value: value3 }, - ]; - - let grouped = collections.groupByNumber(source, x => x.key); - - // Group 1 - assert.strictEqual(grouped.get(group1)!.length, 2); - assert.strictEqual(grouped.get(group1)![0].value, value1); - assert.strictEqual(grouped.get(group1)![1].value, value2); - - // Group 2 - assert.strictEqual(grouped.get(group2)!.length, 1); - assert.strictEqual(grouped.get(group2)![0].value, value3); - }); }); diff --git a/lib/vscode/src/vs/base/test/common/decorators.test.ts b/lib/vscode/src/vs/base/test/common/decorators.test.ts index 0e88e4f2ec09..873003106507 100644 --- a/lib/vscode/src/vs/base/test/common/decorators.test.ts +++ b/lib/vscode/src/vs/base/test/common/decorators.test.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; -import { memoize, createMemoizer, throttle } from 'vs/base/common/decorators'; +import { memoize, throttle } from 'vs/base/common/decorators'; suite('Decorators', () => { test('memoize should memoize methods', () => { @@ -131,28 +131,6 @@ suite('Decorators', () => { } }); - test('memoize clear', () => { - const memoizer = createMemoizer(); - let counter = 0; - class Foo { - @memoizer - get answer() { - return ++counter; - } - } - - const foo = new Foo(); - assert.strictEqual(foo.answer, 1); - assert.strictEqual(foo.answer, 1); - memoizer.clear(); - assert.strictEqual(foo.answer, 2); - assert.strictEqual(foo.answer, 2); - memoizer.clear(); - assert.strictEqual(foo.answer, 3); - assert.strictEqual(foo.answer, 3); - assert.strictEqual(foo.answer, 3); - }); - test('throttle', () => { const spy = sinon.spy(); const clock = sinon.useFakeTimers(); diff --git a/lib/vscode/src/vs/base/test/common/event.test.ts b/lib/vscode/src/vs/base/test/common/event.test.ts index a0f55cd405c1..680c4aa2804d 100644 --- a/lib/vscode/src/vs/base/test/common/event.test.ts +++ b/lib/vscode/src/vs/base/test/common/event.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Event, Emitter, EventBufferer, EventMultiplexer, PauseableEmitter } from 'vs/base/common/event'; +import { Event, Emitter, EventBufferer, EventMultiplexer, PauseableEmitter, Relay } from 'vs/base/common/event'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { AsyncEmitter, IWaitUntil, timeout } from 'vs/base/common/async'; @@ -894,4 +894,70 @@ suite('Event utils', () => { listener.dispose(); }); + test('dispose is reentrant', () => { + const emitter = new Emitter({ + onLastListenerRemove: () => { + emitter.dispose(); + } + }); + + const listener = emitter.event(() => undefined); + listener.dispose(); // should not crash + }); + + suite('Relay', () => { + test('should input work', () => { + const e1 = new Emitter(); + const e2 = new Emitter(); + const relay = new Relay(); + + const result: number[] = []; + const listener = (num: number) => result.push(num); + const subscription = relay.event(listener); + + e1.fire(1); + assert.deepStrictEqual(result, []); + + relay.input = e1.event; + e1.fire(2); + assert.deepStrictEqual(result, [2]); + + relay.input = e2.event; + e1.fire(3); + e2.fire(4); + assert.deepStrictEqual(result, [2, 4]); + + subscription.dispose(); + e1.fire(5); + e2.fire(6); + assert.deepStrictEqual(result, [2, 4]); + }); + + test('should Relay dispose work', () => { + const e1 = new Emitter(); + const e2 = new Emitter(); + const relay = new Relay(); + + const result: number[] = []; + const listener = (num: number) => result.push(num); + relay.event(listener); + + e1.fire(1); + assert.deepStrictEqual(result, []); + + relay.input = e1.event; + e1.fire(2); + assert.deepStrictEqual(result, [2]); + + relay.input = e2.event; + e1.fire(3); + e2.fire(4); + assert.deepStrictEqual(result, [2, 4]); + + relay.dispose(); + e1.fire(5); + e2.fire(6); + assert.deepStrictEqual(result, [2, 4]); + }); + }); }); diff --git a/lib/vscode/src/vs/base/test/common/mime.test.ts b/lib/vscode/src/vs/base/test/common/mime.test.ts index f2583fed404c..05515ed1e59b 100644 --- a/lib/vscode/src/vs/base/test/common/mime.test.ts +++ b/lib/vscode/src/vs/base/test/common/mime.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { guessMimeTypes, registerTextMime } from 'vs/base/common/mime'; +import { guessMimeTypes, normalizeMimeType, registerTextMime } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; suite('Mime', () => { @@ -126,4 +126,13 @@ suite('Mime', () => { assert.deepStrictEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); + + test('normalize', () => { + assert.strictEqual(normalizeMimeType('invalid'), 'invalid'); + assert.strictEqual(normalizeMimeType('invalid', true), undefined); + assert.strictEqual(normalizeMimeType('Text/plain'), 'text/plain'); + assert.strictEqual(normalizeMimeType('Text/plรคin'), 'text/plรคin'); + assert.strictEqual(normalizeMimeType('Text/plain;UPPER'), 'text/plain;UPPER'); + assert.strictEqual(normalizeMimeType('Text/plain;lower'), 'text/plain;lower'); + }); }); diff --git a/lib/vscode/src/vs/base/test/common/stream.test.ts b/lib/vscode/src/vs/base/test/common/stream.test.ts index 4cb309a84ea6..913cf6e08172 100644 --- a/lib/vscode/src/vs/base/test/common/stream.test.ts +++ b/lib/vscode/src/vs/base/test/common/stream.test.ts @@ -91,7 +91,7 @@ suite('Stream', () => { }); test('WriteableStream - end with error works', async () => { - const reducer = (errors: Error[]) => errors.length > 0 ? errors[0] : null as unknown as Error; + const reducer = (errors: Error[]) => errors[0]; const stream = newWriteableStream(reducer); stream.end(new Error('error')); diff --git a/lib/vscode/src/vs/base/test/node/crypto.test.ts b/lib/vscode/src/vs/base/test/node/crypto.test.ts index 67c188de8f05..f1b7ef70b948 100644 --- a/lib/vscode/src/vs/base/test/node/crypto.test.ts +++ b/lib/vscode/src/vs/base/test/node/crypto.test.ts @@ -6,8 +6,7 @@ import { checksum } from 'vs/base/node/crypto'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { rimraf, writeFile } from 'vs/base/node/pfs'; +import { Promises, rimraf, writeFile } from 'vs/base/node/pfs'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; flakySuite('Crypto', () => { @@ -17,7 +16,7 @@ flakySuite('Crypto', () => { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(function () { diff --git a/lib/vscode/src/vs/base/test/node/extpath.test.ts b/lib/vscode/src/vs/base/test/node/extpath.test.ts index a1c8a7f94d23..0487ffcaebf2 100644 --- a/lib/vscode/src/vs/base/test/node/extpath.test.ts +++ b/lib/vscode/src/vs/base/test/node/extpath.test.ts @@ -5,8 +5,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises, rimraf } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -16,7 +15,7 @@ flakySuite('Extpath', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/lib/vscode/src/vs/base/test/node/pfs/pfs.test.ts b/lib/vscode/src/vs/base/test/node/pfs/pfs.test.ts index 0eceb128b9af..2139f6cbe44e 100644 --- a/lib/vscode/src/vs/base/test/node/pfs/pfs.test.ts +++ b/lib/vscode/src/vs/base/test/node/pfs/pfs.test.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; import { join, sep } from 'vs/base/common/path'; import { generateUuid } from 'vs/base/common/uuid'; -import { copy, exists, move, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; +import { copy, exists, move, Promises, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -22,7 +22,7 @@ flakySuite('PFS', function () { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); - return fs.promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { @@ -36,7 +36,7 @@ flakySuite('PFS', function () { await writeFile(testFile, 'Hello World', (null!)); - assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World'); + assert.strictEqual((await Promises.readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { @@ -200,7 +200,7 @@ flakySuite('PFS', function () { const id3 = generateUuid(); const copyTarget = join(testDir, id3); - await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); + await Promises.mkdir(symbolicLinkTarget, { recursive: true }); fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); @@ -217,7 +217,7 @@ flakySuite('PFS', function () { assert.ok(symbolicLink); assert.ok(!symbolicLink.dangling); - const target = await fs.promises.readlink(copyTarget); + const target = await Promises.readlink(copyTarget); assert.strictEqual(target, symbolicLinkTarget); // Copy does not preserve symlinks if configured as such @@ -253,7 +253,7 @@ flakySuite('PFS', function () { const sourceLinkTestFolder = join(sourceFolder, 'link-test'); // copy-test/link-test const sourceLinkMD5JSFolder = join(sourceLinkTestFolder, 'md5'); // copy-test/link-test/md5 const sourceLinkMD5JSFile = join(sourceLinkMD5JSFolder, 'md5.js'); // copy-test/link-test/md5/md5.js - await fs.promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); + await Promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); await writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked @@ -278,10 +278,10 @@ flakySuite('PFS', function () { assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); - const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); + const linkTarget = await Promises.readlink(targetLinkMD5JSFolderLinked); assert.strictEqual(linkTarget, targetLinkMD5JSFolder); - await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + await Promises.rmdir(targetLinkTestFolder, { recursive: true }); } // Copy with `preserveSymlinks: false` and verify result @@ -315,7 +315,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await fs.promises.mkdir(directory, { recursive: true }); + await Promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -334,7 +334,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await fs.promises.mkdir(directory, { recursive: true }); + await Promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -350,7 +350,7 @@ flakySuite('PFS', function () { const id = generateUuid(); const newDir = join(testDir, 'pfs', id, 'รถรครผ'); - await fs.promises.mkdir(newDir, { recursive: true }); + await Promises.mkdir(newDir, { recursive: true }); assert.ok(fs.existsSync(newDir)); @@ -362,7 +362,7 @@ flakySuite('PFS', function () { test('readdir (with file types)', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { const newDir = join(testDir, 'รถรครผ'); - await fs.promises.mkdir(newDir, { recursive: true }); + await Promises.mkdir(newDir, { recursive: true }); await writeFile(join(testDir, 'somefile.txt'), 'contents'); diff --git a/lib/vscode/src/vs/base/test/node/zip/zip.test.ts b/lib/vscode/src/vs/base/test/node/zip/zip.test.ts index 4f151af16e95..5e5470ce155b 100644 --- a/lib/vscode/src/vs/base/test/node/zip/zip.test.ts +++ b/lib/vscode/src/vs/base/test/node/zip/zip.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { extract } from 'vs/base/node/zip'; -import { rimraf, exists } from 'vs/base/node/pfs'; +import { rimraf, exists, Promises } from 'vs/base/node/pfs'; import { createCancelablePromise } from 'vs/base/common/async'; import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils'; @@ -19,7 +18,7 @@ suite('Zip', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/lib/vscode/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts b/lib/vscode/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts new file mode 100644 index 000000000000..f74c90e013a3 --- /dev/null +++ b/lib/vscode/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; +import { IWorkbenchListOptions } from 'vs/platform/list/browser/listService'; + +// Simple promisify of setTimeout +function wait(delayMS: number) { + return new Promise(function (resolve) { + setTimeout(resolve, delayMS); + }); +} + +suite('QuickInput', () => { + let fixture: HTMLElement, controller: QuickInputController; + + setup(() => { + fixture = document.createElement('div'); + document.body.appendChild(fixture); + + controller = new QuickInputController({ + container: fixture, + idPrefix: 'testQuickInput', + ignoreFocusOut() { return false; }, + isScreenReaderOptimized() { return false; }, + returnFocus() { }, + backKeybindingLabel() { return undefined; }, + setContextKey() { return undefined; }, + createList: ( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + options: IWorkbenchListOptions, + ) => new List(user, container, delegate, renderers, options), + styles: { + button: {}, + countBadge: {}, + inputBox: {}, + keybindingLabel: {}, + list: {}, + progressBar: {}, + widget: {} + } + }); + }); + + teardown(() => { + controller.dispose(); + document.body.removeChild(fixture); + }); + + test('onDidChangeValue gets triggered when .value is set', async () => { + const quickpick = controller.createQuickPick(); + + let value: string | undefined = undefined; + quickpick.onDidChangeValue((e) => value = e); + + // Trigger a change + quickpick.value = 'changed'; + + try { + // wait a bit to let the event play out. + await wait(200); + assert.strictEqual(value, quickpick.value); + } finally { + quickpick.dispose(); + } + }); +}); diff --git a/lib/vscode/src/vs/code/browser/workbench/workbench.ts b/lib/vscode/src/vs/code/browser/workbench/workbench.ts index e525a92725e9..789066d6a6a3 100644 --- a/lib/vscode/src/vs/code/browser/workbench/workbench.ts +++ b/lib/vscode/src/vs/code/browser/workbench/workbench.ts @@ -17,7 +17,6 @@ import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; import product from 'vs/platform/product/common/product'; -import { encodePath } from 'vs/server/node/util'; function doCreateUri(path: string, queryValues: Map): URI { let query: string | undefined = undefined; @@ -37,6 +36,15 @@ function doCreateUri(path: string, queryValues: Map): URI { return URI.parse(window.location.href).with({ path, query }); } +/** + * NOTE@coder: Add this function. + * Encode a path for opening via the folder or workspace query parameter. This + * preserves slashes so it can be edited by hand more easily. + */ + export const encodePath = (path: string): string => { + return path.split('/').map((p) => encodeURIComponent(p)).join('/'); +}; + interface ICredential { service: string; account: string; @@ -278,8 +286,8 @@ class WorkspaceProvider implements IWorkspaceProvider { readonly trusted = true; constructor( - public readonly workspace: IWorkspace, - public readonly payload: object + readonly workspace: IWorkspace, + readonly payload: object ) { } async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { @@ -409,7 +417,6 @@ class WindowIndicator implements IWindowIndicator { } (function () { - // Find config by checking for DOM const configElement = document.getElementById('vscode-workbench-web-configuration'); const configElementAttribute = configElement ? configElement.getAttribute('data-settings') : undefined; diff --git a/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 00df213a37cf..b76d76688020 100644 --- a/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -55,7 +54,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { this._logService.info('Starting to clean up unused language packs.'); try { const installed: IStringDictionary = Object.create(null); - const metaData: LanguagePackFile = JSON.parse(await fs.promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: LanguagePackFile = JSON.parse(await pfs.Promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); for (let locale of Object.keys(metaData)) { const entry = metaData[locale]; installed[`${entry.hash}.${locale}`] = true; @@ -83,7 +82,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { continue; } const candidate = path.join(folder, entry); - const stat = await fs.promises.stat(candidate); + const stat = await pfs.Promises.stat(candidate); if (stat.isDirectory()) { const diff = now - stat.mtime.getTime(); if (diff > this._DataMaxAge) { diff --git a/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts b/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts index fdb9aa7bb40c..a6afc6976170 100644 --- a/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts +++ b/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; import { basename, dirname, join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { readdir, rimraf } from 'vs/base/node/pfs'; +import { Promises, readdir, rimraf } from 'vs/base/node/pfs'; import { IProductService } from 'vs/platform/product/common/productService'; export class NodeCachedDataCleaner { @@ -56,7 +55,7 @@ export class NodeCachedDataCleaner { if (entry !== nodeCachedDataCurrent) { const path = join(nodeCachedDataRootDir, entry); - deletes.push(promises.stat(path).then(stats => { + deletes.push(Promises.stat(path).then(stats => { // stat check // * only directories // * only when old enough diff --git a/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index fc26cb229c3c..120557549707 100644 --- a/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/lib/vscode/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/path'; -import { readdir, rimraf } from 'vs/base/node/pfs'; +import { Promises, readdir, rimraf } from 'vs/base/node/pfs'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup'; @@ -33,7 +32,7 @@ export class StorageDataCleaner extends Disposable { try { // Leverage the backup workspace file to find out which empty workspace is currently in use to // determine which empty workspace storage can safely be deleted - const contents = await promises.readFile(this.backupWorkspacesPath, 'utf8'); + const contents = await Promises.readFile(this.backupWorkspacesPath, 'utf8'); const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat; const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(info => info.backupFolder); diff --git a/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 6c6e3f8cf5e0..c9651466d39e 100644 --- a/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -46,10 +46,7 @@ * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean - * }, - * canModifyDOM?: (config: ISandboxConfiguration) => void, - * beforeLoaderConfig?: (loaderConfig: object) => void, - * beforeRequire?: () => void + * } * } * ) => Promise * }} diff --git a/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 0f2a79cac7df..5b0ee538150f 100644 --- a/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/lib/vscode/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -77,7 +77,7 @@ import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/con import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { TerminalIpcChannels } from 'vs/platform/terminal/common/terminal'; +import { LocalReconnectConstants, TerminalIpcChannels } from 'vs/platform/terminal/common/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; @@ -272,7 +272,19 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); // Terminal - services.set(ILocalPtyService, this._register(new PtyHostService(logService, telemetryService))); + services.set( + ILocalPtyService, + this._register( + new PtyHostService({ + GraceTime: LocalReconnectConstants.GraceTime, + ShortGraceTime: LocalReconnectConstants.ShortGraceTime + }, + configurationService, + logService, + telemetryService + ) + ) + ); return new InstantiationService(services); } diff --git a/lib/vscode/src/vs/code/electron-browser/workbench/workbench.html b/lib/vscode/src/vs/code/electron-browser/workbench/workbench.html index 2933be273644..38405e41e15b 100644 --- a/lib/vscode/src/vs/code/electron-browser/workbench/workbench.html +++ b/lib/vscode/src/vs/code/electron-browser/workbench/workbench.html @@ -4,7 +4,7 @@ - + diff --git a/lib/vscode/src/vs/code/electron-browser/workbench/workbench.js b/lib/vscode/src/vs/code/electron-browser/workbench/workbench.js index 6a92a3c203e0..2d6fec36d122 100644 --- a/lib/vscode/src/vs/code/electron-browser/workbench/workbench.js +++ b/lib/vscode/src/vs/code/electron-browser/workbench/workbench.js @@ -43,7 +43,7 @@ }; }, canModifyDOM: function (windowConfig) { - showPartsSplash(windowConfig); + showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { loaderConfig.recordStats = true; @@ -89,19 +89,20 @@ /** * @typedef {import('../../../platform/windows/common/windows').INativeWindowConfiguration} INativeWindowConfiguration + * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration) => unknown, + * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { - * configureDeveloperSettings?: (config: INativeWindowConfiguration & object) => { + * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { * forceDisableShowDevtoolsOnError?: boolean, * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean * }, - * canModifyDOM?: (config: INativeWindowConfiguration & object) => void, + * canModifyDOM?: (config: INativeWindowConfiguration & NativeParsedArgs) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void * } @@ -114,28 +115,15 @@ } /** - * @param {{ - * partsSplashPath?: string, - * colorScheme: ('light' | 'dark' | 'hc'), - * autoDetectHighContrast?: boolean, - * extensionDevelopmentPath?: string[], - * workspace?: import('../../../platform/workspaces/common/workspaces').IWorkspaceIdentifier | import('../../../platform/workspaces/common/workspaces').ISingleFolderWorkspaceIdentifier - * }} configuration + * @param {INativeWindowConfiguration & NativeParsedArgs} configuration */ - function showPartsSplash(configuration) { + function showSplash(configuration) { performance.mark('code/willShowPartsSplash'); - let data; - if (typeof configuration.partsSplashPath === 'string') { - try { - data = JSON.parse(require.__$__nodeRequire('fs').readFileSync(configuration.partsSplashPath, 'utf8')); - } catch (e) { - // ignore - } - } + let data = configuration.partsSplash; // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts - const isHighContrast = configuration.colorScheme === 'hc' /* ColorScheme.HIGH_CONTRAST */ && configuration.autoDetectHighContrast; + const isHighContrast = configuration.colorScheme.highContrast && configuration.autoDetectHighContrast; if (data && isHighContrast && data.baseTheme !== 'hc-black') { data = undefined; } @@ -160,16 +148,18 @@ shellBackground = '#1E1E1E'; shellForeground = '#CCCCCC'; } + const style = document.createElement('style'); style.className = 'initialShellColors'; document.head.appendChild(style); style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; - if (data && data.layoutInfo) { - // restore parts if possible (we might not always store layout info) - const { id, layoutInfo, colorInfo } = data; + // restore parts if possible (we might not always store layout info) + if (data?.layoutInfo) { + const { layoutInfo, colorInfo } = data; + const splash = document.createElement('div'); - splash.id = id; + splash.id = 'monaco-parts-splash'; splash.className = baseTheme; if (layoutInfo.windowBorder) { @@ -198,8 +188,8 @@ splash.appendChild(activityDiv); // part: side bar (only when opening workspace/folder) + // folder or workspace -> status bar color, sidebar if (configuration.workspace) { - // folder or workspace -> status bar color, sidebar const sideDiv = document.createElement('div'); sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`); splash.appendChild(sideDiv); diff --git a/lib/vscode/src/vs/code/electron-main/app.ts b/lib/vscode/src/vs/code/electron-main/app.ts index 28fc931cd21f..630c577a6403 100644 --- a/lib/vscode/src/vs/code/electron-main/app.ts +++ b/lib/vscode/src/vs/code/electron-main/app.ts @@ -23,7 +23,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenURLOptions, IURLService } from 'vs/platform/url/common/url'; @@ -81,12 +81,14 @@ import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platfo import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { isEqualOrParent } from 'vs/base/common/extpath'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; import { once } from 'vs/base/common/functional'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { ISignService } from 'vs/platform/sign/common/sign'; +import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; /** * The main VS Code application. There will only ever be one instance, @@ -105,7 +107,7 @@ export class CodeApplication extends Disposable { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService ) { @@ -247,7 +249,7 @@ export class CodeApplication extends Disposable { //#endregion let macOpenFileURIs: IWindowOpenable[] = []; - let runningTimeout: NodeJS.Timeout | null = null; + let runningTimeout: NodeJS.Timeout | undefined = undefined; app.on('open-file', (event, path) => { this.logService.trace('app#open-file: ', path); event.preventDefault(); @@ -256,9 +258,9 @@ export class CodeApplication extends Disposable { macOpenFileURIs.push(this.getWindowOpenableFromPathSync(path)); // Clear previous handler if any - if (runningTimeout !== null) { + if (runningTimeout !== undefined) { clearTimeout(runningTimeout); - runningTimeout = null; + runningTimeout = undefined; } // Handle paths delayed in case more are coming! @@ -272,7 +274,7 @@ export class CodeApplication extends Disposable { }); macOpenFileURIs = []; - runningTimeout = null; + runningTimeout = undefined; }, 100); }); @@ -282,71 +284,29 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers - let slowShellResolveWarningShown = false; ipcMain.handle('vscode:fetchShellEnv', event => { - return new Promise(async resolve => { - // DO NOT remove: not only usual windows are fetching the - // shell environment but also shared process, issue reporter - // etc, so we need to reply via `webContents` always - const webContents = event.sender; - - let replied = false; - - function acceptShellEnv(env: IProcessEnvironment): void { - clearTimeout(shellEnvSlowWarningHandle); - clearTimeout(shellEnvTimeoutErrorHandle); - - if (!replied) { - replied = true; - - if (!webContents.isDestroyed()) { - resolve(env); - } - } - } - - // Handle slow shell environment resolve calls: - // - a warning after 3s but continue to resolve (only once in active window) - // - an error after 10s and stop trying to resolve (in every window where this happens) - const cts = new CancellationTokenSource(); - - const shellEnvSlowWarningHandle = setTimeout(() => { - if (!slowShellResolveWarningShown) { - this.windowsMainService?.sendToFocused('vscode:showShellEnvSlowWarning', cts.token); - slowShellResolveWarningShown = true; - } - }, 3000); - - const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process!! - const shellEnvTimeoutErrorHandle = setTimeout(() => { - cts.dispose(true); - window?.sendWhenReady('vscode:showShellEnvTimeoutError', CancellationToken.None); - acceptShellEnv({}); - }, 10000); - - // Prefer to use the args and env from the target window - // when resolving the shell env. It is possible that - // a first window was opened from the UI but a second - // from the CLI and that has implications for whether to - // resolve the shell environment or not. - // - // Window can be undefined for e.g. the shared process - // that is not part of our windows registry! - let args: NativeParsedArgs; - let env: IProcessEnvironment; - if (window?.config) { - args = window.config; - env = { ...process.env, ...window.config.userEnv }; - } else { - args = this.environmentMainService.args; - env = process.env; - } + // Prefer to use the args and env from the target window + // when resolving the shell env. It is possible that + // a first window was opened from the UI but a second + // from the CLI and that has implications for whether to + // resolve the shell environment or not. + // + // Window can be undefined for e.g. the shared process + // that is not part of our windows registry! + const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process + let args: NativeParsedArgs; + let env: IProcessEnvironment; + if (window?.config) { + args = window.config; + env = { ...process.env, ...window.config.userEnv }; + } else { + args = this.environmentMainService.args; + env = process.env; + } - // Resolve shell env - const shellEnv = await resolveShellEnv(this.logService, args, env); - acceptShellEnv(shellEnv); - }); + // Resolve shell env + return resolveShellEnv(this.logService, args, env); }); ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { @@ -498,11 +458,11 @@ export class CodeApplication extends Disposable { // We cache the machineId for faster lookups on startup // and resolve it only once initially if not cached or we need to replace the macOS iBridge device - let machineId = this.stateService.getItem(machineIdKey); + let machineId = this.stateMainService.getItem(machineIdKey); if (!machineId || (isMacintosh && machineId === '6c9d2bc8f91b89624add29c0abeae7fb42bf539fa1cdb2e3e57cd668fa9bcead')) { machineId = await getMachineId(); - this.stateService.setItem(machineIdKey, machineId); + this.stateMainService.setItem(machineIdKey, machineId); } return machineId; @@ -593,6 +553,15 @@ export class CodeApplication extends Disposable { // Storage services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); + // External terminal + if (isWindows) { + services.set(IExternalTerminalMainService, new SyncDescriptor(WindowsExternalTerminalService)); + } else if (isMacintosh) { + services.set(IExternalTerminalMainService, new SyncDescriptor(MacExternalTerminalService)); + } else if (isLinux) { + services.set(IExternalTerminalMainService, new SyncDescriptor(LinuxExternalTerminalService)); + } + // Backups const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService); services.set(IBackupMainService, backupMainService); @@ -679,6 +648,10 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); + // External Terminal + const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService)); + mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel); + // Log Level (main & shared process) const logLevelChannel = new LogLevelChannel(accessor.get(ILogService)); mainProcessElectronServer.registerChannel('logLevel', logLevelChannel); @@ -744,8 +717,16 @@ export class CodeApplication extends Disposable { // protocol invocations outside of VSCode. const app = this; const environmentService = this.environmentMainService; + const productService = this.productService; urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise { + if (uri.scheme === productService.urlProtocol && uri.path === 'workspace') { + uri = uri.with({ + authority: 'file', + path: URI.parse(uri.query).path, + query: '' + }); + } // If URI should be blocked, behave as if it's handled if (app.shouldBlockURI(uri)) { @@ -960,7 +941,7 @@ export class CodeApplication extends Disposable { if (typeof details === 'string') { message = details; } else { - message = `SharedProcess: crashed (detail: ${details.reason})`; + message = `SharedProcess: crashed (detail: ${details.reason}, code: ${details.exitCode})`; } onUnexpectedError(new Error(message)); @@ -1012,7 +993,16 @@ export class CodeApplication extends Disposable { } // Start to fetch shell environment (if needed) after window has opened - resolveShellEnv(this.logService, this.environmentMainService.args, process.env); + // Since this operation can take a long time, we want to warm it up while + // the window is opening. + // We also print a warning if the resolution takes longer than 10s. + (async () => { + const slowResolveShellEnvWarning = this._register(new RunOnceScheduler(() => this.logService.warn('Resolving your shell environment is taking more than 10s. Please review your shell configuration. Learn more at https://go.microsoft.com/fwlink/?linkid=2149667.'), 10000)); + slowResolveShellEnvWarning.schedule(); + + await resolveShellEnv(this.logService, this.environmentMainService.args, process.env); + slowResolveShellEnvWarning.dispose(); + })(); // If enable-crash-reporter argv is undefined then this is a fresh start, // based on telemetry.enableCrashreporter settings, generate a UUID which diff --git a/lib/vscode/src/vs/code/electron-main/main.ts b/lib/vscode/src/vs/code/electron-main/main.ts index b4650d50702c..8c9be0c67f0c 100644 --- a/lib/vscode/src/vs/code/electron-main/main.ts +++ b/lib/vscode/src/vs/code/electron-main/main.ts @@ -5,7 +5,8 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { promises, unlinkSync } from 'fs'; +import { unlinkSync } from 'fs'; +import { Promises as FSPromises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { mark } from 'vs/base/common/performance'; @@ -22,8 +23,8 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService, ConsoleMainLogger, MultiplexLogService, getLogLevel, ILoggerService } from 'vs/platform/log/common/log'; -import { StateService } from 'vs/platform/state/node/stateService'; -import { IStateService } from 'vs/platform/state/node/state'; +import { StateMainService } from 'vs/platform/state/electron-main/stateMainService'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; @@ -57,6 +58,7 @@ import { LoggerService } from 'vs/platform/log/node/loggerService'; import { cwd } from 'vs/base/common/process'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { ProtocolMainService } from 'vs/platform/protocol/electron-main/protocolMainService'; +import { Promises } from 'vs/base/common/async'; /** * The main VS Code entry point. @@ -84,17 +86,17 @@ class CodeMain { setUnexpectedErrorHandler(err => console.error(err)); // Create services - const [instantiationService, instanceEnvironment, environmentService, configurationService, stateService, bufferLogService, productService] = this.createServices(); + const [instantiationService, instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogService, productService] = this.createServices(); try { // Init services try { - await this.initServices(environmentService, configurationService, stateService); + await this.initServices(environmentMainService, configurationService, stateMainService); } catch (error) { // Show a dialog for errors that can be resolved by the user - this.handleStartupDataDirError(environmentService, productService.nameLong, error); + this.handleStartupDataDirError(environmentMainService, productService.nameLong, error); throw error; } @@ -108,10 +110,10 @@ class CodeMain { // Create the main IPC server by trying to be the server // If this throws an error it means we are not the first // instance of VS Code running and so we would quit. - const mainProcessNodeIpcServer = await this.claimInstance(logService, environmentService, lifecycleMainService, instantiationService, productService, true); + const mainProcessNodeIpcServer = await this.claimInstance(logService, environmentMainService, lifecycleMainService, instantiationService, productService, true); // Delay creation of spdlog for perf reasons (https://github.com/microsoft/vscode/issues/72906) - bufferLogService.logger = new SpdLogLogger('main', join(environmentService.logsPath, 'main.log'), true, bufferLogService.getLevel()); + bufferLogService.logger = new SpdLogLogger('main', join(environmentMainService.logsPath, 'main.log'), true, bufferLogService.getLevel()); // Lifecycle once(lifecycleMainService.onWillShutdown)(() => { @@ -126,7 +128,7 @@ class CodeMain { } } - private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, ConfigurationService, StateService, BufferLogService, IProductService] { + private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, ConfigurationService, StateMainService, BufferLogService, IProductService] { const services = new ServiceCollection(); // Product @@ -163,8 +165,8 @@ class CodeMain { services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService)); // State - const stateService = new StateService(environmentMainService, logService); - services.set(IStateService, stateService); + const stateMainService = new StateMainService(environmentMainService, logService, fileService); + services.set(IStateMainService, stateMainService); // Request services.set(IRequestService, new SyncDescriptor(RequestMainService)); @@ -181,7 +183,7 @@ class CodeMain { // Protocol services.set(IProtocolMainService, new SyncDescriptor(ProtocolMainService)); - return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, configurationService, stateService, bufferLogService, productService]; + return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogService, productService]; } private patchEnvironment(environmentMainService: IEnvironmentMainService): IProcessEnvironment { @@ -201,25 +203,25 @@ class CodeMain { return instanceEnvironment; } - private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateService: StateService): Promise { - - // Environment service (paths) - const environmentServiceInitialization = Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.nodeCachedDataDir, - environmentMainService.logsPath, - environmentMainService.globalStorageHome.fsPath, - environmentMainService.workspaceStorageHome.fsPath, - environmentMainService.backupHome - ].map(path => path ? promises.mkdir(path, { recursive: true }) : undefined)); - - // Configuration service - const configurationServiceInitialization = configurationService.initialize(); - - // State service - const stateServiceInitialization = stateService.init(); - - return Promise.all([environmentServiceInitialization, configurationServiceInitialization, stateServiceInitialization]); + private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateMainService: StateMainService): Promise { + return Promises.settled([ + + // Environment service (paths) + Promise.all([ + environmentMainService.extensionsPath, + environmentMainService.nodeCachedDataDir, + environmentMainService.logsPath, + environmentMainService.globalStorageHome.fsPath, + environmentMainService.workspaceStorageHome.fsPath, + environmentMainService.backupHome + ].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)), + + // Configuration service + configurationService.initialize(), + + // State service + stateMainService.init() + ]); } private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { diff --git a/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporter.js b/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporter.js index caa0b29f95b3..b486c703057b 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporter.js +++ b/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporter.js @@ -35,10 +35,7 @@ * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean - * }, - * canModifyDOM?: (config: ISandboxConfiguration) => void, - * beforeLoaderConfig?: (loaderConfig: object) => void, - * beforeRequire?: () => void + * } * } * ) => Promise * }} diff --git a/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index ad1abc732a4e..26c871d3449b 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -11,6 +11,7 @@ import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; +import { Delayer } from 'vs/base/common/async'; import { groupBy } from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -62,6 +63,7 @@ export class IssueReporter extends Disposable { private receivedPerformanceInfo = false; private shouldQueueSearch = false; private hasBeenSubmitted = false; + private delayedSubmit = new Delayer(300); private readonly previewButton!: Button; @@ -85,6 +87,7 @@ export class IssueReporter extends Disposable { const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { this.previewButton = new Button(issueReporterElement); + this.updatePreviewButtonState(); } const issueTitle = configuration.data.issueTitle; @@ -137,6 +140,7 @@ export class IssueReporter extends Disposable { this.applyStyles(configuration.data.styles); this.handleExtensionData(configuration.data.enabledExtensions); this.updateExperimentsInfo(configuration.data.experiments); + this.updateRestrictedMode(configuration.data.restrictedMode); } render(): void { @@ -355,7 +359,11 @@ export class IssueReporter extends Disposable { this.searchIssues(title, fileOnExtension, fileOnMarketplace); }); - this.previewButton.onDidClick(() => this.createIssue()); + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); function sendWorkbenchCommand(commandId: string) { ipcRenderer.send('vscode:workbenchCommand', { id: commandId, from: 'issueReporter' }); @@ -382,9 +390,11 @@ export class IssueReporter extends Disposable { const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window if (cmdOrCtrlKey && e.keyCode === 13) { - if (await this.createIssue()) { - ipcRenderer.send('vscode:closeIssueReporter'); - } + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + ipcRenderer.send('vscode:closeIssueReporter'); + } + }); } // Cmd/Ctrl + w closes issue window @@ -1150,6 +1160,10 @@ export class IssueReporter extends Disposable { } } + private updateRestrictedMode(restrictedMode: boolean) { + this.issueReporterModel.update({ restrictedMode }); + } + private updateExperimentsInfo(experimentInfo: string | undefined) { this.issueReporterModel.update({ experimentInfo }); const target = document.querySelector('.block-experiments .block-info'); diff --git a/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterModel.ts index 04a8ac509e53..63de037bc44b 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterModel.ts +++ b/lib/vscode/src/vs/code/electron-sandbox/issue/issueReporterModel.ts @@ -32,6 +32,7 @@ export interface IssueReporterData { query?: string; filterResultCount?: number; experimentInfo?: string; + restrictedMode?: boolean; } export class IssueReporterModel { @@ -67,6 +68,7 @@ ${this._data.issueDescription} ${this.getExtensionVersion()} VS Code version: ${this._data.versionInfo && this._data.versionInfo.vscodeVersion} OS version: ${this._data.versionInfo && this._data.versionInfo.os} +Restricted Mode: ${this._data.restrictedMode ? 'Yes' : 'No'} ${this.getRemoteOSes()} ${this.getInfos()} `; diff --git a/lib/vscode/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts b/lib/vscode/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts index 769fcd02ac8b..5e8cfb05f6e3 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts +++ b/lib/vscode/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts @@ -33,6 +33,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No Extensions: none `); @@ -63,6 +64,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
    System Info @@ -106,6 +108,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
    System Info @@ -160,6 +163,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
    System Info @@ -216,6 +220,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No Remote OS version: Linux x64 4.18.0
    @@ -264,6 +269,7 @@ undefined VS Code version: undefined OS version: undefined +Restricted Mode: No
    System Info diff --git a/lib/vscode/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css b/lib/vscode/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css index 095663c05d02..0129b721f773 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css +++ b/lib/vscode/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css @@ -49,6 +49,10 @@ body { width: 90px; } +.monaco-list:focus { + outline: 0; +} + .monaco-list-row:first-of-type { border-bottom: 1px solid; } diff --git a/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 7aeb6f35d806..8234b734d06c 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -32,10 +32,7 @@ * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean - * }, - * canModifyDOM?: (config: ISandboxConfiguration) => void, - * beforeLoaderConfig?: (loaderConfig: object) => void, - * beforeRequire?: () => void + * } * } * ) => Promise * }} diff --git a/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 546c167d93b0..3c2e84a386ad 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/lib/vscode/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -14,13 +14,15 @@ import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; -import { append, $ } from 'vs/base/browser/dom'; +import { append, $, createStyleSheet } from 'vs/base/browser/dom'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ByteSize } from 'vs/platform/files/common/files'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; +import { RunOnceScheduler } from 'vs/base/common/async'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; @@ -310,8 +312,7 @@ class ProcessExplorer { renderers, new ProcessTreeDataSource(), { - identityProvider: - { + identityProvider: { getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => { if (isProcessItem(element)) { return element.pid.toString(); @@ -331,7 +332,7 @@ class ProcessExplorer { return 'header'; } - } + }, }); this.tree.setInput({ processes: { processRoots } }); @@ -378,21 +379,45 @@ class ProcessExplorer { } private applyStyles(styles: ProcessExplorerStyles): void { - const styleTag = document.createElement('style'); + const styleElement = createStyleSheet(); const content: string[] = []; - if (styles.hoverBackground) { - content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`); + if (styles.listFocusBackground) { + content.push(`.monaco-list:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); + content.push(`.monaco-list:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); + } + + if (styles.listFocusForeground) { + content.push(`.monaco-list:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`); + } + + if (styles.listActiveSelectionBackground) { + content.push(`.monaco-list:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`); + content.push(`.monaco-list:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); + } + + if (styles.listActiveSelectionForeground) { + content.push(`.monaco-list:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`); + } + + if (styles.listHoverBackground) { + content.push(`.monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); } - if (styles.hoverForeground) { - content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`); + if (styles.listHoverForeground) { + content.push(`.monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); } - styleTag.textContent = content.join('\n'); - if (document.head) { - document.head.appendChild(styleTag); + if (styles.listFocusOutline) { + content.push(`.monaco-list:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }`); } + + if (styles.listHoverOutline) { + content.push(`.monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); + } + + styleElement.textContent = content.join('\n'); + if (styles.color) { document.body.style.color = styles.color; } @@ -475,9 +500,24 @@ class ProcessExplorer { } } +function createCodiconStyleSheet() { + const codiconStyleSheet = createStyleSheet(); + codiconStyleSheet.id = 'codiconStyles'; + + const iconsStyleSheet = getIconsStyleSheet(); + function updateAll() { + codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); + } + + const delayer = new RunOnceScheduler(updateAll, 0); + iconsStyleSheet.onDidChange(() => delayer.schedule()); + delayer.schedule(); +} + export function startup(configuration: ProcessExplorerWindowConfiguration): void { const platformClass = configuration.data.platform === 'win32' ? 'windows' : configuration.data.platform === 'linux' ? 'linux' : 'mac'; document.body.classList.add(platformClass); // used by our fonts + createCodiconStyleSheet(); applyZoom(configuration.data.zoomLevel); new ProcessExplorer(configuration.windowId, configuration.data); diff --git a/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.html b/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.html index 2933be273644..38405e41e15b 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.html @@ -4,7 +4,7 @@ - + diff --git a/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.js b/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.js index 38cd78a89b53..c42f3aadbfbc 100644 --- a/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/lib/vscode/src/vs/code/electron-sandbox/workbench/workbench.js @@ -43,7 +43,7 @@ }; }, canModifyDOM: function (windowConfig) { - // TODO@sandbox part-splash is non-sandboxed only + showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { loaderConfig.recordStats = true; @@ -89,18 +89,20 @@ /** * @typedef {import('../../../platform/windows/common/windows').INativeWindowConfiguration} INativeWindowConfiguration + * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration) => unknown, + * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { - * configureDeveloperSettings?: (config: INativeWindowConfiguration & object) => { + * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { + * forceDisableShowDevtoolsOnError?: boolean, * forceEnableDeveloperKeybindings?: boolean, * disallowReloadKeybinding?: boolean, * removeDeveloperKeybindingsAfterLoad?: boolean * }, - * canModifyDOM?: (config: INativeWindowConfiguration & object) => void, + * canModifyDOM?: (config: INativeWindowConfiguration & NativeParsedArgs) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void * } @@ -112,5 +114,97 @@ return window.MonacoBootstrapWindow; } + /** + * @param {INativeWindowConfiguration & NativeParsedArgs} configuration + */ + function showSplash(configuration) { + performance.mark('code/willShowPartsSplash'); + + let data = configuration.partsSplash; + + // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts + const isHighContrast = configuration.colorScheme.highContrast && configuration.autoDetectHighContrast; + if (data && isHighContrast && data.baseTheme !== 'hc-black') { + data = undefined; + } + + // developing an extension -> ignore stored layouts + if (data && configuration.extensionDevelopmentPath) { + data.layoutInfo = undefined; + } + + // minimal color configuration (works with or without persisted data) + let baseTheme, shellBackground, shellForeground; + if (data) { + baseTheme = data.baseTheme; + shellBackground = data.colorInfo.editorBackground; + shellForeground = data.colorInfo.foreground; + } else if (isHighContrast) { + baseTheme = 'hc-black'; + shellBackground = '#000000'; + shellForeground = '#FFFFFF'; + } else { + baseTheme = 'vs-dark'; + shellBackground = '#1E1E1E'; + shellForeground = '#CCCCCC'; + } + + const style = document.createElement('style'); + style.className = 'initialShellColors'; + document.head.appendChild(style); + style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; + + // restore parts if possible (we might not always store layout info) + if (data?.layoutInfo) { + const { layoutInfo, colorInfo } = data; + + const splash = document.createElement('div'); + splash.id = 'monaco-parts-splash'; + splash.className = baseTheme; + + if (layoutInfo.windowBorder) { + splash.style.position = 'relative'; + splash.style.height = 'calc(100vh - 2px)'; + splash.style.width = 'calc(100vw - 2px)'; + splash.style.border = '1px solid var(--window-border-color)'; + splash.style.setProperty('--window-border-color', colorInfo.windowBorder); + + if (layoutInfo.windowBorderRadius) { + splash.style.borderRadius = layoutInfo.windowBorderRadius; + } + } + + // ensure there is enough space + layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth)); + + // part: title + const titleDiv = document.createElement('div'); + titleDiv.setAttribute('style', `position: absolute; width: 100%; left: 0; top: 0; height: ${layoutInfo.titleBarHeight}px; background-color: ${colorInfo.titleBarBackground}; -webkit-app-region: drag;`); + splash.appendChild(titleDiv); + + // part: activity bar + const activityDiv = document.createElement('div'); + activityDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: 0; width: ${layoutInfo.activityBarWidth}px; background-color: ${colorInfo.activityBarBackground};`); + splash.appendChild(activityDiv); + + // part: side bar (only when opening workspace/folder) + // folder or workspace -> status bar color, sidebar + if (configuration.workspace) { + const sideDiv = document.createElement('div'); + sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`); + splash.appendChild(sideDiv); + } + + // part: statusbar + const statusDiv = document.createElement('div'); + statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); + splash.appendChild(statusDiv); + + document.body.appendChild(splash); + } + + performance.mark('code/didShowPartsSplash'); + } + //#endregion }()); diff --git a/lib/vscode/src/vs/code/node/cli.ts b/lib/vscode/src/vs/code/node/cli.ts index 0ce6c5ee5be4..169b50ae77c2 100644 --- a/lib/vscode/src/vs/code/node/cli.ts +++ b/lib/vscode/src/vs/code/node/cli.ts @@ -13,8 +13,9 @@ import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; import product from 'vs/platform/product/common/product'; import { isAbsolute, join } from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; -import { findFreePort, randomPort } from 'vs/base/node/ports'; -import { isWindows, isLinux, IProcessEnvironment } from 'vs/base/common/platform'; +import { findFreePort } from 'vs/base/node/ports'; +import { randomPort } from 'vs/base/common/ports'; +import { isLinux, isWindows, IProcessEnvironment } from 'vs/base/common/platform'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; diff --git a/lib/vscode/src/vs/code/node/cliProcessMain.ts b/lib/vscode/src/vs/code/node/cliProcessMain.ts index 11716b334bc9..ade2dd627372 100644 --- a/lib/vscode/src/vs/code/node/cliProcessMain.ts +++ b/lib/vscode/src/vs/code/node/cliProcessMain.ts @@ -6,6 +6,7 @@ import { release, hostname } from 'os'; import * as fs from 'fs'; import { gracefulify } from 'graceful-fs'; +import { Promises } from 'vs/base/node/pfs'; import { isAbsolute, join } from 'vs/base/common/path'; import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; @@ -19,7 +20,7 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme import { IExtensionManagementService, IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; @@ -28,8 +29,6 @@ import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { IStateService } from 'vs/platform/state/node/state'; -import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogger, MultiplexLogService, ILogger } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; @@ -104,7 +103,7 @@ class CliMain extends Disposable { services.set(INativeEnvironmentService, environmentService); // Init folders - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Log const logLevel = getLogLevel(environmentService); @@ -131,10 +130,6 @@ class CliMain extends Disposable { // Init config await configurationService.initialize(); - // State - const stateService = new StateService(environmentService, logService); - services.set(IStateService, stateService); - // Request services.set(IRequestService, new SyncDescriptor(RequestService)); @@ -158,7 +153,19 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, stateService.getItem('telemetry.machineId'), productService.msftInternalDomains, installSourcePath), + commonProperties: (async () => { + let machineId: string | undefined = undefined; + try { + const storageContents = await Promises.readFile(join(environmentService.userDataPath, 'storage.json')); + machineId = JSON.parse(storageContents.toString())[machineIdKey]; + } catch (error) { + if (error.code !== 'ENOENT') { + logService.error(error); + } + } + + return resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, machineId, productService.msftInternalDomains, installSourcePath); + })(), piiPaths: [appRoot, extensionsPath] }; diff --git a/lib/vscode/src/vs/editor/browser/controller/mouseTarget.ts b/lib/vscode/src/vs/editor/browser/controller/mouseTarget.ts index d2af49b222f6..3d766b78764d 100644 --- a/lib/vscode/src/vs/editor/browser/controller/mouseTarget.ts +++ b/lib/vscode/src/vs/editor/browser/controller/mouseTarget.ts @@ -40,54 +40,40 @@ export interface IEmptyContentData { horizontalDistanceToText?: number; } -interface IETextRange { - boundingHeight: number; - boundingLeft: number; - boundingTop: number; - boundingWidth: number; - htmlText: string; - offsetLeft: number; - offsetTop: number; - text: string; - collapse(start?: boolean): void; - compareEndPoints(how: string, sourceRange: IETextRange): number; - duplicate(): IETextRange; - execCommand(cmdID: string, showUI?: boolean, value?: any): boolean; - execCommandShowHelp(cmdID: string): boolean; - expand(Unit: string): boolean; - findText(string: string, count?: number, flags?: number): boolean; - getBookmark(): string; - getBoundingClientRect(): ClientRect; - getClientRects(): ClientRectList; - inRange(range: IETextRange): boolean; - isEqual(range: IETextRange): boolean; - move(unit: string, count?: number): number; - moveEnd(unit: string, count?: number): number; - moveStart(unit: string, count?: number): number; - moveToBookmark(bookmark: string): boolean; - moveToElementText(element: Element): void; - moveToPoint(x: number, y: number): void; - parentElement(): Element; - pasteHTML(html: string): void; - queryCommandEnabled(cmdID: string): boolean; - queryCommandIndeterm(cmdID: string): boolean; - queryCommandState(cmdID: string): boolean; - queryCommandSupported(cmdID: string): boolean; - queryCommandText(cmdID: string): string; - queryCommandValue(cmdID: string): any; - scrollIntoView(fStart?: boolean): void; - select(): void; - setEndPoint(how: string, SourceRange: IETextRange): void; +export interface ITextContentData { + mightBeForeignElement: boolean; } -declare const IETextRange: { - prototype: IETextRange; - new(): IETextRange; -}; +const enum HitTestResultType { + Unknown = 0, + Content = 1, +} -interface IHitTestResult { - position: Position | null; - hitTarget: Element | null; +class UnknownHitTestResult { + readonly type = HitTestResultType.Unknown; + constructor( + readonly hitTarget: Element | null = null + ) { } +} + +class ContentHitTestResult { + readonly type = HitTestResultType.Content; + constructor( + readonly position: Position, + readonly spanNode: HTMLElement + ) { } +} + +type HitTestResult = UnknownHitTestResult | ContentHitTestResult; + +namespace HitTestResult { + export function createFromDOMInfo(ctx: HitTestContext, spanNode: HTMLElement, offset: number): HitTestResult { + const position = ctx.getPositionFromDOMInfo(spanNode, offset); + if (position) { + return new ContentHitTestResult(position, spanNode); + } + return new UnknownHitTestResult(spanNode); + } } export class PointerHandlerLastRenderData { @@ -426,6 +412,17 @@ class HitTestRequest extends BareHitTestRequest { return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (this.target).outerHTML : null}`; } + public fulfill(type: MouseTargetType.UNKNOWN, position?: Position | null, range?: EditorRange | null): MouseTarget; + public fulfill(type: MouseTargetType.TEXTAREA, position: Position | null): MouseTarget; + public fulfill(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, position: Position, range: EditorRange, detail: IMarginData): MouseTarget; + public fulfill(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, position: Position, range: null, detail: IViewZoneData): MouseTarget; + public fulfill(type: MouseTargetType.CONTENT_TEXT, position: Position | null, range: EditorRange | null, detail: ITextContentData): MouseTarget; + public fulfill(type: MouseTargetType.CONTENT_EMPTY, position: Position | null, range: EditorRange | null, detail: IEmptyContentData): MouseTarget; + public fulfill(type: MouseTargetType.CONTENT_WIDGET, position: null, range: null, detail: string): MouseTarget; + public fulfill(type: MouseTargetType.SCROLLBAR, position: Position): MouseTarget; + public fulfill(type: MouseTargetType.OVERLAY_WIDGET, position: null, range: null, detail: string): MouseTarget; + // public fulfill(type: MouseTargetType.OVERVIEW_RULER, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget; + // public fulfill(type: MouseTargetType.OUTSIDE_EDITOR, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget; public fulfill(type: MouseTargetType, position: Position | null = null, range: EditorRange | null = null, detail: any = null): MouseTarget { let mouseColumn = this.mouseColumn; if (position && position.column < this._ctx.model.getLineMaxColumn(position.lineNumber)) { @@ -506,8 +503,8 @@ export class MouseTargetFactory { const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); - if (hitTestResult.position) { - return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); + if (hitTestResult.type === HitTestResultType.Content) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position); } return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); @@ -567,7 +564,7 @@ export class MouseTargetFactory { for (const d of lastViewCursorsRenderData) { if (request.target === d.domNode) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position); + return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position, null, { mightBeForeignElement: false }); } } } @@ -599,7 +596,7 @@ export class MouseTargetFactory { cursorVerticalOffset <= mouseVerticalOffset && mouseVerticalOffset <= cursorVerticalOffset + d.height ) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position); + return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position, null, { mightBeForeignElement: false }); } } } @@ -621,7 +618,7 @@ export class MouseTargetFactory { // Is it the textarea? if (ElementPath.isTextArea(request.targetPath)) { if (ctx.lastRenderData.lastTextareaPosition) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, ctx.lastRenderData.lastTextareaPosition); + return request.fulfill(MouseTargetType.CONTENT_TEXT, ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false }); } return request.fulfill(MouseTargetType.TEXTAREA, ctx.lastRenderData.lastTextareaPosition); } @@ -667,7 +664,7 @@ export class MouseTargetFactory { } if (ctx.isInTopPadding(request.mouseVerticalOffset)) { - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(1, 1), undefined, EMPTY_CONTENT_AFTER_LINES); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(1, 1), null, EMPTY_CONTENT_AFTER_LINES); } // Check if it is below any lines and any view zones @@ -675,7 +672,7 @@ export class MouseTargetFactory { // This most likely indicates it happened after the last view-line const lineCount = ctx.model.getLineCount(); const maxLineColumn = ctx.model.getLineMaxColumn(lineCount); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn), undefined, EMPTY_CONTENT_AFTER_LINES); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn), null, EMPTY_CONTENT_AFTER_LINES); } if (domHitTestExecuted) { @@ -686,14 +683,14 @@ export class MouseTargetFactory { if (ctx.model.getLineLength(lineNumber) === 0) { const lineWidth = ctx.getLineWidth(lineNumber); const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, 1), undefined, detail); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, 1), null, detail); } const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset >= lineWidth) { const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); const pos = new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, undefined, detail); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, null, detail); } } @@ -703,8 +700,8 @@ export class MouseTargetFactory { const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); - if (hitTestResult.position) { - return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); + if (hitTestResult.type === HitTestResultType.Content) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position); } return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); @@ -760,14 +757,15 @@ export class MouseTargetFactory { return (chars + 1); } - private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, lineNumber: number, column: number): MouseTarget { - const pos = new Position(lineNumber, column); + private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position): MouseTarget { + const lineNumber = pos.lineNumber; + const column = pos.column; const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset > lineWidth) { const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, undefined, detail); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, null, detail); } const visibleRange = ctx.visibleRangeForPosition(lineNumber, column); @@ -779,7 +777,7 @@ export class MouseTargetFactory { const columnHorizontalOffset = visibleRange.left; if (request.mouseContentHorizontalOffset === columnHorizontalOffset) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, null, { mightBeForeignElement: false }); } // Let's define a, b, c and check if the offset is in between them... @@ -803,21 +801,25 @@ export class MouseTargetFactory { points.sort((a, b) => a.offset - b.offset); + const mouseCoordinates = request.pos.toClientCoordinates(); + const spanNodeClientRect = spanNode.getBoundingClientRect(); + const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right); + for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) { const rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column); - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode }); } } - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, null, { mightBeForeignElement: !mouseIsOverSpanNode }); } /** * Most probably WebKit browsers and Edge */ - private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult { + private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { // In Chrome, especially on Linux it is possible to click between lines, // so try to adjust the `hity` below so that it lands in the center of a line @@ -836,7 +838,7 @@ export class MouseTargetFactory { const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY); const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates()); - if (r.position) { + if (r.type === HitTestResultType.Content) { return r; } @@ -844,7 +846,7 @@ export class MouseTargetFactory { return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates()); } - private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult { const shadowRoot = dom.getShadowRoot(ctx.viewDomNode); let range: Range; if (shadowRoot) { @@ -858,15 +860,11 @@ export class MouseTargetFactory { } if (!range || !range.startContainer) { - return { - position: null, - hitTarget: null - }; + return new UnknownHitTestResult(); } // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span const startContainer = range.startContainer; - let hitTarget: HTMLElement | null = null; if (startContainer.nodeType === startContainer.TEXT_NODE) { // startContainer is expected to be the token text @@ -876,13 +874,9 @@ export class MouseTargetFactory { const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; if (parent3ClassName === ViewLine.CLASS_NAME) { - const p = ctx.getPositionFromDOMInfo(parent1, range.startOffset); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, parent1, range.startOffset); } else { - hitTarget = startContainer.parentNode; + return new UnknownHitTestResult(startContainer.parentNode); } } else if (startContainer.nodeType === startContainer.ELEMENT_NODE) { // startContainer is expected to be the token span @@ -891,26 +885,19 @@ export class MouseTargetFactory { const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; if (parent2ClassName === ViewLine.CLASS_NAME) { - const p = ctx.getPositionFromDOMInfo(startContainer, (startContainer).textContent!.length); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, startContainer, (startContainer).textContent!.length); } else { - hitTarget = startContainer; + return new UnknownHitTestResult(startContainer); } } - return { - position: null, - hitTarget: hitTarget - }; + return new UnknownHitTestResult(); } /** * Most probably Gecko */ - private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): HitTestResult { const hitResult: { offsetNode: Node; offset: number; } = (document).caretPositionFromPoint(coords.clientX, coords.clientY); if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) { @@ -921,16 +908,9 @@ export class MouseTargetFactory { const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; if (parent3ClassName === ViewLine.CLASS_NAME) { - const p = ctx.getPositionFromDOMInfo(hitResult.offsetNode.parentNode, hitResult.offset); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode.parentNode, hitResult.offset); } else { - return { - position: null, - hitTarget: hitResult.offsetNode.parentNode - }; + return new UnknownHitTestResult(hitResult.offsetNode.parentNode); } } @@ -946,26 +926,15 @@ export class MouseTargetFactory { // it returned the `` of the line and the offset is the `` with the inline decoration const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)]; if (tokenSpan) { - const p = ctx.getPositionFromDOMInfo(tokenSpan, 0); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, tokenSpan, 0); } } else if (parent2ClassName === ViewLine.CLASS_NAME) { // it returned the `` with the inline decoration - const p = ctx.getPositionFromDOMInfo(hitResult.offsetNode, 0); - return { - position: p, - hitTarget: null - }; + return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode, 0); } } - return { - position: null, - hitTarget: hitResult.offsetNode - }; + return new UnknownHitTestResult(hitResult.offsetNode); } private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position { @@ -978,22 +947,17 @@ export class MouseTargetFactory { return position; } - private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult { + private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { - let result: IHitTestResult; + let result: HitTestResult = new UnknownHitTestResult(); if (typeof document.caretRangeFromPoint === 'function') { result = this._doHitTestWithCaretRangeFromPoint(ctx, request); } else if ((document).caretPositionFromPoint) { result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates()); - } else { - result = { - position: null, - hitTarget: null - }; } // Snap to the nearest soft tab boundary if atomic soft tabs are enabled. - if (result.position && ctx.stickyTabStops) { - result.position = this._snapToSoftTabBoundary(result.position, ctx.model); + if (result.type === HitTestResultType.Content && ctx.stickyTabStops) { + result = new ContentHitTestResult(this._snapToSoftTabBoundary(result.position, ctx.model), result.spanNode); } return result; } diff --git a/lib/vscode/src/vs/editor/browser/core/markdownRenderer.ts b/lib/vscode/src/vs/editor/browser/core/markdownRenderer.ts index f8422e25374c..b6bc211f5c3a 100644 --- a/lib/vscode/src/vs/editor/browser/core/markdownRenderer.ts +++ b/lib/vscode/src/vs/editor/browser/core/markdownRenderer.ts @@ -52,18 +52,18 @@ export class MarkdownRenderer { } render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { - const disposeables = new DisposableStore(); + const disposables = new DisposableStore(); let element: HTMLElement; if (!markdown) { element = document.createElement('span'); } else { - element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposeables), ...options }, markedOptions); + element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposables), ...options }, markedOptions); } return { element, - dispose: () => disposeables.dispose() + dispose: () => disposables.dispose() }; } diff --git a/lib/vscode/src/vs/editor/browser/editorBrowser.ts b/lib/vscode/src/vs/editor/browser/editorBrowser.ts index bfa07b8dcf9a..39ecb3745b0d 100644 --- a/lib/vscode/src/vs/editor/browser/editorBrowser.ts +++ b/lib/vscode/src/vs/editor/browser/editorBrowser.ts @@ -622,13 +622,13 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * Get value of the current model attached to this editor. - * @see `ITextModel.getValue` + * @see {@link ITextModel.getValue} */ getValue(options?: { preserveBOM: boolean; lineEnding: string; }): string; /** * Set the value of the current model attached to this editor. - * @see `ITextModel.setValue` + * @see {@link ITextModel.setValue} */ setValue(newValue: string): void; @@ -726,14 +726,14 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * All decorations added through this call will get the ownerId of this editor. - * @see `ITextModel.deltaDecorations` + * @see {@link ITextModel.deltaDecorations} */ deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; /** * @internal */ - setDecorations(decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void; + setDecorations(description: string, decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void; /** * @internal @@ -975,7 +975,7 @@ export interface IDiffEditor extends editorCommon.IEditor { readonly maxComputationTime: number; /** - * @see ICodeEditor.getDomNode + * @see {@link ICodeEditor.getDomNode} */ getDomNode(): HTMLElement; diff --git a/lib/vscode/src/vs/editor/browser/editorExtensions.ts b/lib/vscode/src/vs/editor/browser/editorExtensions.ts index 0ba2633acc47..9cf6d8db06e7 100644 --- a/lib/vscode/src/vs/editor/browser/editorExtensions.ts +++ b/lib/vscode/src/vs/editor/browser/editorExtensions.ts @@ -61,14 +61,14 @@ export interface ICommandMenuOptions { export interface ICommandOptions { id: string; precondition: ContextKeyExpression | undefined; - kbOpts?: ICommandKeybindingsOptions; + kbOpts?: ICommandKeybindingsOptions | ICommandKeybindingsOptions[]; description?: ICommandHandlerDescription; menuOpts?: ICommandMenuOptions | ICommandMenuOptions[]; } export abstract class Command { public readonly id: string; public readonly precondition: ContextKeyExpression | undefined; - private readonly _kbOpts: ICommandKeybindingsOptions | undefined; + private readonly _kbOpts: ICommandKeybindingsOptions | ICommandKeybindingsOptions[] | undefined; private readonly _menuOpts: ICommandMenuOptions | ICommandMenuOptions[] | undefined; private readonly _description: ICommandHandlerDescription | undefined; @@ -89,37 +89,38 @@ export abstract class Command { } if (this._kbOpts) { - let kbWhen = this._kbOpts.kbExpr; - if (this.precondition) { - if (kbWhen) { - kbWhen = ContextKeyExpr.and(kbWhen, this.precondition); - } else { - kbWhen = this.precondition; + const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts]; + for (const kbOpts of kbOptsArr) { + let kbWhen = kbOpts.kbExpr; + if (this.precondition) { + if (kbWhen) { + kbWhen = ContextKeyExpr.and(kbWhen, this.precondition); + } else { + kbWhen = this.precondition; + } } - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: this.id, - handler: (accessor, args) => this.runCommand(accessor, args), - weight: this._kbOpts.weight, - args: this._kbOpts.args, - when: kbWhen, - primary: this._kbOpts.primary, - secondary: this._kbOpts.secondary, - win: this._kbOpts.win, - linux: this._kbOpts.linux, - mac: this._kbOpts.mac, - description: this._description - }); - - } else { - CommandsRegistry.registerCommand({ - id: this.id, - handler: (accessor, args) => this.runCommand(accessor, args), - description: this._description - }); + const desc = { + id: this.id, + weight: kbOpts.weight, + args: kbOpts.args, + when: kbWhen, + primary: kbOpts.primary, + secondary: kbOpts.secondary, + win: kbOpts.win, + linux: kbOpts.linux, + mac: kbOpts.mac, + }; + + KeybindingsRegistry.registerKeybindingRule(desc); + } } + + CommandsRegistry.registerCommand({ + id: this.id, + handler: (accessor, args) => this.runCommand(accessor, args), + description: this._description + }); } private _registerMenuItem(item: ICommandMenuOptions): void { @@ -348,14 +349,16 @@ export abstract class EditorAction extends EditorCommand { public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise; } +export type EditorActionImplementation = (accessor: ServicesAccessor, editor: ICodeEditor, args: any) => boolean | Promise; + export class MultiEditorAction extends EditorAction { - private readonly _implementations: [number, CommandImplementation][] = []; + private readonly _implementations: [number, EditorActionImplementation][] = []; /** * A higher priority gets to be looked at first */ - public addImplementation(priority: number, implementation: CommandImplementation): IDisposable { + public addImplementation(priority: number, implementation: EditorActionImplementation): IDisposable { this._implementations.push([priority, implementation]); this._implementations.sort((a, b) => b[0] - a[0]); return { @@ -372,7 +375,7 @@ export class MultiEditorAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { for (const impl of this._implementations) { - const result = impl[1](accessor, args); + const result = impl[1](accessor, editor, args); if (result) { if (typeof result === 'boolean') { return; diff --git a/lib/vscode/src/vs/editor/browser/services/abstractCodeEditorService.ts b/lib/vscode/src/vs/editor/browser/services/abstractCodeEditorService.ts index 3ebb6454bb86..c6f58e65c6fa 100644 --- a/lib/vscode/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/lib/vscode/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -92,7 +92,7 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC return editorWithWidgetFocus; } - abstract registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; + abstract registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; abstract removeDecorationType(key: string): void; abstract resolveDecorationOptions(decorationTypeKey: string | undefined, writable: boolean): IModelDecorationOptions; abstract resolveDecorationCSSRules(decorationTypeKey: string): CSSRuleList | null; diff --git a/lib/vscode/src/vs/editor/browser/services/codeEditorService.ts b/lib/vscode/src/vs/editor/browser/services/codeEditorService.ts index 8665de00275a..b56596939a8f 100644 --- a/lib/vscode/src/vs/editor/browser/services/codeEditorService.ts +++ b/lib/vscode/src/vs/editor/browser/services/codeEditorService.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -39,7 +39,7 @@ export interface ICodeEditorService { */ getFocusedCodeEditor(): ICodeEditor | null; - registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; + registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; removeDecorationType(key: string): void; resolveDecorationOptions(typeKey: string, writable: boolean): IModelDecorationOptions; resolveDecorationCSSRules(decorationTypeKey: string): CSSRuleList | null; @@ -52,5 +52,5 @@ export interface ICodeEditorService { getTransientModelProperties(model: ITextModel): [string, any][] | undefined; getActiveCodeEditor(): ICodeEditor | null; - openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; + openCodeEditor(input: ITextResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise; } diff --git a/lib/vscode/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/lib/vscode/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 1451ddac061c..7e7ddddee847 100644 --- a/lib/vscode/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/lib/vscode/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -123,7 +123,7 @@ export abstract class CodeEditorServiceImpl extends AbstractCodeEditorService { this._editorStyleSheets.delete(editorId); } - public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void { + public registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void { let provider = this._decorationOptionProviders.get(key); if (!provider) { const styleSheet = this._getOrCreateStyleSheet(editor); @@ -134,7 +134,7 @@ export abstract class CodeEditorServiceImpl extends AbstractCodeEditorService { options: options || Object.create(null) }; if (!parentTypeKey) { - provider = new DecorationTypeOptionsProvider(this._themeService, styleSheet, providerArgs); + provider = new DecorationTypeOptionsProvider(description, this._themeService, styleSheet, providerArgs); } else { provider = new DecorationSubTypeOptionsProvider(this._themeService, styleSheet, providerArgs); } @@ -240,6 +240,7 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro private readonly _styleSheet: GlobalStyleSheet | RefCountedStyleSheet; public refCount: number; + public description: string; public className: string | undefined; public inlineClassName: string | undefined; public inlineClassNameAffectsLetterSpacing: boolean | undefined; @@ -250,7 +251,9 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro public overviewRuler: IModelDecorationOverviewRulerOptions | undefined; public stickiness: TrackedRangeStickiness | undefined; - constructor(themeService: IThemeService, styleSheet: GlobalStyleSheet | RefCountedStyleSheet, providerArgs: ProviderArguments) { + constructor(description: string, themeService: IThemeService, styleSheet: GlobalStyleSheet | RefCountedStyleSheet, providerArgs: ProviderArguments) { + this.description = description; + this._styleSheet = styleSheet; this._styleSheet.ref(); this.refCount = 0; @@ -305,6 +308,7 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro return this; } return { + description: this.description, inlineClassName: this.inlineClassName, beforeContentClassName: this.beforeContentClassName, afterContentClassName: this.afterContentClassName, diff --git a/lib/vscode/src/vs/editor/browser/services/openerService.ts b/lib/vscode/src/vs/editor/browser/services/openerService.ts index bc54879c7dc8..0a3b27961270 100644 --- a/lib/vscode/src/vs/editor/browser/services/openerService.ts +++ b/lib/vscode/src/vs/editor/browser/services/openerService.ts @@ -191,31 +191,41 @@ export class OpenerService implements IOpenerService { async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise { for (const resolver of this._resolvers) { - const result = await resolver.resolveExternalUri(resource, options); - if (result) { - if (!this._resolvedUriTargets.has(result.resolved)) { - this._resolvedUriTargets.set(result.resolved, resource); + try { + const result = await resolver.resolveExternalUri(resource, options); + if (result) { + if (!this._resolvedUriTargets.has(result.resolved)) { + this._resolvedUriTargets.set(result.resolved, resource); + } + return result; } - return result; + } catch { + // noop } } - return { resolved: resource, dispose: () => { } }; + throw new Error('Could not resolve external URI: ' + resource.toString()); } private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise { //todo@jrieken IExternalUriResolver should support `uri: URI | string` const uri = typeof resource === 'string' ? URI.parse(resource) : resource; - const { resolved } = await this.resolveExternalUri(uri, options); + let externalUri: URI; + + try { + externalUri = (await this.resolveExternalUri(uri, options)).resolved; + } catch { + externalUri = uri; + } let href: string; - if (typeof resource === 'string' && uri.toString() === resolved.toString()) { + if (typeof resource === 'string' && uri.toString() === externalUri.toString()) { // open the url-string AS IS href = resource; } else { // open URI using the toString(noEncode)+encodeURI-trick - href = encodeURI(resolved.toString(true)); + href = encodeURI(externalUri.toString(true)); } if (options?.allowContributedOpeners) { diff --git a/lib/vscode/src/vs/editor/browser/viewParts/minimap/minimap.ts b/lib/vscode/src/vs/editor/browser/viewParts/minimap/minimap.ts index 0ea038e6b890..454d7b4e15e5 100644 --- a/lib/vscode/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/lib/vscode/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1120,7 +1120,7 @@ class InnerMinimap extends Disposable { } if (this._model.options.size !== 'proportional') { if (e.leftButton && this._lastRenderData) { - // pretend the click occured in the center of the slider + // pretend the click occurred in the center of the slider const position = dom.getDomNodePagePosition(this._slider.domNode); const initialPosY = position.top + position.height / 2; this._startSliderDragging(e.buttons, e.posx, initialPosY, e.posy, this._lastRenderData.renderedLayout); diff --git a/lib/vscode/src/vs/editor/browser/widget/codeEditorWidget.ts b/lib/vscode/src/vs/editor/browser/widget/codeEditorWidget.ts index 9d89b72db24f..977e55d180ba 100644 --- a/lib/vscode/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/lib/vscode/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -217,8 +217,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _id: number; private readonly _configuration: editorCommon.IConfiguration; - protected readonly _contributions: { [key: string]: editorCommon.IEditorContribution; }; - protected readonly _actions: { [key: string]: editorCommon.IEditorAction; }; + protected _contributions: { [key: string]: editorCommon.IEditorContribution; }; + protected _actions: { [key: string]: editorCommon.IEditorAction; }; // --- Members logically associated to a model protected _modelData: ModelData | null; @@ -226,14 +226,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE protected readonly _instantiationService: IInstantiationService; protected readonly _contextKeyService: IContextKeyService; private readonly _notificationService: INotificationService; - private readonly _codeEditorService: ICodeEditorService; + protected readonly _codeEditorService: ICodeEditorService; private readonly _commandService: ICommandService; private readonly _themeService: IThemeService; private readonly _focusTracker: CodeEditorWidgetFocusTracker; - private readonly _contentWidgets: { [key: string]: IContentWidgetData; }; - private readonly _overlayWidgets: { [key: string]: IOverlayWidgetData; }; + private _contentWidgets: { [key: string]: IContentWidgetData; }; + private _overlayWidgets: { [key: string]: IOverlayWidgetData; }; /** * map from "parent" decoration type to live decoration ids. @@ -307,6 +307,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE contributions = EditorExtensionsRegistry.getEditorContributions(); } for (const desc of contributions) { + if (this._contributions[desc.id]) { + onUnexpectedError(new Error(`Cannot have two contributions with the same id ${desc.id}`)); + continue; + } try { const contribution = this._instantiationService.createInstance(desc.ctor, this); this._contributions[desc.id] = contribution; @@ -316,6 +320,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } EditorExtensionsRegistry.getEditorActions().forEach((action) => { + if (this._actions[action.id]) { + onUnexpectedError(new Error(`Cannot have two actions with the same id ${action.id}`)); + return; + } const internalAction = new InternalEditorAction( action.id, action.label, @@ -356,6 +364,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const contributionId = keys[i]; this._contributions[contributionId].dispose(); } + this._contributions = {}; + this._actions = {}; + this._contentWidgets = {}; + this._overlayWidgets = {}; this._removeDecorationTypes(); this._postDetachModelCleanup(this._detachModel()); @@ -1036,6 +1048,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return; } + this._triggerCommand(handlerId, payload); + } + + protected _triggerCommand(handlerId: string, payload: any): void { this._commandService.executeCommand(handlerId, payload); } @@ -1205,7 +1221,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.model.deltaDecorations(oldDecorations, newDecorations, this._id); } - public setDecorations(decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void { + public setDecorations(description: string, decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void { const newDecorationsSubTypes: { [key: string]: boolean } = {}; const oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {}; @@ -1224,7 +1240,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE typeKey = decorationTypeKey + '-' + subType; if (!oldDecorationsSubTypes[subType] && !newDecorationsSubTypes[subType]) { // decoration type did not exist before, register new one - this._registerDecorationType(typeKey, decorationOption.renderOptions, decorationTypeKey); + this._registerDecorationType(description, typeKey, decorationOption.renderOptions, decorationTypeKey); } newDecorationsSubTypes[subType] = true; } @@ -1685,8 +1701,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return model; } - private _registerDecorationType(key: string, options: editorCommon.IDecorationRenderOptions, parentTypeKey?: string): void { - this._codeEditorService.registerDecorationType(key, options, parentTypeKey, this); + private _registerDecorationType(description: string, key: string, options: editorCommon.IDecorationRenderOptions, parentTypeKey?: string): void { + this._codeEditorService.registerDecorationType(description, key, options, parentTypeKey, this); } private _removeDecorationType(key: string): void { @@ -1849,7 +1865,7 @@ export class EditorModeContext extends Disposable { private readonly _hasMultipleDocumentFormattingProvider: IContextKey; private readonly _hasMultipleDocumentSelectionFormattingProvider: IContextKey; private readonly _hasSignatureHelpProvider: IContextKey; - private readonly _hasInlineHintsProvider: IContextKey; + private readonly _hasInlayHintsProvider: IContextKey; private readonly _isInWalkThrough: IContextKey; constructor( @@ -1872,7 +1888,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(_contextKeyService); this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(_contextKeyService); this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(_contextKeyService); - this._hasInlineHintsProvider = EditorContextKeys.hasInlineHintsProvider.bindTo(_contextKeyService); + this._hasInlayHintsProvider = EditorContextKeys.hasInlayHintsProvider.bindTo(_contextKeyService); this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); @@ -1901,7 +1917,7 @@ export class EditorModeContext extends Disposable { this._register(modes.DocumentFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.DocumentRangeFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.SignatureHelpProviderRegistry.onDidChange(update)); - this._register(modes.InlineHintsProviderRegistry.onDidChange(update)); + this._register(modes.InlayHintsProviderRegistry.onDidChange(update)); update(); } @@ -1953,7 +1969,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); - this._hasInlineHintsProvider.set(modes.InlineHintsProviderRegistry.has(model)); + this._hasInlayHintsProvider.set(modes.InlayHintsProviderRegistry.has(model)); this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length + modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); diff --git a/lib/vscode/src/vs/editor/browser/widget/diffEditorWidget.ts b/lib/vscode/src/vs/editor/browser/widget/diffEditorWidget.ts index 19170cfe8280..ec7c270000f4 100644 --- a/lib/vscode/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/lib/vscode/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1775,27 +1775,33 @@ function createDecoration(startLineNumber: number, startColumn: number, endLineN const DECORATIONS = { charDelete: ModelDecorationOptions.register({ + description: 'diff-editor-char-delete', className: 'char-delete' }), charDeleteWholeLine: ModelDecorationOptions.register({ + description: 'diff-editor-char-delete-whole-line', className: 'char-delete', isWholeLine: true }), charInsert: ModelDecorationOptions.register({ + description: 'diff-editor-char-insert', className: 'char-insert' }), charInsertWholeLine: ModelDecorationOptions.register({ + description: 'diff-editor-char-insert-whole-line', className: 'char-insert', isWholeLine: true }), lineInsert: ModelDecorationOptions.register({ + description: 'diff-editor-line-insert', className: 'line-insert', marginClassName: 'line-insert', isWholeLine: true }), lineInsertWithSign: ModelDecorationOptions.register({ + description: 'diff-editor-line-insert-with-sign', className: 'line-insert', linesDecorationsClassName: 'insert-sign ' + ThemeIcon.asClassName(diffInsertIcon), marginClassName: 'line-insert', @@ -1803,11 +1809,13 @@ const DECORATIONS = { }), lineDelete: ModelDecorationOptions.register({ + description: 'diff-editor-line-delete', className: 'line-delete', marginClassName: 'line-delete', isWholeLine: true }), lineDeleteWithSign: ModelDecorationOptions.register({ + description: 'diff-editor-line-delete-with-sign', className: 'line-delete', linesDecorationsClassName: 'delete-sign ' + ThemeIcon.asClassName(diffRemoveIcon), marginClassName: 'line-delete', @@ -1815,6 +1823,7 @@ const DECORATIONS = { }), lineDeleteMargin: ModelDecorationOptions.register({ + description: 'diff-editor-line-delete-margin', marginClassName: 'line-delete', }) diff --git a/lib/vscode/src/vs/editor/browser/widget/diffReview.ts b/lib/vscode/src/vs/editor/browser/widget/diffReview.ts index 8a568c82136e..3d27ade5911a 100644 --- a/lib/vscode/src/vs/editor/browser/widget/diffReview.ts +++ b/lib/vscode/src/vs/editor/browser/widget/diffReview.ts @@ -22,7 +22,6 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { ILineChange, ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { ColorId, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry'; import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; @@ -777,19 +776,7 @@ export class DiffReview extends Disposable { private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number): string { const lineContent = model.getLineContent(lineNumber); const fontInfo = options.get(EditorOption.fontInfo); - - const defaultMetadata = ( - (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) - | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) - | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) - ) >>> 0; - - const tokens = new Uint32Array(2); - tokens[0] = lineContent.length; - tokens[1] = defaultMetadata; - - const lineTokens = new LineTokens(tokens, lineContent); - + const lineTokens = LineTokens.createEmpty(lineContent); const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); const r = renderViewLine(new RenderLineInput( diff --git a/lib/vscode/src/vs/editor/common/config/commonEditorConfig.ts b/lib/vscode/src/vs/editor/common/config/commonEditorConfig.ts index 4a888eb21e78..3ec9a2093e79 100644 --- a/lib/vscode/src/vs/editor/common/config/commonEditorConfig.ts +++ b/lib/vscode/src/vs/editor/common/config/commonEditorConfig.ts @@ -204,6 +204,7 @@ function migrateOptions(options: IEditorOptions): void { mapping['method'] = 'showMethods'; mapping['function'] = 'showFunctions'; mapping['constructor'] = 'showConstructors'; + mapping['deprecated'] = 'showDeprecated'; mapping['field'] = 'showFields'; mapping['variable'] = 'showVariables'; mapping['class'] = 'showClasses'; diff --git a/lib/vscode/src/vs/editor/common/config/editorOptions.ts b/lib/vscode/src/vs/editor/common/config/editorOptions.ts index ee5f4095357a..4d45c26f8fa6 100644 --- a/lib/vscode/src/vs/editor/common/config/editorOptions.ts +++ b/lib/vscode/src/vs/editor/common/config/editorOptions.ts @@ -382,8 +382,9 @@ export interface IEditorOptions { * Suggest options. */ suggest?: ISuggestOptions; + inlineSuggest?: IInlineSuggestOptions; /** - * Smart select opptions; + * Smart select options. */ smartSelect?: ISmartSelectOptions; /** @@ -637,7 +638,11 @@ export interface IEditorOptions { /** * Control the behavior and rendering of the inline hints. */ - inlineHints?: IEditorInlineHintsOptions; + inlayHints?: IEditorInlayHintsOptions; + /** + * Control if the editor should use shadow DOM. + */ + useShadowDOM?: boolean; } /** @@ -2396,12 +2401,12 @@ class EditorLightbulb extends BaseEditorOption>; +export type EditorInlayHintsOptions = Readonly>; -class EditorInlineHints extends BaseEditorOption { +class EditorInlayHints extends BaseEditorOption { constructor() { - const defaults: EditorInlineHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; + const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; super( - EditorOption.inlineHints, 'inlineHints', defaults, + EditorOption.inlayHints, 'inlayHints', defaults, { - 'editor.inlineHints.enabled': { + 'editor.inlayHints.enabled': { type: 'boolean', default: defaults.enabled, - description: nls.localize('inlineHints.enable', "Enables the inline hints in the editor.") + description: nls.localize('inlayHints.enable', "Enables the inlay hints in the editor.") }, - 'editor.inlineHints.fontSize': { + 'editor.inlayHints.fontSize': { type: 'number', default: defaults.fontSize, - description: nls.localize('inlineHints.fontSize', "Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") + description: nls.localize('inlayHints.fontSize', "Controls font size of inlay hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") }, - 'editor.inlineHints.fontFamily': { + 'editor.inlayHints.fontFamily': { type: 'string', default: defaults.fontFamily, - description: nls.localize('inlineHints.fontFamily', "Controls font family of inline hints in the editor.") + description: nls.localize('inlayHints.fontFamily', "Controls font family of inlay hints in the editor.") }, } ); } - public validate(_input: any): EditorInlineHintsOptions { + public validate(_input: any): EditorInlayHintsOptions { if (!_input || typeof _input !== 'object') { return this.defaultValue; } - const input = _input as IEditorInlineHintsOptions; + const input = _input as IEditorInlayHintsOptions; return { enabled: boolean(input.enabled, this.defaultValue.enabled), fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), @@ -3136,6 +3141,51 @@ class EditorScrollbar extends BaseEditorOption>; + +/** + * Configuration options for inline suggestions + */ +class InlineEditorSuggest extends BaseEditorOption { + constructor() { + const defaults: InternalInlineSuggestOptions = { + enabled: false + }; + + super( + EditorOption.inlineSuggest, 'inlineSuggest', defaults, + { + 'editor.inlineSuggest.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('inlineSuggest.enabled', "Controls whether to automatically show inline suggestions in the editor.") + }, + } + ); + } + + public validate(_input: any): InternalInlineSuggestOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IInlineSuggestOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled), + }; + } +} + +//#endregion + //#region suggest /** @@ -3170,6 +3220,10 @@ export interface ISuggestOptions { * Enable or disable the suggest status bar. */ showStatusBar?: boolean; + /** + * Enable or disable the rendering of the suggestion preview. + */ + preview?: boolean; /** * Show details inline with the label. Defaults to true. */ @@ -3186,6 +3240,10 @@ export interface ISuggestOptions { * Show constructor-suggestions. */ showConstructors?: boolean; + /** + * Show deprecated-suggestions. + */ + showDeprecated?: boolean; /** * Show field-suggestions. */ @@ -3297,10 +3355,12 @@ class EditorSuggest extends BaseEditorOption(); + result.push(-1); + let pos = 0; + let i = 0; + while (i < endOffset) { + const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); + i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + result.push(pos); + if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) { + result.push(pos); + } + + if (codePoint === CharCode.Tab) { + pos = CursorColumns.nextRenderTabStop(pos, tabSize); + } else { + let graphemeBreakType = strings.getGraphemeBreakType(codePoint); + while (i < endOffset) { + const nextCodePoint = strings.getNextCodePoint(lineContent, endOffset, i); + const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); + if (strings.breakBetweenGraphemeBreakType(graphemeBreakType, nextGraphemeBreakType)) { + break; + } + i += (nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + result.push(pos); + if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) { + result.push(pos); + } + + graphemeBreakType = nextGraphemeBreakType; + } + if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) { + pos = pos + 2; + } else { + pos = pos + 1; + } + } + } + result.push(pos); + return result; + } + public static toStatusbarColumn(lineContent: string, column: number, tabSize: number): number { const lineContentLength = lineContent.length; const endOffset = column - 1 < lineContentLength ? column - 1 : lineContentLength; diff --git a/lib/vscode/src/vs/editor/common/controller/cursorDeleteOperations.ts b/lib/vscode/src/vs/editor/common/controller/cursorDeleteOperations.ts index 1d0fdd265762..5b799220d594 100644 --- a/lib/vscode/src/vs/editor/common/controller/cursorDeleteOperations.ts +++ b/lib/vscode/src/vs/editor/common/controller/cursorDeleteOperations.ts @@ -12,6 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand } from 'vs/editor/common/editorCommon'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; +import { Position } from 'vs/editor/common/core/position'; export class DeleteOperations { @@ -25,7 +26,7 @@ export class DeleteOperations { if (deleteSelection.isEmpty()) { let position = selection.getPosition(); - let rightOfPosition = MoveOperations.right(config, model, position.lineNumber, position.column); + let rightOfPosition = MoveOperations.right(config, model, position); deleteSelection = new Range( rightOfPosition.lineNumber, rightOfPosition.column, @@ -141,63 +142,72 @@ export class DeleteOperations { } public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], autoClosedCharacters: Range[]): [boolean, Array] { - if (this.isAutoClosingPairDelete(config.autoClosingDelete, config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairs.autoClosingPairsOpenByEnd, model, selections, autoClosedCharacters)) { return this._runAutoClosingPairDelete(config, model, selections); } - let commands: Array = []; + const commands: Array = []; let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingLeft); for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - let deleteSelection: Range = selection; - - if (deleteSelection.isEmpty()) { - let position = selection.getPosition(); - - if (config.useTabStops && position.column > 1) { - let lineContent = model.getLineContent(position.lineNumber); - - let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); - let lastIndentationColumn = ( - firstNonWhitespaceIndex === -1 - ? /* entire string is whitespace */lineContent.length + 1 - : firstNonWhitespaceIndex + 1 - ); - - if (position.column <= lastIndentationColumn) { - let fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position); - let toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize); - let toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn); - deleteSelection = new Range(position.lineNumber, toColumn, position.lineNumber, position.column); - } else { - deleteSelection = new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column); - } - } else { - let leftOfPosition = MoveOperations.left(config, model, position.lineNumber, position.column); - deleteSelection = new Range( - leftOfPosition.lineNumber, - leftOfPosition.column, - position.lineNumber, - position.column - ); - } - } + let deleteRange = DeleteOperations.getDeleteRange(selections[i], model, config); - if (deleteSelection.isEmpty()) { - // Probably at beginning of file => ignore + // Ignore empty delete ranges, as they have no effect + // They happen if the cursor is at the beginning of the file. + if (deleteRange.isEmpty()) { commands[i] = null; continue; } - if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) { + if (deleteRange.startLineNumber !== deleteRange.endLineNumber) { shouldPushStackElementBefore = true; } - commands[i] = new ReplaceCommand(deleteSelection, ''); + commands[i] = new ReplaceCommand(deleteRange, ''); } return [shouldPushStackElementBefore, commands]; + + } + + private static getDeleteRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration,): Range { + if (!selection.isEmpty()) { + return selection; + } + + const position = selection.getPosition(); + + // Unintend when using tab stops and cursor is within indentation + if (config.useTabStops && position.column > 1) { + const lineContent = model.getLineContent(position.lineNumber); + + const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); + const lastIndentationColumn = ( + firstNonWhitespaceIndex === -1 + ? /* entire string is whitespace */ lineContent.length + 1 + : firstNonWhitespaceIndex + 1 + ); + + if (position.column <= lastIndentationColumn) { + const fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position); + const toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize); + const toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn); + return new Range(position.lineNumber, toColumn, position.lineNumber, position.column); + } + } + + return Range.fromPositions(DeleteOperations.getPositionAfterDeleteLeft(position, model), position); + } + + private static getPositionAfterDeleteLeft(position: Position, model: ICursorSimpleModel): Position { + if (position.column > 1) { + // Convert 1-based columns to 0-based offsets and back. + const idx = strings.getLeftDeleteOffset(position.column - 1, model.getLineContent(position.lineNumber)); + return position.with(undefined, idx + 1); + } else if (position.lineNumber > 1) { + const newLine = position.lineNumber - 1; + return new Position(newLine, model.getLineMaxColumn(newLine)); + } else { + return position; + } } public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult { diff --git a/lib/vscode/src/vs/editor/common/controller/cursorMoveCommands.ts b/lib/vscode/src/vs/editor/common/controller/cursorMoveCommands.ts index 2ae510c30a05..a07173f2eede 100644 --- a/lib/vscode/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/lib/vscode/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -419,29 +419,11 @@ export class CursorMoveCommands { } private static _moveLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { - const hasMultipleCursors = (cursors.length > 1); - let result: PartialCursorState[] = []; - for (let i = 0, len = cursors.length; i < len; i++) { - const cursor = cursors[i]; - const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); - let newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - - if (skipWrappingPointStop - && noOfColumns === 1 - && cursor.viewState.position.column === viewModel.getLineMinColumn(cursor.viewState.position.lineNumber) - && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber - ) { - // moved over to the previous view line - const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); - if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) { - // stayed on the same model line => pass wrapping point where 2 view positions map to a single model position - newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1); - } - } - - result[i] = CursorState.fromViewState(newViewState); - } - return result; + return cursors.map(cursor => + CursorState.fromViewState( + MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns) + ) + ); } private static _moveHalfLineLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { @@ -456,29 +438,11 @@ export class CursorMoveCommands { } private static _moveRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { - const hasMultipleCursors = (cursors.length > 1); - let result: PartialCursorState[] = []; - for (let i = 0, len = cursors.length; i < len; i++) { - const cursor = cursors[i]; - const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); - let newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - - if (skipWrappingPointStop - && noOfColumns === 1 - && cursor.viewState.position.column === viewModel.getLineMaxColumn(cursor.viewState.position.lineNumber) - && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber - ) { - // moved over to the next view line - const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); - if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) { - // stayed on the same model line => pass wrapping point where 2 view positions map to a single model position - newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1); - } - } - - result[i] = CursorState.fromViewState(newViewState); - } - return result; + return cursors.map(cursor => + CursorState.fromViewState( + MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns) + ) + ); } private static _moveHalfLineRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { diff --git a/lib/vscode/src/vs/editor/common/controller/cursorMoveOperations.ts b/lib/vscode/src/vs/editor/common/controller/cursorMoveOperations.ts index 7d94c4374349..7fa58fa5c09a 100644 --- a/lib/vscode/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/lib/vscode/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -9,6 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import * as strings from 'vs/base/common/strings'; import { Constants } from 'vs/base/common/uint'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/controller/cursorAtomicMoveOperations'; +import { PositionNormalizationAffinity } from 'vs/editor/common/model'; export class CursorPosition { _cursorPositionBrand: void; @@ -25,51 +26,86 @@ export class CursorPosition { } export class MoveOperations { - - public static leftPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position { - if (column > model.getLineMinColumn(lineNumber)) { - column = column - strings.prevCharLength(model.getLineContent(lineNumber), column - 1); - } else if (lineNumber > 1) { - lineNumber = lineNumber - 1; - column = model.getLineMaxColumn(lineNumber); + public static leftPosition(model: ICursorSimpleModel, position: Position): Position { + if (position.column > model.getLineMinColumn(position.lineNumber)) { + return position.delta(undefined, -strings.prevCharLength(model.getLineContent(position.lineNumber), position.column - 1)); + } else if (position.lineNumber > 1) { + const newLineNumber = position.lineNumber - 1; + return new Position(newLineNumber, model.getLineMaxColumn(newLineNumber)); + } else { + return position; } - return new Position(lineNumber, column); } - public static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number): Position { - const minColumn = model.getLineMinColumn(lineNumber); - const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Left); - if (newPosition === -1 || newPosition + 1 < minColumn) { - return this.leftPosition(model, lineNumber, column); + private static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, position: Position, tabSize: number): Position { + if (position.column <= model.getLineIndentColumn(position.lineNumber)) { + const minColumn = model.getLineMinColumn(position.lineNumber); + const lineContent = model.getLineContent(position.lineNumber); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Left); + if (newPosition !== -1 && newPosition + 1 >= minColumn) { + return new Position(position.lineNumber, newPosition + 1); + } } - return new Position(lineNumber, newPosition + 1); + return this.leftPosition(model, position); } - public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { + private static left(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition { const pos = config.stickyTabStops - ? MoveOperations.leftPositionAtomicSoftTabs(model, lineNumber, column, config.tabSize) - : MoveOperations.leftPosition(model, lineNumber, column); + ? MoveOperations.leftPositionAtomicSoftTabs(model, position, config.tabSize) + : MoveOperations.leftPosition(model, position); return new CursorPosition(pos.lineNumber, pos.column, 0); } + /** + * @param noOfColumns Must be either `1` + * or `Math.round(viewModel.getLineContent(viewLineNumber).length / 2)` (for half lines). + */ public static moveLeft(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState { let lineNumber: number, column: number; if (cursor.hasSelection() && !inSelectionMode) { - // If we are in selection mode, move left without selection cancels selection and puts cursor at the beginning of the selection + // If the user has a selection and does not want to extend it, + // put the cursor at the beginning of the selection. lineNumber = cursor.selection.startLineNumber; column = cursor.selection.startColumn; } else { - let r = MoveOperations.left(config, model, cursor.position.lineNumber, cursor.position.column - (noOfColumns - 1)); - lineNumber = r.lineNumber; - column = r.column; + // This has no effect if noOfColumns === 1. + // It is ok to do so in the half-line scenario. + const pos = cursor.position.delta(undefined, -(noOfColumns - 1)); + // We clip the position before normalization, as normalization is not defined + // for possibly negative columns. + const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Left); + const p = MoveOperations.left(config, model, normalizedPos); + + lineNumber = p.lineNumber; + column = p.column; } return cursor.move(inSelectionMode, lineNumber, column, 0); } + /** + * Adjusts the column so that it is within min/max of the line. + */ + private static clipPositionColumn(position: Position, model: ICursorSimpleModel): Position { + return new Position( + position.lineNumber, + MoveOperations.clipRange(position.column, model.getLineMinColumn(position.lineNumber), + model.getLineMaxColumn(position.lineNumber)) + ); + } + + private static clipRange(value: number, min: number, max: number): number { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; + } + public static rightPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position { if (column < model.getLineMaxColumn(lineNumber)) { column = column + strings.nextCharLength(model.getLineContent(lineNumber), column - 1); @@ -81,18 +117,20 @@ export class MoveOperations { } public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position { - const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); - if (newPosition === -1) { - return this.rightPosition(model, lineNumber, column); + if (column < model.getLineIndentColumn(lineNumber)) { + const lineContent = model.getLineContent(lineNumber); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); + if (newPosition !== -1) { + return new Position(lineNumber, newPosition + 1); + } } - return new Position(lineNumber, newPosition + 1); + return this.rightPosition(model, lineNumber, column); } - public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { + public static right(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition { const pos = config.stickyTabStops - ? MoveOperations.rightPositionAtomicSoftTabs(model, lineNumber, column, config.tabSize, config.indentSize) - : MoveOperations.rightPosition(model, lineNumber, column); + ? MoveOperations.rightPositionAtomicSoftTabs(model, position.lineNumber, position.column, config.tabSize, config.indentSize) + : MoveOperations.rightPosition(model, position.lineNumber, position.column); return new CursorPosition(pos.lineNumber, pos.column, 0); } @@ -105,7 +143,9 @@ export class MoveOperations { lineNumber = cursor.selection.endLineNumber; column = cursor.selection.endColumn; } else { - let r = MoveOperations.right(config, model, cursor.position.lineNumber, cursor.position.column + (noOfColumns - 1)); + const pos = cursor.position.delta(undefined, noOfColumns - 1); + const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Right); + const r = MoveOperations.right(config, model, normalizedPos); lineNumber = r.lineNumber; column = r.column; } diff --git a/lib/vscode/src/vs/editor/common/controller/cursorTypeOperations.ts b/lib/vscode/src/vs/editor/common/controller/cursorTypeOperations.ts index 4acc0ae72be2..3f2e81ed0e1b 100644 --- a/lib/vscode/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/lib/vscode/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -260,8 +260,8 @@ export class TypeOperations { public static compositionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): EditOperationResult { const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing), + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), shouldPushStackElementAfter: false }); } @@ -484,8 +484,8 @@ export class TypeOperations { const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); commands[i] = new ReplaceCommand(typeSelection, ch); } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing), + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), shouldPushStackElementAfter: false }); } @@ -636,7 +636,7 @@ export class TypeOperations { const selection = selections[i]; commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPairClose); } - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }); @@ -762,7 +762,7 @@ export class TypeOperations { let typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); const command = new ReplaceCommand(typeSelection, typeText); - return new EditOperationResult(EditOperationType.Typing, [command], { + return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { shouldPushStackElementBefore: false, shouldPushStackElementAfter: true }); @@ -803,7 +803,7 @@ export class TypeOperations { if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { // Unfortunately, the close character is at this point "doubled", so we need to delete it... const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }); @@ -824,7 +824,7 @@ export class TypeOperations { for (let i = 0, len = selections.length; i < len; i++) { commands[i] = TypeOperations._enter(config, model, false, selections[i]); } - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false, }); @@ -841,7 +841,7 @@ export class TypeOperations { } } if (!autoIndentFails) { - return new EditOperationResult(EditOperationType.Typing, commands, { + return new EditOperationResult(EditOperationType.TypingOther, commands, { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false, }); @@ -877,12 +877,10 @@ export class TypeOperations { for (let i = 0, len = selections.length; i < len; i++) { commands[i] = new ReplaceCommand(selections[i], ch); } - let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.Typing); - if (ch === ' ') { - shouldPushStackElementBefore = true; - } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: shouldPushStackElementBefore, + + const opType = getTypingOperation(ch, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), shouldPushStackElementAfter: false }); } @@ -892,8 +890,9 @@ export class TypeOperations { for (let i = 0, len = selections.length; i < len; i++) { commands[i] = new ReplaceCommand(selections[i], str); } - return new EditOperationResult(EditOperationType.Typing, commands, { - shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing), + const opType = getTypingOperation(str, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), shouldPushStackElementAfter: false }); } @@ -965,3 +964,40 @@ export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorSt return super.computeCursorState(model, helper); } } + +function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { + if (typedText === ' ') { + return previousTypingOperation === EditOperationType.TypingFirstSpace + || previousTypingOperation === EditOperationType.TypingConsecutiveSpace + ? EditOperationType.TypingConsecutiveSpace + : EditOperationType.TypingFirstSpace; + } + + return EditOperationType.TypingOther; +} + +function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { + if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { + // Always set an undo stop before non-type operations + return true; + } + if (previousTypingOperation === EditOperationType.TypingFirstSpace) { + // `abc |d`: No undo stop + // `abc |d`: Undo stop + return false; + } + // Insert undo stop between different operation types + return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); +} + +function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { + return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) + ? 'space' + : type; +} + +function isTypingOperation(type: EditOperationType): boolean { + return type === EditOperationType.TypingOther + || type === EditOperationType.TypingFirstSpace + || type === EditOperationType.TypingConsecutiveSpace; +} diff --git a/lib/vscode/src/vs/editor/common/core/lineTokens.ts b/lib/vscode/src/vs/editor/common/core/lineTokens.ts index 51aa6a93ca83..66d9e3678178 100644 --- a/lib/vscode/src/vs/editor/common/core/lineTokens.ts +++ b/lib/vscode/src/vs/editor/common/core/lineTokens.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ColorId, LanguageId, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; export interface IViewLineTokens { equals(other: IViewLineTokens): boolean; @@ -22,6 +22,20 @@ export class LineTokens implements IViewLineTokens { private readonly _tokensCount: number; private readonly _text: string; + public static createEmpty(lineContent: string): LineTokens { + const defaultMetadata = ( + (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) + | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) + | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) + ) >>> 0; + + const tokens = new Uint32Array(2); + tokens[0] = lineContent.length; + tokens[1] = defaultMetadata; + + return new LineTokens(tokens, lineContent); + } + constructor(tokens: Uint32Array, text: string) { this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); diff --git a/lib/vscode/src/vs/editor/common/editorCommon.ts b/lib/vscode/src/vs/editor/common/editorCommon.ts index 962a78885a10..fd38dafbd887 100644 --- a/lib/vscode/src/vs/editor/common/editorCommon.ts +++ b/lib/vscode/src/vs/editor/common/editorCommon.ts @@ -503,7 +503,7 @@ export interface IEditor { * Change the decorations. All decorations added through this changeAccessor * will get the ownerId of the editor (meaning they will not show up in other * editors). - * @see `ITextModel.changeDecorations` + * @see {@link ITextModel.changeDecorations} * @internal */ changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any; @@ -639,6 +639,7 @@ export interface IContentDecorationRenderOptions { textDecoration?: string; color?: string | ThemeColor; backgroundColor?: string | ThemeColor; + opacity?: string; margin?: string; padding?: string; diff --git a/lib/vscode/src/vs/editor/common/editorContextKeys.ts b/lib/vscode/src/vs/editor/common/editorContextKeys.ts index 58beda673801..11c78368665a 100644 --- a/lib/vscode/src/vs/editor/common/editorContextKeys.ts +++ b/lib/vscode/src/vs/editor/common/editorContextKeys.ts @@ -62,7 +62,7 @@ export namespace EditorContextKeys { export const hasReferenceProvider = new RawContextKey('editorHasReferenceProvider', false, nls.localize('editorHasReferenceProvider', "Whether the editor has a reference provider")); export const hasRenameProvider = new RawContextKey('editorHasRenameProvider', false, nls.localize('editorHasRenameProvider', "Whether the editor has a rename provider")); export const hasSignatureHelpProvider = new RawContextKey('editorHasSignatureHelpProvider', false, nls.localize('editorHasSignatureHelpProvider', "Whether the editor has a signature help provider")); - export const hasInlineHintsProvider = new RawContextKey('editorHasInlineHintsProvider', false, nls.localize('editorHasInlineHintsProvider', "Whether the editor has an inline hints provider")); + export const hasInlayHintsProvider = new RawContextKey('editorHasInlayHintsProvider', false, nls.localize('editorHasInlayHintsProvider', "Whether the editor has an inline hints provider")); // -- mode context keys: formatting export const hasDocumentFormattingProvider = new RawContextKey('editorHasDocumentFormattingProvider', false, nls.localize('editorHasDocumentFormattingProvider', "Whether the editor has a document formatting provider")); diff --git a/lib/vscode/src/vs/editor/common/model.ts b/lib/vscode/src/vs/editor/common/model.ts index 3f1a239dcbf8..a10d22ba6300 100644 --- a/lib/vscode/src/vs/editor/common/model.ts +++ b/lib/vscode/src/vs/editor/common/model.ts @@ -73,6 +73,11 @@ export interface IModelDecorationMinimapOptions extends IDecorationOptions { * Options for a model decoration. */ export interface IModelDecorationOptions { + /** + * A debug description that can be used for inspecting model decorations. + * @internal + */ + description: string; /** * Customize the growing behavior of the decoration when typing at the edges of the decoration. * Defaults to TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges @@ -1220,7 +1225,6 @@ export interface ITextModel { /** * An event emitted when the model has been attached to the first editor or detached from the last editor. * @event - * @internal */ onDidChangeAttached(listener: () => void): IDisposable; /** @@ -1247,7 +1251,6 @@ export interface ITextModel { /** * Returns if this model is attached to an editor or not. - * @internal */ isAttachedToEditor(): boolean; @@ -1256,6 +1259,33 @@ export interface ITextModel { * @internal */ getAttachedEditorCount(): number; + + /** + * Among all positions that are projected to the same position in the underlying text model as + * the given position, select a unique position as indicated by the affinity. + * @internal + */ + normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position; + + /** + * Gets the column at which indentation stops at a given line. + * @internal + */ + getLineIndentColumn(lineNumber: number): number; +} + +/** + * @internal + */ +export const enum PositionNormalizationAffinity { + /** + * Prefers the left most position. + */ + Left = 0, + /** + * Prefers the right most position. + */ + Right = 1, } /** diff --git a/lib/vscode/src/vs/editor/common/model/textModel.ts b/lib/vscode/src/vs/editor/common/model/textModel.ts index 61452e16c782..d4b415295910 100644 --- a/lib/vscode/src/vs/editor/common/model/textModel.ts +++ b/lib/vscode/src/vs/editor/common/model/textModel.ts @@ -3028,6 +3028,30 @@ export class TextModel extends Disposable implements model.ITextModel { } //#endregion + normalizePosition(position: Position, affinity: model.PositionNormalizationAffinity): Position { + return position; + } + + /** + * Gets the column at which indentation stops at a given line. + * @internal + */ + public getLineIndentColumn(lineNumber: number): number { + // Columns start with 1. + return indentOfLine(this.getLineContent(lineNumber)) + 1; + } +} + +function indentOfLine(line: string): number { + let indent = 0; + for (const c of line) { + if (c === ' ' || c === '\t') { + indent++; + } else { + break; + } + } + return indent; } //#region Decorations @@ -3205,6 +3229,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { return new ModelDecorationOptions(options); } + readonly description: string; readonly stickiness: model.TrackedRangeStickiness; readonly zIndex: number; readonly className: string | null; @@ -3225,6 +3250,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly afterContentClassName: string | null; private constructor(options: model.IModelDecorationOptions) { + this.description = options.description; this.stickiness = options.stickiness || model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges; this.zIndex = options.zIndex || 0; this.className = options.className ? cleanClassName(options.className) : null; @@ -3245,16 +3271,16 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.afterContentClassName = options.afterContentClassName ? cleanClassName(options.afterContentClassName) : null; } } -ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({}); +ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({ description: 'empty' }); /** * The order carefully matches the values of the enum. */ const TRACKED_RANGE_OPTIONS = [ - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), - ModelDecorationOptions.register({ stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), + ModelDecorationOptions.register({ description: 'tracked-range-always-grows-when-typing-at-edges', stickiness: model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ description: 'tracked-range-never-grows-when-typing-at-edges', stickiness: model.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ description: 'tracked-range-grows-only-when-typing-before', stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), + ModelDecorationOptions.register({ description: 'tracked-range-grows-only-when-typing-after', stickiness: model.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), ]; function _normalizeOptions(options: model.IModelDecorationOptions): ModelDecorationOptions { diff --git a/lib/vscode/src/vs/editor/common/modes.ts b/lib/vscode/src/vs/editor/common/modes.ts index a60bb77be2ae..f42c4ee03075 100644 --- a/lib/vscode/src/vs/editor/common/modes.ts +++ b/lib/vscode/src/vs/editor/common/modes.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token'; @@ -227,7 +227,7 @@ export interface IState { } /** - * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * A provider result represents the values a provider, like the {@link HoverProvider}, * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. @@ -557,13 +557,13 @@ export interface CompletionItem { documentation?: string | IMarkdownString; /** * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. */ sortText?: string; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. */ filterText?: string; @@ -587,11 +587,11 @@ export interface CompletionItem { /** * A range of text that should be replaced by this completion item. * - * Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * Defaults to a range from the start of the {@link TextDocument.getWordRangeAtPosition current word} to the * current position. * - * *Note:* The range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note:* The range must be a {@link Range.isSingleLine single line} and it must + * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ range: IRange | { insert: IRange, replace: IRange }; /** @@ -638,7 +638,7 @@ export const enum CompletionTriggerKind { } /** * Contains additional information about the context in which - * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered. */ export interface CompletionContext { /** @@ -658,10 +658,10 @@ export interface CompletionContext { * * When computing *complete* completion items is expensive, providers can optionally implement * the `resolveCompletionItem`-function. In that case it is enough to return completion - * items with a [label](#CompletionItem.label) from the - * [provideCompletionItems](#CompletionItemProvider.provideCompletionItems)-function. Subsequently, + * items with a {@link CompletionItem.label label} from the + * {@link CompletionItemProvider.provideCompletionItems provideCompletionItems}-function. Subsequently, * when a completion item is shown in the UI and gains focus this provider is asked to resolve - * the item, like adding [doc-comment](#CompletionItem.documentation) or [details](#CompletionItem.detail). + * the item, like adding {@link CompletionItem.documentation doc-comment} or {@link CompletionItem.detail details}. */ export interface CompletionItemProvider { @@ -677,14 +677,73 @@ export interface CompletionItemProvider { provideCompletionItems(model: model.ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult; /** - * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) - * or [details](#CompletionItem.detail). + * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment} + * or {@link CompletionItem.detail details}. * * The editor will only resolve a completion item once. */ resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; } +/** + * How an {@link InlineCompletionsProvider inline completion provider} was triggered. + */ +export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1, +} + +export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; +} + +export interface InlineCompletion { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + readonly text: string; + + /** + * The range to replace. + * Must begin and end on the same line. + */ + readonly range?: IRange; + + readonly command?: Command; +} + +export interface InlineCompletions { + readonly items: readonly TItem[]; +} + +export interface InlineCompletionsProvider { + provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + + /** + * Will be called when an item is shown. + */ + handleItemDidShow?(completions: T, item: T['items'][number]): void; + + /** + * Will be called when a completions list is no longer in use and can be garbage-collected. + */ + freeInlineCompletions(completions: T): void; +} + export interface CodeAction { title: string; command?: Command; @@ -870,7 +929,7 @@ export interface DocumentHighlight { */ range: IRange; /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ kind?: DocumentHighlightKind; } @@ -1323,12 +1382,12 @@ export interface IColorPresentation { */ label: string; /** - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this presentation for the color. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when + * An optional array of additional {@link TextEdit text edits} that are applied when * selecting this color presentation. */ additionalTextEdits?: TextEdit[]; @@ -1406,10 +1465,10 @@ export interface FoldingRange { end: number; /** - * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or - * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + * {@link FoldingRangeKind} for an enumeration of standardized kinds. */ kind?: FoldingRangeKind; } @@ -1429,7 +1488,7 @@ export class FoldingRangeKind { static readonly Region = new FoldingRangeKind('region'); /** - * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * Creates a new {@link FoldingRangeKind}. * * @param value of the kind. */ @@ -1701,24 +1760,23 @@ export interface CodeLensProvider { } -export enum InlineHintKind { +export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2, } -export interface InlineHint { +export interface InlayHint { text: string; - range: IRange; - kind: InlineHintKind; - description?: string | IMarkdownString; + position: IPosition; + kind: InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; } -export interface InlineHintsProvider { - onDidChangeInlineHints?: Event | undefined; - provideInlineHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; +export interface InlayHintsProvider { + onDidChangeInlayHints?: Event | undefined; + provideInlayHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; } export interface SemanticTokensLegend { @@ -1771,6 +1829,11 @@ export const RenameProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const InlineCompletionsProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ @@ -1834,7 +1897,7 @@ export const CodeLensProviderRegistry = new LanguageFeatureRegistry(); +export const InlayHintsProviderRegistry = new LanguageFeatureRegistry(); /** * @internal diff --git a/lib/vscode/src/vs/editor/common/modes/linkComputer.ts b/lib/vscode/src/vs/editor/common/modes/linkComputer.ts index c0ca979fb9b9..c6249cb323ae 100644 --- a/lib/vscode/src/vs/editor/common/modes/linkComputer.ts +++ b/lib/vscode/src/vs/editor/common/modes/linkComputer.ts @@ -154,7 +154,7 @@ function getClassifier(): CharacterClassifier { if (_classifier === null) { _classifier = new CharacterClassifier(CharacterClass.None); - const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"ใ€ใ€‚๏ฝก๏ฝค๏ผŒ๏ผŽ๏ผš๏ผ›โ€˜โ€œใ€ˆใ€Šใ€Œใ€Žใ€ใ€”๏ผˆ๏ผป๏ฝ›๏ฝข๏ฝฃ๏ฝ๏ผฝ๏ผ‰ใ€•ใ€‘ใ€ใ€ใ€‹ใ€‰โ€โ€™๏ฝ€๏ฝžโ€ฆ'; + const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"ใ€ใ€‚๏ฝก๏ฝค๏ผŒ๏ผŽ๏ผš๏ผ›โ€˜ใ€ˆใ€Œใ€Žใ€”๏ผˆ๏ผป๏ฝ›๏ฝข๏ฝฃ๏ฝ๏ผฝ๏ผ‰ใ€•ใ€ใ€ใ€‰โ€™๏ฝ€๏ฝžโ€ฆ'; for (let i = 0; i < FORCE_TERMINATION_CHARACTERS.length; i++) { _classifier.set(FORCE_TERMINATION_CHARACTERS.charCodeAt(i), CharacterClass.ForceTermination); } diff --git a/lib/vscode/src/vs/editor/common/modes/supports/onEnter.ts b/lib/vscode/src/vs/editor/common/modes/supports/onEnter.ts index c03b524394c1..4e868fbef155 100644 --- a/lib/vscode/src/vs/editor/common/modes/supports/onEnter.ts +++ b/lib/vscode/src/vs/editor/common/modes/supports/onEnter.ts @@ -64,7 +64,12 @@ export class OnEnterSupport { reg: rule.previousLineText, text: previousLineText }].every((obj): boolean => { - return obj.reg ? obj.reg.test(obj.text) : true; + if (!obj.reg) { + return true; + } + + obj.reg.lastIndex = 0; // To disable the effect of the "g" flag. + return obj.reg.test(obj.text); }); if (regResult) { diff --git a/lib/vscode/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/lib/vscode/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index 939470195c94..2e0cddcc5fb8 100644 --- a/lib/vscode/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/lib/vscode/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -239,6 +239,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor } return { + description: 'marker-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className, showIfCollapsed: true, diff --git a/lib/vscode/src/vs/editor/common/standalone/standaloneEnums.ts b/lib/vscode/src/vs/editor/common/standalone/standaloneEnums.ts index 9860ffa9285d..c8389af9446a 100644 --- a/lib/vscode/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/lib/vscode/src/vs/editor/common/standalone/standaloneEnums.ts @@ -219,82 +219,84 @@ export enum EditorOption { highlightActiveIndentGuide = 49, hover = 50, inDiffEditor = 51, - letterSpacing = 52, - lightbulb = 53, - lineDecorationsWidth = 54, - lineHeight = 55, - lineNumbers = 56, - lineNumbersMinChars = 57, - linkedEditing = 58, - links = 59, - matchBrackets = 60, - minimap = 61, - mouseStyle = 62, - mouseWheelScrollSensitivity = 63, - mouseWheelZoom = 64, - multiCursorMergeOverlapping = 65, - multiCursorModifier = 66, - multiCursorPaste = 67, - occurrencesHighlight = 68, - overviewRulerBorder = 69, - overviewRulerLanes = 70, - padding = 71, - parameterHints = 72, - peekWidgetDefaultFocus = 73, - definitionLinkOpensInPeek = 74, - quickSuggestions = 75, - quickSuggestionsDelay = 76, - readOnly = 77, - renameOnType = 78, - renderControlCharacters = 79, - renderIndentGuides = 80, - renderFinalNewline = 81, - renderLineHighlight = 82, - renderLineHighlightOnlyWhenFocus = 83, - renderValidationDecorations = 84, - renderWhitespace = 85, - revealHorizontalRightPadding = 86, - roundedSelection = 87, - rulers = 88, - scrollbar = 89, - scrollBeyondLastColumn = 90, - scrollBeyondLastLine = 91, - scrollPredominantAxis = 92, - selectionClipboard = 93, - selectionHighlight = 94, - selectOnLineNumbers = 95, - showFoldingControls = 96, - showUnused = 97, - snippetSuggestions = 98, - smartSelect = 99, - smoothScrolling = 100, - stickyTabStops = 101, - stopRenderingLineAfter = 102, - suggest = 103, - suggestFontSize = 104, - suggestLineHeight = 105, - suggestOnTriggerCharacters = 106, - suggestSelection = 107, - tabCompletion = 108, - tabIndex = 109, - unusualLineTerminators = 110, - useTabStops = 111, - wordSeparators = 112, - wordWrap = 113, - wordWrapBreakAfterCharacters = 114, - wordWrapBreakBeforeCharacters = 115, - wordWrapColumn = 116, - wordWrapOverride1 = 117, - wordWrapOverride2 = 118, - wrappingIndent = 119, - wrappingStrategy = 120, - showDeprecated = 121, - inlineHints = 122, - editorClassName = 123, - pixelRatio = 124, - tabFocusMode = 125, - layoutInfo = 126, - wrappingInfo = 127 + inlineSuggest = 52, + letterSpacing = 53, + lightbulb = 54, + lineDecorationsWidth = 55, + lineHeight = 56, + lineNumbers = 57, + lineNumbersMinChars = 58, + linkedEditing = 59, + links = 60, + matchBrackets = 61, + minimap = 62, + mouseStyle = 63, + mouseWheelScrollSensitivity = 64, + mouseWheelZoom = 65, + multiCursorMergeOverlapping = 66, + multiCursorModifier = 67, + multiCursorPaste = 68, + occurrencesHighlight = 69, + overviewRulerBorder = 70, + overviewRulerLanes = 71, + padding = 72, + parameterHints = 73, + peekWidgetDefaultFocus = 74, + definitionLinkOpensInPeek = 75, + quickSuggestions = 76, + quickSuggestionsDelay = 77, + readOnly = 78, + renameOnType = 79, + renderControlCharacters = 80, + renderIndentGuides = 81, + renderFinalNewline = 82, + renderLineHighlight = 83, + renderLineHighlightOnlyWhenFocus = 84, + renderValidationDecorations = 85, + renderWhitespace = 86, + revealHorizontalRightPadding = 87, + roundedSelection = 88, + rulers = 89, + scrollbar = 90, + scrollBeyondLastColumn = 91, + scrollBeyondLastLine = 92, + scrollPredominantAxis = 93, + selectionClipboard = 94, + selectionHighlight = 95, + selectOnLineNumbers = 96, + showFoldingControls = 97, + showUnused = 98, + snippetSuggestions = 99, + smartSelect = 100, + smoothScrolling = 101, + stickyTabStops = 102, + stopRenderingLineAfter = 103, + suggest = 104, + suggestFontSize = 105, + suggestLineHeight = 106, + suggestOnTriggerCharacters = 107, + suggestSelection = 108, + tabCompletion = 109, + tabIndex = 110, + unusualLineTerminators = 111, + useShadowDOM = 112, + useTabStops = 113, + wordSeparators = 114, + wordWrap = 115, + wordWrapBreakAfterCharacters = 116, + wordWrapBreakBeforeCharacters = 117, + wordWrapColumn = 118, + wordWrapOverride1 = 119, + wordWrapOverride2 = 120, + wrappingIndent = 121, + wrappingStrategy = 122, + showDeprecated = 123, + inlayHints = 124, + editorClassName = 125, + pixelRatio = 126, + tabFocusMode = 127, + layoutInfo = 128, + wrappingInfo = 129 } /** @@ -353,12 +355,28 @@ export enum IndentAction { Outdent = 3 } -export enum InlineHintKind { +export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2 } +/** + * How an {@link InlineCompletionsProvider inline completion provider} was triggered. + */ +export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1 +} + /** * Virtual Key Codes, the value does not hold any inherent meaning. * Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx diff --git a/lib/vscode/src/vs/editor/common/view/editorColorRegistry.ts b/lib/vscode/src/vs/editor/common/view/editorColorRegistry.ts index 89c3c90dc59f..df5cdcf3e1f4 100644 --- a/lib/vscode/src/vs/editor/common/view/editorColorRegistry.ts +++ b/lib/vscode/src/vs/editor/common/view/editorColorRegistry.ts @@ -43,6 +43,9 @@ export const editorGutter = registerColor('editorGutter.background', { dark: edi export const editorUnnecessaryCodeBorder = registerColor('editorUnnecessaryCode.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('unnecessaryCodeBorder', 'Border color of unnecessary (unused) source code in the editor.')); export const editorUnnecessaryCodeOpacity = registerColor('editorUnnecessaryCode.opacity', { dark: Color.fromHex('#000a'), light: Color.fromHex('#0007'), hc: null }, nls.localize('unnecessaryCodeOpacity', 'Opacity of unnecessary (unused) source code in the editor. For example, "#000000c0" will render the code with 75% opacity. For high contrast themes, use the \'editorUnnecessaryCode.border\' theme color to underline unnecessary code instead of fading it out.')); +export const ghostTextBorder = registerColor('editorGhostText.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('editorGhostTextBorder', 'Border color of ghost text in the editor.')); +export const ghostTextForeground = registerColor('editorGhostText.foreground', { dark: Color.fromHex('#ffffff56'), light: Color.fromHex('#0007'), hc: null }, nls.localize('editorGhostTextForeground', 'Foreground color of the ghost text in the editor.')); + const rulerRangeDefault = new Color(new RGBA(0, 122, 204, 0.6)); export const overviewRulerRangeHighlight = registerColor('editorOverviewRuler.rangeHighlightForeground', { dark: rulerRangeDefault, light: rulerRangeDefault, hc: rulerRangeDefault }, nls.localize('overviewRulerRangeHighlight', 'Overview ruler marker color for range highlights. The color must not be opaque so as not to hide underlying decorations.'), true); export const overviewRulerError = registerColor('editorOverviewRuler.errorForeground', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('overviewRuleError', 'Overview ruler marker color for errors.')); diff --git a/lib/vscode/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/lib/vscode/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 5a6540d79610..76f0fc654307 100644 --- a/lib/vscode/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/lib/vscode/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -47,6 +47,10 @@ class LinePart { public isWhitespace(): boolean { return (this.metadata & LinePartMetadata.IS_WHITESPACE_MASK ? true : false); } + + public isPseudoAfter(): boolean { + return (this.metadata & LinePartMetadata.PSEUDO_AFTER_MASK ? true : false); + } } export class LineRange { @@ -792,14 +796,11 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP const lastTokenEndIndex = tokens[tokens.length - 1].endIndex; if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { - let classNames: string[] = []; - let metadata = 0; while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { - classNames.push(lineDecorations[lineDecorationIndex].className); - metadata |= lineDecorations[lineDecorationIndex].metadata; + const lineDecoration = lineDecorations[lineDecorationIndex]; + result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata); lineDecorationIndex++; } - result[resultLen++] = new LinePart(lastResultEndIndex, classNames.join(' '), metadata); } return result; @@ -827,6 +828,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const renderControlCharacters = input.renderControlCharacters; const characterMapping = new CharacterMapping(len + 1, parts.length); + let lastCharacterMappingDefined = false; let charIndex = 0; let visibleColumn = startVisibleColumn; @@ -850,7 +852,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const partType = part.type; const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace()); const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements); - const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.metadata === LinePartMetadata.PSEUDO_AFTER); + const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter()); charOffsetInPart = 0; sb.appendASCIIString(' anchor, getActions: () => menuActions, onHide: () => { diff --git a/lib/vscode/src/vs/editor/contrib/colorPicker/colorDetector.ts b/lib/vscode/src/vs/editor/contrib/colorPicker/colorDetector.ts index 4579e3c2daf9..50ca4ec1f56e 100644 --- a/lib/vscode/src/vs/editor/contrib/colorPicker/colorDetector.ts +++ b/lib/vscode/src/vs/editor/contrib/colorPicker/colorDetector.ts @@ -178,7 +178,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { let key = 'colorBox-' + subKey; if (!this._decorationsTypes.has(key) && !newDecorationsTypes[key]) { - this._codeEditorService.registerDecorationType(key, { + this._codeEditorService.registerDecorationType('color-detector-color', key, { before: { contentText: ' ', border: 'solid 0.1em #000', diff --git a/lib/vscode/src/vs/editor/contrib/contextmenu/contextmenu.ts b/lib/vscode/src/vs/editor/contrib/contextmenu/contextmenu.ts index 8a2a53b8dde0..a325a4086335 100644 --- a/lib/vscode/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/lib/vscode/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -23,6 +23,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { isIOS } from 'vs/base/common/platform'; export class ContextMenuController implements IEditorContribution { @@ -135,7 +136,8 @@ export class ContextMenuController implements IEditorContribution { } // Find actions available for menu - const menuActions = this._getMenuActions(this._editor.getModel(), MenuId.EditorContext); + const menuActions = this._getMenuActions(this._editor.getModel(), + this._editor.isSimpleWidget ? MenuId.SimpleEditorContext : MenuId.EditorContext); // Show menu if we have actions to show if (menuActions.length > 0) { @@ -208,10 +210,12 @@ export class ContextMenuController implements IEditorContribution { anchor = { x: posx, y: posy }; } + const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035 + // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ - domForShadowRoot: this._editor.getDomNode(), + domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined, getAnchor: () => anchor!, diff --git a/lib/vscode/src/vs/editor/contrib/dnd/dnd.ts b/lib/vscode/src/vs/editor/contrib/dnd/dnd.ts index 1f15af6828a9..4596072c0325 100644 --- a/lib/vscode/src/vs/editor/contrib/dnd/dnd.ts +++ b/lib/vscode/src/vs/editor/contrib/dnd/dnd.ts @@ -204,6 +204,7 @@ export class DragAndDropController extends Disposable implements IEditorContribu } private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'dnd-target', className: 'dnd-target' }); diff --git a/lib/vscode/src/vs/editor/contrib/find/findController.ts b/lib/vscode/src/vs/editor/contrib/find/findController.ts index 596463ecae3c..b7f3bba4aef2 100644 --- a/lib/vscode/src/vs/editor/contrib/find/findController.ts +++ b/lib/vscode/src/vs/editor/contrib/find/findController.ts @@ -26,7 +26,6 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IThemeService } from 'vs/platform/theme/common/themeService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -502,12 +501,7 @@ export const StartFindAction = registerMultiEditorAction(new MultiEditorAction({ } })); -StartFindAction.addImplementation(0, (accessor: ServicesAccessor, args: any): boolean | Promise => { - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (!editor) { - return false; - } +StartFindAction.addImplementation(0, (accessor: ServicesAccessor, editor: ICodeEditor, args: any): boolean | Promise => { const controller = CommonFindController.get(editor); if (!controller) { return false; @@ -587,39 +581,16 @@ export class NextMatchFindAction extends MatchFindAction { label: nls.localize('findNextMatchAction', "Find Next"), alias: 'Find Next', precondition: undefined, - kbOpts: { + kbOpts: [{ kbExpr: EditorContextKeys.focus, primary: KeyCode.F3, mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }, weight: KeybindingWeight.EditorContrib - } - }); - } - - protected _run(controller: CommonFindController): boolean { - const result = controller.moveToNextMatch(); - if (result) { - controller.editor.pushUndoStop(); - return true; - } - - return false; - } -} - -export class NextMatchFindAction2 extends MatchFindAction { - - constructor() { - super({ - id: FIND_IDS.NextMatchFindAction, - label: nls.localize('findNextMatchAction', "Find Next"), - alias: 'Find Next', - precondition: undefined, - kbOpts: { + }, { kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib - } + }] }); } @@ -642,33 +613,17 @@ export class PreviousMatchFindAction extends MatchFindAction { label: nls.localize('findPreviousMatchAction', "Find Previous"), alias: 'Find Previous', precondition: undefined, - kbOpts: { + kbOpts: [{ kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyCode.F3, mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, weight: KeybindingWeight.EditorContrib - } - }); - } - - protected _run(controller: CommonFindController): boolean { - return controller.moveToPrevMatch(); - } -} - -export class PreviousMatchFindAction2 extends MatchFindAction { - - constructor() { - super({ - id: FIND_IDS.PreviousMatchFindAction, - label: nls.localize('findPreviousMatchAction', "Find Previous"), - alias: 'Find Previous', - precondition: undefined, - kbOpts: { + }, { kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), primary: KeyMod.Shift | KeyCode.Enter, weight: KeybindingWeight.EditorContrib } + ] }); } @@ -765,10 +720,8 @@ export const StartFindReplaceAction = registerMultiEditorAction(new MultiEditorA } })); -StartFindReplaceAction.addImplementation(0, (accessor: ServicesAccessor, args: any): boolean | Promise => { - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (!editor || !editor.hasModel() || editor.getOption(EditorOption.readOnly)) { +StartFindReplaceAction.addImplementation(0, (accessor: ServicesAccessor, editor: ICodeEditor, args: any): boolean | Promise => { + if (!editor.hasModel() || editor.getOption(EditorOption.readOnly)) { return false; } const controller = CommonFindController.get(editor); @@ -808,9 +761,7 @@ registerEditorContribution(CommonFindController.ID, FindController); registerEditorAction(StartFindWithSelectionAction); registerEditorAction(NextMatchFindAction); -registerEditorAction(NextMatchFindAction2); registerEditorAction(PreviousMatchFindAction); -registerEditorAction(PreviousMatchFindAction2); registerEditorAction(NextSelectionMatchFindAction); registerEditorAction(PreviousSelectionMatchFindAction); diff --git a/lib/vscode/src/vs/editor/contrib/find/findDecorations.ts b/lib/vscode/src/vs/editor/contrib/find/findDecorations.ts index c6b966e77c19..08b1bc807e55 100644 --- a/lib/vscode/src/vs/editor/contrib/find/findDecorations.ts +++ b/lib/vscode/src/vs/editor/contrib/find/findDecorations.ts @@ -276,6 +276,7 @@ export class FindDecorations implements IDisposable { } public static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'current-find-match', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', @@ -291,6 +292,7 @@ export class FindDecorations implements IDisposable { }); public static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + description: 'find-match', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true, @@ -305,12 +307,14 @@ export class FindDecorations implements IDisposable { }); public static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + description: 'find-match-no-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true }); private static readonly _FIND_MATCH_ONLY_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + description: 'find-match-only-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), @@ -319,12 +323,14 @@ export class FindDecorations implements IDisposable { }); private static readonly _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({ + description: 'find-range-highlight', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', isWholeLine: true }); private static readonly _FIND_SCOPE_DECORATION = ModelDecorationOptions.register({ + description: 'find-scope', className: 'findScope', isWholeLine: true }); diff --git a/lib/vscode/src/vs/editor/contrib/folding/folding.ts b/lib/vscode/src/vs/editor/contrib/folding/folding.ts index c6396a556db8..de52951d6e04 100644 --- a/lib/vscode/src/vs/editor/contrib/folding/folding.ts +++ b/lib/vscode/src/vs/editor/contrib/folding/folding.ts @@ -51,7 +51,7 @@ interface FoldingStateMemento { export class FoldingController extends Disposable implements IEditorContribution { - public static ID = 'editor.contrib.folding'; + public static readonly ID = 'editor.contrib.folding'; static readonly MAX_FOLDING_REGIONS = 5000; diff --git a/lib/vscode/src/vs/editor/contrib/folding/foldingDecorations.ts b/lib/vscode/src/vs/editor/contrib/folding/foldingDecorations.ts index 22601d1298e8..bbca380a7480 100644 --- a/lib/vscode/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/lib/vscode/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -17,6 +17,7 @@ export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.ch export class FoldingDecorationProvider implements IDecorationProvider { private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-collapsed-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true, @@ -24,6 +25,7 @@ export class FoldingDecorationProvider implements IDecorationProvider { }); private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-collapsed-highlighted-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', @@ -32,18 +34,21 @@ export class FoldingDecorationProvider implements IDecorationProvider { }); private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-expanded-auto-hide-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon) }); private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-expanded-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, isWholeLine: true, firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon) }); private static readonly HIDDEN_RANGE_DECORATION = ModelDecorationOptions.register({ + description: 'folding-hidden-range-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }); diff --git a/lib/vscode/src/vs/editor/contrib/folding/intializingRangeProvider.ts b/lib/vscode/src/vs/editor/contrib/folding/intializingRangeProvider.ts index 754ac39f0483..9064c178be51 100644 --- a/lib/vscode/src/vs/editor/contrib/folding/intializingRangeProvider.ts +++ b/lib/vscode/src/vs/editor/contrib/folding/intializingRangeProvider.ts @@ -28,6 +28,7 @@ export class InitializingRangeProvider implements RangeProvider { endColumn: editorModel.getLineLength(range.endLineNumber) }, options: { + description: 'folding-initializing-range-provider', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }; diff --git a/lib/vscode/src/vs/editor/contrib/folding/test/foldingModel.test.ts b/lib/vscode/src/vs/editor/contrib/folding/test/foldingModel.test.ts index 317f12223dd2..fb8275939e65 100644 --- a/lib/vscode/src/vs/editor/contrib/folding/test/foldingModel.test.ts +++ b/lib/vscode/src/vs/editor/contrib/folding/test/foldingModel.test.ts @@ -29,16 +29,19 @@ interface ExpectedDecoration { export class TestDecorationProvider { private static readonly collapsedDecoration = ModelDecorationOptions.register({ + description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); private static readonly expandedDecoration = ModelDecorationOptions.register({ + description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); private static readonly hiddenDecoration = ModelDecorationOptions.register({ + description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, linesDecorationsClassName: 'folding' }); diff --git a/lib/vscode/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/lib/vscode/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index 2152693b449e..1218703746d8 100644 --- a/lib/vscode/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/lib/vscode/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -190,7 +190,7 @@ abstract class SymbolNavigationAction extends EditorAction { if (highlight) { const modelNow = targetEditor.getModel(); - const ids = targetEditor.deltaDecorations([], [{ range, options: { className: 'symbolHighlight' } }]); + const ids = targetEditor.deltaDecorations([], [{ range, options: { description: 'symbol-navigate-action-highlight', className: 'symbolHighlight' } }]); setTimeout(() => { if (targetEditor.getModel() === modelNow) { targetEditor.deltaDecorations(ids, []); diff --git a/lib/vscode/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/lib/vscode/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index 68c3275a7a4a..5a4c164cd52c 100644 --- a/lib/vscode/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/lib/vscode/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -305,6 +305,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri const newDecorations: IModelDeltaDecoration = { range: range, options: { + description: 'goto-definition-link', inlineClassName: 'goto-definition-link', hoverMessage } diff --git a/lib/vscode/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/lib/vscode/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 9f93856f7da5..f1a11176a8b7 100644 --- a/lib/vscode/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/lib/vscode/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -40,6 +40,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; class DecorationsManager implements IDisposable { private static readonly DecorationOptions = ModelDecorationOptions.register({ + description: 'reference-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'reference-decoration' }); diff --git a/lib/vscode/src/vs/editor/contrib/hover/colorHoverParticipant.ts b/lib/vscode/src/vs/editor/contrib/hover/colorHoverParticipant.ts new file mode 100644 index 000000000000..0c17aa895268 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/hover/colorHoverParticipant.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { DocumentColorProvider, IColorInformation } from 'vs/editor/common/modes'; +import { IIdentifiedSingleEditOperation, IModelDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; +import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; +import { Color, RGBA } from 'vs/base/common/color'; +import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; +import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export class ColorHover implements IHoverPart { + + /** + * Force the hover to always be rendered at this specific range, + * even in the case of multiple hover parts. + */ + public readonly forceShowAtRange: boolean = true; + + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range, + public readonly model: ColorPickerModel, + public readonly provider: DocumentColorProvider + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +export class ColorHoverParticipant implements IEditorHoverParticipant { + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IThemeService private readonly _themeService: IThemeService, + ) { } + + public computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): ColorHover[] { + return []; + } + + public async computeAsync(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): Promise { + if (!this._editor.hasModel()) { + return []; + } + const colorDetector = ColorDetector.get(this._editor); + for (const d of lineDecorations) { + const colorData = colorDetector.getColorData(d.range.getStartPosition()); + if (colorData) { + const colorHover = await this._createColorHover(this._editor.getModel(), colorData.colorInfo, colorData.provider); + return [colorHover]; + } + } + return []; + } + + private async _createColorHover(editorModel: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider): Promise { + const originalText = editorModel.getValueInRange(colorInfo.range); + const { red, green, blue, alpha } = colorInfo.color; + const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha); + const color = new Color(rgba); + + const colorPresentations = await getColorPresentations(editorModel, colorInfo, provider, CancellationToken.None); + const model = new ColorPickerModel(color, [], 0); + model.colorPresentations = colorPresentations || []; + model.guessColorPresentation(color, originalText); + + return new ColorHover(this, Range.lift(colorInfo.range), model, provider); + } + + public renderHoverParts(hoverParts: ColorHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable { + if (hoverParts.length === 0 || !this._editor.hasModel()) { + return Disposable.None; + } + + const disposables = new DisposableStore(); + const colorHover = hoverParts[0]; + const editorModel = this._editor.getModel(); + const model = colorHover.model; + const widget = disposables.add(new ColorPickerWidget(fragment, model, this._editor.getOption(EditorOption.pixelRatio), this._themeService)); + + let range = new Range(colorHover.range.startLineNumber, colorHover.range.startColumn, colorHover.range.endLineNumber, colorHover.range.endColumn); + + const updateEditorModel = () => { + let textEdits: IIdentifiedSingleEditOperation[]; + let newRange: Range; + if (model.presentation.textEdit) { + textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation]; + newRange = new Range( + model.presentation.textEdit.range.startLineNumber, + model.presentation.textEdit.range.startColumn, + model.presentation.textEdit.range.endLineNumber, + model.presentation.textEdit.range.endColumn + ); + const trackedRange = this._editor.getModel()!._setTrackedRange(null, newRange, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); + this._editor.pushUndoStop(); + this._editor.executeEdits('colorpicker', textEdits); + newRange = this._editor.getModel()!._getTrackedRange(trackedRange) || newRange; + } else { + textEdits = [{ identifier: null, range, text: model.presentation.label, forceMoveMarkers: false }]; + newRange = range.setEndPosition(range.endLineNumber, range.startColumn + model.presentation.label.length); + this._editor.pushUndoStop(); + this._editor.executeEdits('colorpicker', textEdits); + } + + if (model.presentation.additionalTextEdits) { + textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]]; + this._editor.executeEdits('colorpicker', textEdits); + this._hover.hide(); + } + this._editor.pushUndoStop(); + range = newRange; + }; + + const updateColorPresentations = (color: Color) => { + return getColorPresentations(editorModel, { + range: range, + color: { + red: color.rgba.r / 255, + green: color.rgba.g / 255, + blue: color.rgba.b / 255, + alpha: color.rgba.a + } + }, colorHover.provider, CancellationToken.None).then((colorPresentations) => { + model.colorPresentations = colorPresentations || []; + }); + }; + + disposables.add(model.onColorFlushed((color: Color) => { + updateColorPresentations(color).then(updateEditorModel); + })); + disposables.add(model.onDidChangeColor(updateColorPresentations)); + + this._hover.setColorPicker(widget); + + return disposables; + } +} diff --git a/lib/vscode/src/vs/editor/contrib/hover/hover.ts b/lib/vscode/src/vs/editor/contrib/hover/hover.ts index 276ad87a8ffe..9d8a6c8ee370 100644 --- a/lib/vscode/src/vs/editor/contrib/hover/hover.ts +++ b/lib/vscode/src/vs/editor/contrib/hover/hover.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -21,7 +20,7 @@ import { ModesGlyphHoverWidget } from 'vs/editor/contrib/hover/modesGlyphHover'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorHoverBackground, editorHoverBorder, editorHoverHighlight, textCodeBlockBackground, textLinkForeground, editorHoverStatusBarBackground, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -52,7 +51,6 @@ export class ModesHoverController implements IEditorContribution { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IModeService private readonly _modeService: IModeService, - @IThemeService private readonly _themeService: IThemeService, @IContextKeyService _contextKeyService: IContextKeyService ) { this._isMouseDown = false; @@ -166,45 +164,27 @@ export class ModesHoverController implements IEditorContribution { return; } - if (targetType === MouseTargetType.CONTENT_EMPTY) { - const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; - const data = mouseEvent.target.detail; - if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.horizontalDistanceToText < epsilon) { - // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough - targetType = MouseTargetType.CONTENT_TEXT; - } + if (!this._isHoverEnabled) { + this._hideWidgets(); + return; } - if (targetType === MouseTargetType.CONTENT_TEXT) { + const contentWidget = this._getOrCreateContentWidget(); + if (contentWidget.maybeShowAt(mouseEvent)) { this._glyphWidget?.hide(); + return; + } - if (this._isHoverEnabled && mouseEvent.target.range) { - // TODO@rebornix. This should be removed if we move Color Picker out of Hover component. - // Check if mouse is hovering on color decorator - const hoverOnColorDecorator = [...mouseEvent.target.element?.classList.values() || []].find(className => className.startsWith('ced-colorBox')) - && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1; - const showAtRange = ( - hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character. - ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1) - : mouseEvent.target.range - ); - if (!this._contentWidget) { - this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); - } - this._contentWidget.startShowingAt(showAtRange, HoverStartMode.Delayed, false); - } - } else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) { + if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN && mouseEvent.target.position) { this._contentWidget?.hide(); - - if (this._isHoverEnabled && mouseEvent.target.position) { - if (!this._glyphWidget) { - this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); - } - this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + if (!this._glyphWidget) { + this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); } - } else { - this._hideWidgets(); + this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + return; } + + this._hideWidgets(); } private _onKeyDown(e: IKeyboardEvent): void { @@ -224,15 +204,19 @@ export class ModesHoverController implements IEditorContribution { this._contentWidget?.hide(); } + private _getOrCreateContentWidget(): ModesContentHoverWidget { + if (!this._contentWidget) { + this._contentWidget = this._instantiationService.createInstance(ModesContentHoverWidget, this._editor, this._hoverVisibleKey); + } + return this._contentWidget; + } + public isColorPickerVisible(): boolean { return this._contentWidget?.isColorPickerVisible() || false; } public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void { - if (!this._contentWidget) { - this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); - } - this._contentWidget.startShowingAt(range, mode, focus); + this._getOrCreateContentWidget().startShowingAtRange(range, mode, focus); } public dispose(): void { diff --git a/lib/vscode/src/vs/editor/contrib/hover/hoverTypes.ts b/lib/vscode/src/vs/editor/contrib/hover/hoverTypes.ts new file mode 100644 index 000000000000..2515385bfa48 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/hover/hoverTypes.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; +import { IModelDecoration } from 'vs/editor/common/model'; + +export interface IHoverPart { + /** + * The creator of this hover part. + */ + readonly owner: IEditorHoverParticipant; + /** + * The range where this hover part applies. + */ + readonly range: Range; + /** + * Force the hover to always be rendered at this specific range, + * even in the case of multiple hover parts. + */ + readonly forceShowAtRange?: boolean; + + isValidForHoverAnchor(anchor: HoverAnchor): boolean; +} + +export interface IEditorHover { + hide(): void; + onContentsChanged(): void; + setColorPicker(widget: ColorPickerWidget): void; +} + +export const enum HoverAnchorType { + Range = 1, + ForeignElement = 2 +} + +export class HoverRangeAnchor { + public readonly type = HoverAnchorType.Range; + constructor( + public readonly priority: number, + public readonly range: Range + ) { + } + public equals(other: HoverAnchor) { + return (other.type === HoverAnchorType.Range && this.range.equalsRange(other.range)); + } + public canAdoptVisibleHover(lastAnchor: HoverAnchor, showAtPosition: Position): boolean { + return (lastAnchor.type === HoverAnchorType.Range && showAtPosition.lineNumber === this.range.startLineNumber); + } +} + +export class HoverForeignElementAnchor { + public readonly type = HoverAnchorType.ForeignElement; + constructor( + public readonly priority: number, + public readonly owner: IEditorHoverParticipant, + public readonly range: Range + ) { + } + public equals(other: HoverAnchor) { + return (other.type === HoverAnchorType.ForeignElement && this.owner === other.owner); + } + public canAdoptVisibleHover(lastAnchor: HoverAnchor, showAtPosition: Position): boolean { + return (lastAnchor.type === HoverAnchorType.ForeignElement && this.owner === lastAnchor.owner); + } +} + +export type HoverAnchor = HoverRangeAnchor | HoverForeignElementAnchor; + +export interface IEditorHoverStatusBar { + addAction(actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): void; + append(element: HTMLElement): HTMLElement; +} + +export interface IEditorHoverParticipant { + suggestHoverAnchor?(mouseEvent: IEditorMouseEvent): HoverAnchor | null; + computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): T[]; + computeAsync?(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): Promise; + createLoadingMessage?(anchor: HoverAnchor): T | null; + renderHoverParts(hoverParts: T[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable; +} diff --git a/lib/vscode/src/vs/editor/contrib/hover/markdownHoverParticipant.ts b/lib/vscode/src/vs/editor/contrib/hover/markdownHoverParticipant.ts index ec4442dbd5a7..ee67c5c3e420 100644 --- a/lib/vscode/src/vs/editor/contrib/hover/markdownHoverParticipant.ts +++ b/lib/vscode/src/vs/editor/contrib/hover/markdownHoverParticipant.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; @@ -14,7 +14,7 @@ import { asArray } from 'vs/base/common/arrays'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelDecoration } from 'vs/editor/common/model'; -import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; +import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; import { HoverProviderRegistry } from 'vs/editor/common/modes'; import { getHover } from 'vs/editor/contrib/hover/getHover'; import { Position } from 'vs/editor/common/core/position'; @@ -25,15 +25,17 @@ const $ = dom.$; export class MarkdownHover implements IHoverPart { constructor( + public readonly owner: IEditorHoverParticipant, public readonly range: Range, public readonly contents: IMarkdownString[] ) { } - public equals(other: IHoverPart): boolean { - if (other instanceof MarkdownHover) { - return markedStringsEquals(this.contents, other.contents); - } - return false; + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); } } @@ -46,17 +48,20 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { - if (!this._editor.hasModel() || !range) { + public async computeAsync(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): Promise { + if (!this._editor.hasModel() || anchor.type !== HoverAnchorType.Range) { return Promise.resolve([]); } @@ -87,8 +92,8 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant, public readonly range: Range, public readonly marker: IMarker, ) { } - public equals(other: IHoverPart): boolean { - if (other instanceof MarkerHover) { - return IMarkerData.makeKey(this.marker) === IMarkerData.makeKey(other.marker); - } - return false; + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); } } @@ -58,17 +58,16 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant fragment.appendChild(this.renderMarkerHover(msg, disposables))); const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; - fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar, disposables)); + this.renderMarkerStatusbar(markerHoverForStatusbar, statusBar, disposables); return disposables; } @@ -165,11 +164,9 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { @@ -177,11 +174,11 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { @@ -231,17 +228,9 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant void, commandId: string }): IDisposable { - const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - return renderHoverAction(parent, actionOptions, keybindingLabel); } private getCodeActions(marker: IMarker): CancelablePromise { diff --git a/lib/vscode/src/vs/editor/contrib/hover/modesContentHover.ts b/lib/vscode/src/vs/editor/contrib/hover/modesContentHover.ts index ade944741b10..8512b9261e79 100644 --- a/lib/vscode/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/lib/vscode/src/vs/editor/contrib/hover/modesContentHover.ts @@ -5,21 +5,17 @@ import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Color, RGBA } from 'vs/base/common/color'; -import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { DocumentColorProvider, IColor, TokenizationRegistry } from 'vs/editor/common/modes'; -import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; -import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; -import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; +import { TokenizationRegistry } from 'vs/editor/common/modes'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { coalesce } from 'vs/base/common/arrays'; -import { IIdentifiedSingleEditOperation, IModelDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { coalesce, flatten } from 'vs/base/common/arrays'; +import { IModelDecoration } from 'vs/editor/common/model'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -27,65 +23,67 @@ import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Widget } from 'vs/base/browser/ui/widget'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; -import { MarkerHover, MarkerHoverParticipant } from 'vs/editor/contrib/hover/markerHoverParticipant'; +import { HoverWidget, renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/markerHoverParticipant'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkdownHover, MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; +import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant'; +import { ColorHoverParticipant } from 'vs/editor/contrib/hover/colorHoverParticipant'; +import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IEditorHoverStatusBar, IHoverPart, HoverAnchor, IEditorHoverParticipant, HoverAnchorType, IEditorHover, HoverRangeAnchor } from 'vs/editor/contrib/hover/hoverTypes'; -export interface IHoverPart { - readonly range: Range; - equals(other: IHoverPart): boolean; -} +const $ = dom.$; -export interface IEditorHover { - hide(): void; - onContentsChanged(): void; -} +class EditorHoverStatusBar extends Disposable implements IEditorHoverStatusBar { -export interface IEditorHoverParticipant { - computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): T[]; - computeAsync?(range: Range, token: CancellationToken): Promise; - renderHoverParts(hoverParts: T[], fragment: DocumentFragment): IDisposable; -} + public readonly hoverElement: HTMLElement; + private readonly actionsElement: HTMLElement; + private _hasContent: boolean = false; -class ColorHover implements IHoverPart { + public get hasContent() { + return this._hasContent; + } constructor( - public readonly range: Range, - public readonly color: IColor, - public readonly provider: DocumentColorProvider - ) { } + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + super(); + this.hoverElement = $('div.hover-row.status-bar'); + this.actionsElement = dom.append(this.hoverElement, $('div.actions')); + } - equals(other: IHoverPart): boolean { - return false; + public addAction(actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): void { + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + this._register(renderHoverAction(this.actionsElement, actionOptions, keybindingLabel)); + this._hasContent = true; } -} -class HoverPartInfo { - constructor( - public readonly owner: IEditorHoverParticipant | null, - public readonly data: IHoverPart - ) { } + public append(element: HTMLElement): HTMLElement { + const result = dom.append(this.actionsElement, element); + this._hasContent = true; + return result; + } } -class ModesContentComputer implements IHoverComputer { +class ModesContentComputer implements IHoverComputer { private readonly _editor: ICodeEditor; - private _result: HoverPartInfo[]; - private _range: Range | null; + private _result: IHoverPart[]; + private _anchor: HoverAnchor | null; constructor( editor: ICodeEditor, - private readonly _markerHoverParticipant: IEditorHoverParticipant, - private readonly _markdownHoverParticipant: MarkdownHoverParticipant + private readonly _participants: readonly IEditorHoverParticipant[] ) { this._editor = editor; this._result = []; - this._range = null; + this._anchor = null; } - public setRange(range: Range): void { - this._range = range; + public setAnchor(anchor: HoverAnchor): void { + this._anchor = anchor; this._result = []; } @@ -93,65 +91,64 @@ class ModesContentComputer implements IHoverComputer { this._result = []; } - public async computeAsync(token: CancellationToken): Promise { - if (!this._editor.hasModel() || !this._range) { - return Promise.resolve([]); - } - - const markdownHovers = await this._markdownHoverParticipant.computeAsync(this._range, token); - return markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h)); - } - - public computeSync(): HoverPartInfo[] { - if (!this._editor.hasModel() || !this._range) { - return []; - } - - const model = this._editor.getModel(); - const hoverRange = this._range; - const lineNumber = hoverRange.startLineNumber; - - if (lineNumber > this._editor.getModel().getLineCount()) { - // Illegal line number => no results + private static _getLineDecorations(editor: IActiveCodeEditor, anchor: HoverAnchor): IModelDecoration[] { + if (anchor.type !== HoverAnchorType.Range) { return []; } + const model = editor.getModel(); + const lineNumber = anchor.range.startLineNumber; const maxColumn = model.getLineMaxColumn(lineNumber); - const lineDecorations = this._editor.getLineDecorations(lineNumber).filter((d) => { + return editor.getLineDecorations(lineNumber).filter((d) => { if (d.options.isWholeLine) { return true; } const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; - if (startColumn > hoverRange.startColumn || hoverRange.endColumn > endColumn) { + if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) { return false; } return true; }); + } - let result: HoverPartInfo[] = []; + public async computeAsync(token: CancellationToken): Promise { + const anchor = this._anchor; - const colorDetector = ColorDetector.get(this._editor); - for (const d of lineDecorations) { - const colorData = colorDetector.getColorData(d.range.getStartPosition()); - if (colorData) { - const { color, range } = colorData.colorInfo; - result.push(new HoverPartInfo(null, new ColorHover(Range.lift(range), color, colorData.provider))); - break; - } + if (!this._editor.hasModel() || !anchor) { + return Promise.resolve([]); } - const markdownHovers = this._markdownHoverParticipant.computeSync(this._range, lineDecorations); - result = result.concat(markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h))); + const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, anchor); - const markerHovers = this._markerHoverParticipant.computeSync(this._range, lineDecorations); - result = result.concat(markerHovers.map(h => new HoverPartInfo(this._markerHoverParticipant, h))); + const allResults = await Promise.all(this._participants.map(p => this._computeAsync(p, lineDecorations, anchor, token))); + return flatten(allResults); + } + + private async _computeAsync(participant: IEditorHoverParticipant, lineDecorations: IModelDecoration[], anchor: HoverAnchor, token: CancellationToken): Promise { + if (!participant.computeAsync) { + return []; + } + return participant.computeAsync(anchor, lineDecorations, token); + } + + public computeSync(): IHoverPart[] { + if (!this._editor.hasModel() || !this._anchor) { + return []; + } + + const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, this._anchor); + + let result: IHoverPart[] = []; + for (const participant of this._participants) { + result = result.concat(participant.computeSync(this._anchor, lineDecorations)); + } return coalesce(result); } - public onResult(result: HoverPartInfo[], isFromSynchronousComputation: boolean): void { + public onResult(result: IHoverPart[], isFromSynchronousComputation: boolean): void { // Always put synchronous messages before asynchronous ones if (isFromSynchronousComputation) { this._result = result.concat(this._result); @@ -160,15 +157,20 @@ class ModesContentComputer implements IHoverComputer { } } - public getResult(): HoverPartInfo[] { + public getResult(): IHoverPart[] { return this._result.slice(0); } - public getResultWithLoadingMessage(): HoverPartInfo[] { - if (this._range) { - const loadingMessage = new HoverPartInfo(this._markdownHoverParticipant, this._markdownHoverParticipant.createLoadingMessage(this._range)); - return this._result.slice(0).concat([loadingMessage]); - + public getResultWithLoadingMessage(): IHoverPart[] { + if (this._anchor) { + for (const participant of this._participants) { + if (participant.createLoadingMessage) { + const loadingMessage = participant.createLoadingMessage(this._anchor); + if (loadingMessage) { + return this._result.slice(0).concat([loadingMessage]); + } + } + } } return this._result.slice(0); } @@ -178,8 +180,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I static readonly ID = 'editor.contrib.modesContentHoverWidget'; - private readonly _markerHoverParticipant: IEditorHoverParticipant; - private readonly _markdownHoverParticipant: MarkdownHoverParticipant; + private readonly _participants: IEditorHoverParticipant[]; private readonly _hover: HoverWidget; private readonly _id: string; @@ -192,10 +193,10 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I // IContentWidget.allowEditorOverflow public readonly allowEditorOverflow = true; - private _messages: HoverPartInfo[]; - private _lastRange: Range | null; + private _messages: IHoverPart[]; + private _lastAnchor: HoverAnchor | null; private readonly _computer: ModesContentComputer; - private readonly _hoverOperation: HoverOperation; + private readonly _hoverOperation: HoverOperation; private _highlightDecorations: string[]; private _isChangingDecorations: boolean; private _shouldFocus: boolean; @@ -205,13 +206,17 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I constructor( editor: ICodeEditor, private readonly _hoverVisibleKey: IContextKey, - instantiationService: IInstantiationService, - private readonly _themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); - this._markerHoverParticipant = instantiationService.createInstance(MarkerHoverParticipant, editor, this); - this._markdownHoverParticipant = instantiationService.createInstance(MarkdownHoverParticipant, editor, this); + this._participants = [ + instantiationService.createInstance(ColorHoverParticipant, editor, this), + instantiationService.createInstance(MarkdownHoverParticipant, editor, this), + instantiationService.createInstance(InlineCompletionsHoverParticipant, editor, this), + instantiationService.createInstance(MarkerHoverParticipant, editor, this), + ]; this._hover = this._register(new HoverWidget()); this._id = ModesContentHoverWidget.ID; @@ -241,8 +246,8 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I this._stoleFocus = false; this._messages = []; - this._lastRange = null; - this._computer = new ModesContentComputer(this._editor, this._markerHoverParticipant, this._markdownHoverParticipant); + this._lastAnchor = null; + this._computer = new ModesContentComputer(this._editor, this._participants); this._highlightDecorations = []; this._isChangingDecorations = false; this._shouldFocus = false; @@ -268,26 +273,9 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay); })); this._register(TokenizationRegistry.onDidChange(() => { - if (this._isVisible && this._lastRange && this._messages.length > 0) { - this._messages = this._messages.map(msg => { - // If a color hover is visible, we need to update the message that - // created it so that the color matches the last chosen color - if (msg.data instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.data.range) && this._colorPicker?.model.color) { - const color = this._colorPicker.model.color; - const newColor = { - red: color.rgba.r / 255, - green: color.rgba.g / 255, - blue: color.rgba.b / 255, - alpha: color.rgba.a - }; - return new HoverPartInfo(msg.owner, new ColorHover(msg.data.range, newColor, msg.data.provider)); - } else { - return msg; - } - }); - + if (this._isVisible && this._lastAnchor && this._messages.length > 0) { this._hover.contentsDomNode.textContent = ''; - this._renderMessages(this._lastRange, this._messages); + this._renderMessages(this._lastAnchor, this._messages); } })); } @@ -306,7 +294,60 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I return this._hover.containerDomNode; } - public showAt(position: Position, range: Range | null, focus: boolean): void { + private _shouldShowAt(mouseEvent: IEditorMouseEvent): boolean { + const targetType = mouseEvent.target.type; + if (targetType === MouseTargetType.CONTENT_TEXT) { + return true; + } + + if (targetType === MouseTargetType.CONTENT_EMPTY) { + const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; + const data = mouseEvent.target.detail; + if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.horizontalDistanceToText < epsilon) { + // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough + return true; + } + } + + return false; + } + + public maybeShowAt(mouseEvent: IEditorMouseEvent): boolean { + const anchorCandidates: HoverAnchor[] = []; + + for (const participant of this._participants) { + if (typeof participant.suggestHoverAnchor === 'function') { + const anchor = participant.suggestHoverAnchor(mouseEvent); + if (anchor) { + anchorCandidates.push(anchor); + } + } + } + + if (this._shouldShowAt(mouseEvent) && mouseEvent.target.range) { + // TODO@rebornix. This should be removed if we move Color Picker out of Hover component. + // Check if mouse is hovering on color decorator + const hoverOnColorDecorator = [...mouseEvent.target.element?.classList.values() || []].find(className => className.startsWith('ced-colorBox')) + && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1; + const showAtRange = ( + hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character. + ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1) + : mouseEvent.target.range + ); + anchorCandidates.push(new HoverRangeAnchor(0, showAtRange)); + } + + if (anchorCandidates.length === 0) { + return false; + } + + anchorCandidates.sort((a, b) => b.priority - a.priority); + this._startShowingAt(anchorCandidates[0], HoverStartMode.Delayed, false); + + return true; + } + + private _showAt(position: Position, range: Range | null, focus: boolean): void { // Position has changed this._showAtPosition = position; this._showAtRange = range; @@ -378,8 +419,12 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I } } - public startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { - if (this._lastRange && this._lastRange.equalsRange(range)) { + public startShowingAtRange(range: Range, mode: HoverStartMode, focus: boolean): void { + this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus); + } + + private _startShowingAt(anchor: HoverAnchor, mode: HoverStartMode, focus: boolean): void { + if (this._lastAnchor && this._lastAnchor.equals(anchor)) { // We have to show the widget at the exact same range as before, so no work is needed return; } @@ -390,36 +435,29 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I // The range might have changed, but the hover is visible // Instead of hiding it completely, filter out messages that are still in the new range and // kick off a new computation - if (!this._showAtPosition || this._showAtPosition.lineNumber !== range.startLineNumber) { + if (!this._showAtPosition || !this._lastAnchor || !anchor.canAdoptVisibleHover(this._lastAnchor, this._showAtPosition)) { this.hide(); } else { - let filteredMessages: HoverPartInfo[] = []; - for (let i = 0, len = this._messages.length; i < len; i++) { - const msg = this._messages[i]; - const rng = msg.data.range; - if (rng && rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) { - filteredMessages.push(msg); - } - } - if (filteredMessages.length > 0) { - if (hoverContentsEquals(filteredMessages, this._messages)) { - return; - } - this._renderMessages(range, filteredMessages); - } else { + const filteredMessages = this._messages.filter((m) => m.isValidForHoverAnchor(anchor)); + if (filteredMessages.length === 0) { this.hide(); + } else if (filteredMessages.length === this._messages.length) { + // no change + return; + } else { + this._renderMessages(anchor, filteredMessages); } } } - this._lastRange = range; - this._computer.setRange(range); + this._lastAnchor = anchor; + this._computer.setAnchor(anchor); this._shouldFocus = focus; this._hoverOperation.start(mode); } public hide(): void { - this._lastRange = null; + this._lastAnchor = null; this._hoverOperation.cancel(); if (this._isVisible) { @@ -452,157 +490,79 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I return !!this._colorPicker; } + public setColorPicker(widget: ColorPickerWidget): void { + this._colorPicker = widget; + } + public onContentsChanged(): void { this._hover.onContentsChanged(); } - private _withResult(result: HoverPartInfo[], complete: boolean): void { + private _withResult(result: IHoverPart[], complete: boolean): void { this._messages = result; - if (this._lastRange && this._messages.length > 0) { - this._renderMessages(this._lastRange, this._messages); + if (this._lastAnchor && this._messages.length > 0) { + this._renderMessages(this._lastAnchor, this._messages); } else if (complete) { this.hide(); } } - private _renderMessages(renderRange: Range, messages: HoverPartInfo[]): void { + private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void { if (this._renderDisposable) { this._renderDisposable.dispose(); this._renderDisposable = null; } - this._colorPicker = null; + this._colorPicker = null as ColorPickerWidget | null; // TODO: TypeScript thinks this is always null // update column from which to show let renderColumn = Constants.MAX_SAFE_SMALL_INTEGER; - let highlightRange: Range | null = messages[0].data.range ? Range.lift(messages[0].data.range) : null; + let highlightRange: Range = messages[0].range; + let forceShowAtRange: Range | null = null; let fragment = document.createDocumentFragment(); - let containColorPicker = false; const disposables = new DisposableStore(); - const markerMessages: MarkerHover[] = []; - const markdownParts: MarkdownHover[] = []; - messages.forEach((_msg) => { - const msg = _msg.data; - if (!msg.range) { - return; - } - + const hoverParts = new Map(); + for (const msg of messages) { renderColumn = Math.min(renderColumn, msg.range.startColumn); - highlightRange = highlightRange ? Range.plusRange(highlightRange, msg.range) : Range.lift(msg.range); + highlightRange = Range.plusRange(highlightRange, msg.range); - if (msg instanceof ColorHover) { - containColorPicker = true; - - const { red, green, blue, alpha } = msg.color; - const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha); - const color = new Color(rgba); - - if (!this._editor.hasModel()) { - return; - } - - const editorModel = this._editor.getModel(); - let range = new Range(msg.range.startLineNumber, msg.range.startColumn, msg.range.endLineNumber, msg.range.endColumn); - let colorInfo = { range: msg.range, color: msg.color }; - - // create blank olor picker model and widget first to ensure it's positioned correctly. - const model = new ColorPickerModel(color, [], 0); - const widget = new ColorPickerWidget(fragment, model, this._editor.getOption(EditorOption.pixelRatio), this._themeService); + if (msg.forceShowAtRange) { + forceShowAtRange = msg.range; + } - getColorPresentations(editorModel, colorInfo, msg.provider, CancellationToken.None).then(colorPresentations => { - model.colorPresentations = colorPresentations || []; - if (!this._editor.hasModel()) { - // gone... - return; - } - const originalText = this._editor.getModel().getValueInRange(msg.range); - model.guessColorPresentation(color, originalText); - - const updateEditorModel = () => { - let textEdits: IIdentifiedSingleEditOperation[]; - let newRange: Range; - if (model.presentation.textEdit) { - textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation]; - newRange = new Range( - model.presentation.textEdit.range.startLineNumber, - model.presentation.textEdit.range.startColumn, - model.presentation.textEdit.range.endLineNumber, - model.presentation.textEdit.range.endColumn - ); - const trackedRange = this._editor.getModel()!._setTrackedRange(null, newRange, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); - this._editor.pushUndoStop(); - this._editor.executeEdits('colorpicker', textEdits); - newRange = this._editor.getModel()!._getTrackedRange(trackedRange) || newRange; - } else { - textEdits = [{ identifier: null, range, text: model.presentation.label, forceMoveMarkers: false }]; - newRange = range.setEndPosition(range.endLineNumber, range.startColumn + model.presentation.label.length); - this._editor.pushUndoStop(); - this._editor.executeEdits('colorpicker', textEdits); - } - - if (model.presentation.additionalTextEdits) { - textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]]; - this._editor.executeEdits('colorpicker', textEdits); - this.hide(); - } - this._editor.pushUndoStop(); - range = newRange; - }; - - const updateColorPresentations = (color: Color) => { - return getColorPresentations(editorModel, { - range: range, - color: { - red: color.rgba.r / 255, - green: color.rgba.g / 255, - blue: color.rgba.b / 255, - alpha: color.rgba.a - } - }, msg.provider, CancellationToken.None).then((colorPresentations) => { - model.colorPresentations = colorPresentations || []; - }); - }; - - const colorListener = model.onColorFlushed((color: Color) => { - updateColorPresentations(color).then(updateEditorModel); - }); - const colorChangeListener = model.onDidChangeColor(updateColorPresentations); - - this._colorPicker = widget; - this.showAt(range.getStartPosition(), range, this._shouldFocus); - this._updateContents(fragment); - this._colorPicker.layout(); - - this._renderDisposable = combinedDisposable(colorListener, colorChangeListener, widget, disposables); - }); - } else { - if (msg instanceof MarkerHover) { - markerMessages.push(msg); - } else { - if (msg instanceof MarkdownHover) { - markdownParts.push(msg); - } - } + if (!hoverParts.has(msg.owner)) { + hoverParts.set(msg.owner, []); } - }); + const dest = hoverParts.get(msg.owner)!; + dest.push(msg); + } + + const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService)); - if (markdownParts.length > 0) { - disposables.add(this._markdownHoverParticipant.renderHoverParts(markdownParts, fragment)); + for (const [participant, participantHoverParts] of hoverParts) { + disposables.add(participant.renderHoverParts(participantHoverParts, fragment, statusBar)); } - if (markerMessages.length) { - disposables.add(this._markerHoverParticipant.renderHoverParts(markerMessages, fragment)); + if (statusBar.hasContent) { + fragment.appendChild(statusBar.hoverElement); } this._renderDisposable = disposables; // show - if (!containColorPicker && fragment.hasChildNodes()) { - this.showAt(new Position(renderRange.startLineNumber, renderColumn), highlightRange, this._shouldFocus); + if (fragment.hasChildNodes()) { + if (forceShowAtRange) { + this._showAt(forceShowAtRange.getStartPosition(), forceShowAtRange, this._shouldFocus); + } else { + this._showAt(new Position(anchor.range.startLineNumber, renderColumn), highlightRange, this._shouldFocus); + } this._updateContents(fragment); } + if (this._colorPicker) { + this._colorPicker.layout(); + } this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, highlightRange ? [{ @@ -613,22 +573,11 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I } private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'content-hover-highlight', className: 'hoverHighlight' }); } -function hoverContentsEquals(first: HoverPartInfo[], second: HoverPartInfo[]): boolean { - if (first.length !== second.length) { - return false; - } - for (let i = 0; i < first.length; i++) { - if (!first[i].data.equals(second[i].data)) { - return false; - } - } - return true; -} - registerThemingParticipant((theme, collector) => { const linkFg = theme.getColor(textLinkForeground); if (linkFg) { diff --git a/lib/vscode/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts b/lib/vscode/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts index 70f9fbf73ccc..ab6f08971214 100644 --- a/lib/vscode/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts +++ b/lib/vscode/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts @@ -31,6 +31,7 @@ class InPlaceReplaceController implements IEditorContribution { } private static readonly DECORATION = ModelDecorationOptions.register({ + description: 'in-place-replace', className: 'valueSetReplacement' }); diff --git a/lib/vscode/src/vs/editor/contrib/inlineHints/inlineHintsController.ts b/lib/vscode/src/vs/editor/contrib/inlayHints/inlayHintsController.ts similarity index 68% rename from lib/vscode/src/vs/editor/contrib/inlineHints/inlineHintsController.ts rename to lib/vscode/src/vs/editor/contrib/inlayHints/inlayHintsController.ts index db9477d28b50..f1e4f4b09553 100644 --- a/lib/vscode/src/vs/editor/contrib/inlineHints/inlineHintsController.ts +++ b/lib/vscode/src/vs/editor/contrib/inlayHints/inlayHintsController.ts @@ -12,32 +12,32 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IContentDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; -import { InlineHintsProvider, InlineHintsProviderRegistry, InlineHint } from 'vs/editor/common/modes'; +import { InlayHintsProvider, InlayHintsProviderRegistry, InlayHint } from 'vs/editor/common/modes'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { flatten } from 'vs/base/common/arrays'; -import { editorInlineHintForeground, editorInlineHintBackground } from 'vs/platform/theme/common/colorRegistry'; +import { editorInlayHintForeground, editorInlayHintBackground } from 'vs/platform/theme/common/colorRegistry'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Range } from 'vs/editor/common/core/range'; import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/base/common/range'; import { assertType } from 'vs/base/common/types'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { Position } from 'vs/editor/common/core/position'; const MAX_DECORATORS = 500; -export interface InlineHintsData { - list: InlineHint[]; - provider: InlineHintsProvider; +export interface InlayHintsData { + list: InlayHint[]; + provider: InlayHintsProvider; } -export async function getInlineHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { - const datas: InlineHintsData[] = []; - const providers = InlineHintsProviderRegistry.ordered(model).reverse(); - const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlineHints(model, range, token)).then(result => { +export async function getInlayHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { + const datas: InlayHintsData[] = []; + const providers = InlayHintsProviderRegistry.ordered(model).reverse(); + const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlayHints(model, range, token)).then(result => { if (result) { datas.push({ list: result, provider }); } @@ -50,17 +50,13 @@ export async function getInlineHints(model: ITextModel, ranges: Range[], token: return datas; } -export class InlineHintsController implements IEditorContribution { +export class InlayHintsController implements IEditorContribution { - static readonly ID: string = 'editor.contrib.InlineHints'; - - // static get(editor: ICodeEditor): InlineHintsController { - // return editor.getContribution(this.ID); - // } + static readonly ID: string = 'editor.contrib.InlayHints'; private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private readonly _getInlineHintsDelays = new LanguageFeatureRequestDelays(InlineHintsProviderRegistry, 250, 2500); + private readonly _getInlayHintsDelays = new LanguageFeatureRequestDelays(InlayHintsProviderRegistry, 250, 2500); private _decorationsTypeIds: string[] = []; private _decorationIds: string[] = []; @@ -70,12 +66,12 @@ export class InlineHintsController implements IEditorContribution { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IThemeService private readonly _themeService: IThemeService, ) { - this._disposables.add(InlineHintsProviderRegistry.onDidChange(() => this._update())); + this._disposables.add(InlayHintsProviderRegistry.onDidChange(() => this._update())); this._disposables.add(_themeService.onDidColorThemeChange(() => this._update())); this._disposables.add(_editor.onDidChangeModel(() => this._update())); this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); this._disposables.add(_editor.onDidChangeConfiguration(e => { - if (e.hasChanged(EditorOption.inlineHints)) { + if (e.hasChanged(EditorOption.inlayHints)) { this._update(); } })); @@ -92,13 +88,13 @@ export class InlineHintsController implements IEditorContribution { private _update(): void { this._sessionDisposables.clear(); - if (!this._editor.getOption(EditorOption.inlineHints).enabled) { + if (!this._editor.getOption(EditorOption.inlayHints).enabled) { this._removeAllDecorations(); return; } const model = this._editor.getModel(); - if (!model || !InlineHintsProviderRegistry.has(model)) { + if (!model || !InlayHintsProviderRegistry.has(model)) { this._removeAllDecorations(); return; } @@ -110,16 +106,16 @@ export class InlineHintsController implements IEditorContribution { this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); - const result = await getInlineHints(model, visibleRanges, cts.token); + const result = await getInlayHints(model, visibleRanges, cts.token); // update moving average - const newDelay = this._getInlineHintsDelays.update(model, Date.now() - t1); + const newDelay = this._getInlayHintsDelays.update(model, Date.now() - t1); scheduler.delay = newDelay; // render hints this._updateHintsDecorators(result); - }, this._getInlineHintsDelays.get(model)); + }, this._getInlayHintsDelays.get(model)); this._sessionDisposables.add(scheduler); @@ -131,28 +127,28 @@ export class InlineHintsController implements IEditorContribution { // update inline hints when any any provider fires an event const providerListener = new DisposableStore(); this._sessionDisposables.add(providerListener); - for (const provider of InlineHintsProviderRegistry.all(model)) { - if (typeof provider.onDidChangeInlineHints === 'function') { - providerListener.add(provider.onDidChangeInlineHints(() => scheduler.schedule())); + for (const provider of InlayHintsProviderRegistry.all(model)) { + if (typeof provider.onDidChangeInlayHints === 'function') { + providerListener.add(provider.onDidChangeInlayHints(() => scheduler.schedule())); } } } - private _updateHintsDecorators(hintsData: InlineHintsData[]): void { + private _updateHintsDecorators(hintsData: InlayHintsData[]): void { const { fontSize, fontFamily } = this._getLayoutInfo(); - const backgroundColor = this._themeService.getColorTheme().getColor(editorInlineHintBackground); - const fontColor = this._themeService.getColorTheme().getColor(editorInlineHintForeground); + const backgroundColor = this._themeService.getColorTheme().getColor(editorInlayHintBackground); + const fontColor = this._themeService.getColorTheme().getColor(editorInlayHintForeground); const newDecorationsTypeIds: string[] = []; const newDecorationsData: IModelDeltaDecoration[] = []; - const fontFamilyVar = '--inlineHintsFontFamily'; + const fontFamilyVar = '--inlayHintsFontFamily'; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily); for (const { list: hints } of hintsData) { for (let j = 0; j < hints.length && newDecorationsData.length < MAX_DECORATORS; j++) { - const { text, range, description: hoverMessage, whitespaceBefore, whitespaceAfter } = hints[j]; + const { text, position, whitespaceBefore, whitespaceAfter } = hints[j]; const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; @@ -166,22 +162,16 @@ export class InlineHintsController implements IEditorContribution { padding: `0px ${(fontSize / 4) | 0}px`, borderRadius: `${(fontSize / 4) | 0}px`, }; - const key = 'inlineHints-' + hash(before).toString(16); - this._codeEditorService.registerDecorationType(key, { before }, undefined, this._editor); + const key = 'inlayHints-' + hash(before).toString(16); + this._codeEditorService.registerDecorationType('inlay-hints-controller', key, { before }, undefined, this._editor); // decoration types are ref-counted which means we only need to // call register und remove equally often newDecorationsTypeIds.push(key); const options = this._codeEditorService.resolveDecorationOptions(key, true); - if (typeof hoverMessage === 'string') { - options.hoverMessage = new MarkdownString().appendText(hoverMessage); - } else if (hoverMessage) { - options.hoverMessage = hoverMessage; - } - newDecorationsData.push({ - range, + range: Range.fromPositions(position), options }); } @@ -194,7 +184,7 @@ export class InlineHintsController implements IEditorContribution { } private _getLayoutInfo() { - const options = this._editor.getOption(EditorOption.inlineHints); + const options = this._editor.getOption(EditorOption.inlayHints); const editorFontSize = this._editor.getOption(EditorOption.fontSize); let fontSize = options.fontSize; if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { @@ -211,9 +201,9 @@ export class InlineHintsController implements IEditorContribution { } } -registerEditorContribution(InlineHintsController.ID, InlineHintsController); +registerEditorContribution(InlayHintsController.ID, InlayHintsController); -CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { +CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { const [uri, range] = args; assertType(URI.isUri(uri)); @@ -221,8 +211,8 @@ CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, const ref = await accessor.get(ITextModelService).createModelReference(uri); try { - const data = await getInlineHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); - return flatten(data.map(item => item.list)).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + const data = await getInlayHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); + return flatten(data.map(item => item.list)).sort((a, b) => Position.compare(a.position, b.position)); } finally { ref.dispose(); diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostText.css b/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostText.css new file mode 100644 index 000000000000..009f27cef75d --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostText.css @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .suggest-preview-additional-widget { + white-space: nowrap; +} + +.monaco-editor .suggest-preview-additional-widget .content-spacer { + color: transparent; + white-space: pre; +} + +.monaco-editor .suggest-preview-additional-widget .button { + display: inline-block; + cursor: pointer; + text-decoration: underline; + text-underline-position: under; +} diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts b/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts new file mode 100644 index 000000000000..e226aca73b82 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts @@ -0,0 +1,346 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Range } from 'vs/editor/common/core/range'; +import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; +import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class GhostTextController extends Disposable { + public static readonly inlineSuggestionVisible = new RawContextKey('inlineSuggestionVisible', false, nls.localize('inlineSuggestionVisible', "Whether an inline suggestion is visible")); + public static readonly inlineSuggestionHasIndentation = new RawContextKey('inlineSuggestionHasIndentation', false, nls.localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace")); + + static ID = 'editor.contrib.ghostTextController'; + + public static get(editor: ICodeEditor): GhostTextController { + return editor.getContribution(GhostTextController.ID); + } + + private readonly widget: GhostTextWidget; + private readonly activeController = this._register(new MutableDisposable()); + private readonly contextKeys: GhostTextContextKeys; + private triggeredExplicitly = false; + + constructor( + public readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(); + this.contextKeys = new GhostTextContextKeys(contextKeyService); + + this.widget = this._register(instantiationService.createInstance(GhostTextWidget, this.editor)); + + this._register(this.editor.onDidChangeModel(() => { + this.updateModelController(); + })); + this._register(this.editor.onDidChangeConfiguration((e) => { + if (e.hasChanged(EditorOption.suggest)) { + this.updateModelController(); + } + if (e.hasChanged(EditorOption.inlineSuggest)) { + this.updateModelController(); + } + })); + this.updateModelController(); + } + + // Don't call this method when not neccessary. It will recreate the activeController. + private updateModelController(): void { + const suggestOptions = this.editor.getOption(EditorOption.suggest); + const inlineSuggestOptions = this.editor.getOption(EditorOption.inlineSuggest); + + this.activeController.value = undefined; + // ActiveGhostTextController is only created if one of those settings is set or if the inline completions are triggered explicitly. + this.activeController.value = + this.editor.hasModel() && (suggestOptions.preview || inlineSuggestOptions.enabled || this.triggeredExplicitly) + ? this.instantiationService.createInstance( + ActiveGhostTextController, + this.editor, + this.widget, + this.contextKeys + ) + : undefined; + } + + public shouldShowHoverAt(hoverRange: Range): boolean { + return this.activeController.value?.shouldShowHoverAt(hoverRange) || false; + } + + public shouldShowHoverAtViewZone(viewZoneId: string): boolean { + return this.widget.shouldShowHoverAtViewZone(viewZoneId); + } + + public trigger(): void { + this.triggeredExplicitly = true; + if (!this.activeController.value) { + this.updateModelController(); + } + this.activeController.value?.triggerInlineCompletion(); + } + + public commit(): void { + this.activeController.value?.commitInlineCompletion(); + } + + public hide(): void { + this.activeController.value?.hideInlineCompletion(); + } + + public showNextInlineCompletion(): void { + this.activeController.value?.showNextInlineCompletion(); + } + + public showPreviousInlineCompletion(): void { + this.activeController.value?.showPreviousInlineCompletion(); + } +} + +// TODO: This should be local state to the editor. +// The global state should depend on the local state of the currently focused editor. +// Currently the global state is updated directly, which may lead to conflicts if multiple ghost texts are active. +class GhostTextContextKeys { + private lastInlineCompletionVisibleValue = false; + private readonly inlineCompletionVisible = GhostTextController.inlineSuggestionVisible.bindTo(this.contextKeyService); + + private lastInlineCompletionSuggestsIndentationValue = false; + private readonly inlineCompletionSuggestsIndentation = GhostTextController.inlineSuggestionHasIndentation.bindTo(this.contextKeyService); + + constructor(private readonly contextKeyService: IContextKeyService) { + } + + public setInlineCompletionVisible(value: boolean): void { + // Only modify the context key if we actually changed it. + // Thus, we don't overwrite values set by someone else. + if (value !== this.lastInlineCompletionVisibleValue) { + this.inlineCompletionVisible.set(value); + this.lastInlineCompletionVisibleValue = value; + } + } + + public setInlineCompletionSuggestsIndentation(value: boolean): void { + if (value !== this.lastInlineCompletionSuggestsIndentationValue) { + this.inlineCompletionSuggestsIndentation.set(value); + this.lastInlineCompletionSuggestsIndentationValue = value; + } + } +} + +/** + * The controller for a text editor with an initialized text model. +*/ +export class ActiveGhostTextController extends Disposable { + private readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetAdapterModel(this.editor)); + private readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.commandService)); + + private get activeInlineCompletionsModel(): InlineCompletionsModel | undefined { + if (this.widget.model === this.inlineCompletionsModel) { + return this.inlineCompletionsModel; + } + return undefined; + } + + constructor( + private readonly editor: IActiveCodeEditor, + private readonly widget: GhostTextWidget, + private readonly contextKeys: GhostTextContextKeys, + @ICommandService private readonly commandService: ICommandService, + ) { + super(); + + this._register(this.suggestWidgetAdapterModel.onDidChange(() => { + this.updateModel(); + // When the suggest widget becomes inactive and an inline completion + // becomes visible, we need to update the context keys. + this.updateContextKeys(); + })); + this.updateModel(); + + this._register(toDisposable(() => { + if (widget.model === this.suggestWidgetAdapterModel || widget.model === this.inlineCompletionsModel) { + widget.setModel(undefined); + } + this.contextKeys.setInlineCompletionVisible(false); + this.contextKeys.setInlineCompletionSuggestsIndentation(false); + })); + + if (this.inlineCompletionsModel) { + this._register(this.inlineCompletionsModel.onDidChange(() => { + this.updateContextKeys(); + })); + } + } + + private updateContextKeys(): void { + this.contextKeys.setInlineCompletionVisible( + this.activeInlineCompletionsModel?.ghostText !== undefined + ); + + if (this.inlineCompletionsModel?.ghostText) { + const firstLine = this.inlineCompletionsModel.ghostText.lines[0] || ''; + const suggestionStartsWithWs = firstLine.startsWith(' ') || firstLine.startsWith('\t'); + const p = this.inlineCompletionsModel.ghostText.position; + const indentationEndColumn = this.editor.getModel().getLineIndentColumn(p.lineNumber); + const inIndentation = p.column <= indentationEndColumn; + + this.contextKeys.setInlineCompletionSuggestsIndentation( + this.widget.model === this.inlineCompletionsModel + && suggestionStartsWithWs && inIndentation + ); + } else { + this.contextKeys.setInlineCompletionSuggestsIndentation(false); + } + } + + public shouldShowHoverAt(hoverRange: Range): boolean { + const ghostText = this.activeInlineCompletionsModel?.ghostText; + if (ghostText) { + return hoverRange.containsPosition(ghostText.position); + } + return false; + } + + public triggerInlineCompletion(): void { + this.activeInlineCompletionsModel?.startSession(); + } + + public commitInlineCompletion(): void { + this.activeInlineCompletionsModel?.commitCurrentSuggestion(); + } + + public hideInlineCompletion(): void { + this.activeInlineCompletionsModel?.hide(); + } + + public showNextInlineCompletion(): void { + this.activeInlineCompletionsModel?.showNext(); + } + + public showPreviousInlineCompletion(): void { + this.activeInlineCompletionsModel?.showPrevious(); + } + + private updateModel() { + this.widget.setModel( + this.suggestWidgetAdapterModel.isActive + ? this.suggestWidgetAdapterModel + : this.inlineCompletionsModel + ); + this.inlineCompletionsModel?.setActive(this.widget.model === this.inlineCompletionsModel); + } +} + +const GhostTextCommand = EditorCommand.bindToContribution(GhostTextController.get); + +export const commitInlineSuggestionAction = new GhostTextCommand({ + id: 'editor.action.inlineSuggest.commit', + precondition: ContextKeyExpr.and( + GhostTextController.inlineSuggestionVisible, + GhostTextController.inlineSuggestionHasIndentation.toNegated(), + EditorContextKeys.tabMovesFocus.toNegated() + ), + kbOpts: { + weight: 100, + primary: KeyCode.Tab, + }, + handler(x) { + x.commit(); + x.editor.focus(); + } +}); +registerEditorCommand(commitInlineSuggestionAction); + +registerEditorCommand(new GhostTextCommand({ + id: 'editor.action.inlineSuggest.hide', + precondition: GhostTextController.inlineSuggestionVisible, + kbOpts: { + weight: 100, + primary: KeyCode.Escape, + }, + handler(x) { + x.hide(); + } +})); + +export class ShowNextInlineSuggestionAction extends EditorAction { + public static ID = 'editor.action.inlineSuggest.showNext'; + constructor() { + super({ + id: ShowNextInlineSuggestionAction.ID, + label: nls.localize('action.inlineSuggest.showNext', "Show Next Inline Suggestion"), + alias: 'Show Next Inline Suggestion', + precondition: EditorContextKeys.writable, + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.showNextInlineCompletion(); + editor.focus(); + } + } +} + +export class ShowPreviousInlineSuggestionAction extends EditorAction { + public static ID = 'editor.action.inlineSuggest.showPrevious'; + constructor() { + super({ + id: ShowPreviousInlineSuggestionAction.ID, + label: nls.localize('action.inlineSuggest.showPrevious', "Show Previous Inline Suggestion"), + alias: 'Show Previous Inline Suggestion', + precondition: EditorContextKeys.writable, + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.showPreviousInlineCompletion(); + editor.focus(); + } + } +} + +export class TriggerInlineSuggestionAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineSuggest.trigger', + label: nls.localize('action.inlineSuggest.trigger', "Trigger Inline Suggestion"), + alias: 'Trigger Inline Suggestion', + precondition: EditorContextKeys.writable + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.trigger(); + } + } +} + +registerEditorContribution(GhostTextController.ID, GhostTextController); +registerEditorAction(TriggerInlineSuggestionAction); +registerEditorAction(ShowNextInlineSuggestionAction); +registerEditorAction(ShowPreviousInlineSuggestionAction); diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts b/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts new file mode 100644 index 000000000000..c2e22b180086 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts @@ -0,0 +1,427 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./ghostText'; +import * as dom from 'vs/base/browser/dom'; +import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import * as strings from 'vs/base/common/strings'; +import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { EditorFontLigatures, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { Configuration } from 'vs/editor/browser/config/configuration'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; +import { Position } from 'vs/editor/common/core/position'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry'; +import { RGBA, Color } from 'vs/base/common/color'; +import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; + +const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value }); + +export interface GhostTextWidgetModel { + readonly onDidChange: Event; + readonly ghostText: GhostText | undefined; + + setExpanded(expanded: boolean): void; + readonly expanded: boolean; + + readonly minReservedLineCount: number; +} + +export interface GhostText { + readonly lines: string[]; + readonly position: Position; +} + +export abstract class BaseGhostTextWidgetModel extends Disposable implements GhostTextWidgetModel { + public abstract readonly ghostText: GhostText | undefined; + + private _expanded: boolean | undefined = undefined; + + protected readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + public abstract readonly minReservedLineCount: number; + + public get expanded() { + if (this._expanded === undefined) { + // TODO this should use a global hidden setting. + // See https://github.com/microsoft/vscode/issues/125037. + return true; + } + return this._expanded; + } + + constructor(protected readonly editor: IActiveCodeEditor) { + super(); + + this._register(editor.onDidChangeConfiguration((e) => { + if (e.hasChanged(EditorOption.suggest) && this._expanded === undefined) { + this.onDidChangeEmitter.fire(); + } + })); + } + + public setExpanded(expanded: boolean): void { + this._expanded = true; + this.onDidChangeEmitter.fire(); + } +} + +export class GhostTextWidget extends Disposable { + private static decorationTypeCount = 0; + + private codeEditorDecorationTypeKey: string | null = null; + private readonly modelRef = this._register(new MutableDisposable>()); + private decorationIds: string[] = []; + private viewZoneId: string | null = null; + private viewMoreContentWidget: ViewMoreLinesContentWidget | null = null; + + constructor( + private readonly editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IThemeService private readonly _themeService: IThemeService, + ) { + super(); + + this._register(this.editor.onDidChangeConfiguration((e) => { + if ( + e.hasChanged(EditorOption.disableMonospaceOptimizations) + || e.hasChanged(EditorOption.stopRenderingLineAfter) + || e.hasChanged(EditorOption.renderWhitespace) + || e.hasChanged(EditorOption.renderControlCharacters) + || e.hasChanged(EditorOption.fontLigatures) + || e.hasChanged(EditorOption.fontInfo) + || e.hasChanged(EditorOption.lineHeight) + ) { + this.render(); + } + })); + this._register(toDisposable(() => { + this.setModel(undefined); + })); + } + + public get model(): GhostTextWidgetModel | undefined { + return this.modelRef.value?.object; + } + + public shouldShowHoverAtViewZone(viewZoneId: string): boolean { + return (this.viewZoneId === viewZoneId); + } + + public setModel(model: GhostTextWidgetModel | undefined): void { + if (model === this.model) { return; } + this.modelRef.value = model + ? createDisposableRef(model, model.onDidChange(() => this.render())) + : undefined; + this.render(); + } + + private getRenderData() { + if (!this.editor.hasModel() || !this.model?.ghostText) { + return undefined; + } + + const { minReservedLineCount, expanded } = this.model; + let { position, lines } = this.model.ghostText; + + const textModel = this.editor.getModel(); + const maxColumn = textModel.getLineMaxColumn(position.lineNumber); + const { tabSize } = textModel.getOptions(); + + if (lines.length > 1 && position.column !== maxColumn) { + console.warn('Can only show multiline ghost text at the end of a line'); + lines = []; + position = new Position(position.lineNumber, maxColumn); + } + + return { tabSize, position, lines, minReservedLineCount, expanded }; + } + + private render(): void { + const renderData = this.getRenderData(); + + if (this.codeEditorDecorationTypeKey) { + this._codeEditorService.removeDecorationType(this.codeEditorDecorationTypeKey); + this.codeEditorDecorationTypeKey = null; + } + + if (renderData && renderData.lines.length > 0) { + const foreground = this._themeService.getColorTheme().getColor(ghostTextForeground); + let opacity: string | undefined = undefined; + let color: string | undefined = undefined; + if (foreground) { + function opaque(color: Color): Color { + const { r, b, g } = color.rgba; + return new Color(new RGBA(r, g, b, 255)); + } + + opacity = String(foreground.rgba.a); + color = Color.Format.CSS.format(opaque(foreground))!; + } + + const borderColor = this._themeService.getColorTheme().getColor(ghostTextBorder); + let border: string | undefined = undefined; + if (borderColor) { + border = `2px dashed ${borderColor}`; + } + + // We add 0 to bring it before any other decoration. + this.codeEditorDecorationTypeKey = `0-ghost-text-${++GhostTextWidget.decorationTypeCount}`; + + const line = this.editor.getModel()?.getLineContent(renderData.position.lineNumber) || ''; + const linePrefix = line.substr(0, renderData.position.column - 1); + + // To avoid visual confusion, we don't want to render visible whitespace + const contentText = renderSingleLineText(renderData.lines[0] || '', linePrefix, renderData.tabSize, false); + + this._codeEditorService.registerDecorationType('ghost-text', this.codeEditorDecorationTypeKey, { + after: { + // TODO: escape? + contentText, + opacity, + color, + border, + }, + }); + } + + const newDecorations = new Array(); + if (renderData && this.codeEditorDecorationTypeKey) { + newDecorations.push({ + range: Range.fromPositions(renderData.position, renderData.position), + options: { + ...this._codeEditorService.resolveDecorationOptions(this.codeEditorDecorationTypeKey, true), + } + }); + } + this.decorationIds = this.editor.deltaDecorations(this.decorationIds, newDecorations); + + if (this.viewMoreContentWidget) { + this.viewMoreContentWidget.dispose(); + this.viewMoreContentWidget = null; + } + + this.editor.changeViewZones((changeAccessor) => { + if (this.viewZoneId) { + changeAccessor.removeZone(this.viewZoneId); + this.viewZoneId = null; + } + + if (renderData) { + const remainingLines = renderData.lines.slice(1); + const heightInLines = Math.max(remainingLines.length, renderData.minReservedLineCount); + if (heightInLines > 0) { + if (renderData.expanded) { + const domNode = document.createElement('div'); + this.renderLines(domNode, renderData.tabSize, remainingLines); + + this.viewZoneId = changeAccessor.addZone({ + afterLineNumber: renderData.position.lineNumber, + afterColumn: renderData.position.column, + heightInLines: heightInLines, + domNode, + }); + } else if (remainingLines.length > 0) { + this.viewMoreContentWidget = this.renderViewMoreLines(renderData.position, renderData.lines[0], remainingLines.length); + } + } + } + }); + } + + private renderViewMoreLines(position: Position, firstLineText: string, remainingLinesLength: number): ViewMoreLinesContentWidget { + const fontInfo = this.editor.getOption(EditorOption.fontInfo); + const domNode = document.createElement('div'); + domNode.className = 'suggest-preview-additional-widget'; + Configuration.applyFontInfoSlow(domNode, fontInfo); + + const spacer = document.createElement('span'); + spacer.className = 'content-spacer'; + spacer.append(firstLineText); + domNode.append(spacer); + + const newline = document.createElement('span'); + newline.className = 'content-newline suggest-preview-text'; + newline.append('โŽ '); + domNode.append(newline); + + const disposableStore = new DisposableStore(); + + const button = document.createElement('div'); + button.className = 'button suggest-preview-text'; + button.append(`+${remainingLinesLength} linesโ€ฆ`); + + disposableStore.add(dom.addStandardDisposableListener(button, 'mousedown', (e) => { + this.model?.setExpanded(true); + e.preventDefault(); + this.editor.focus(); + })); + + domNode.append(button); + return new ViewMoreLinesContentWidget(this.editor, position, domNode, disposableStore); + } + + private renderLines(domNode: HTMLElement, tabSize: number, lines: string[]): void { + const opts = this.editor.getOptions(); + const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations); + const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter); + // To avoid visual confusion, we don't want to render visible whitespace + const renderWhitespace = 'none'; + const renderControlCharacters = opts.get(EditorOption.renderControlCharacters); + const fontLigatures = opts.get(EditorOption.fontLigatures); + const fontInfo = opts.get(EditorOption.fontInfo); + const lineHeight = opts.get(EditorOption.lineHeight); + + const sb = createStringBuilder(10000); + sb.appendASCIIString('
    '); + + for (let i = 0, len = lines.length; i < len; i++) { + const line = lines[i]; + sb.appendASCIIString('
    '); + + const isBasicASCII = strings.isBasicASCII(line); + const containsRTL = strings.containsRTL(line); + const lineTokens = LineTokens.createEmpty(line); + + renderViewLine(new RenderLineInput( + (fontInfo.isMonospace && !disableMonospaceOptimizations), + fontInfo.canUseHalfwidthRightwardsArrow, + line, + false, + isBasicASCII, + containsRTL, + 0, + lineTokens, + [], + tabSize, + 0, + fontInfo.spaceWidth, + fontInfo.middotWidth, + fontInfo.wsmiddotWidth, + stopRenderingLineAfter, + renderWhitespace, + renderControlCharacters, + fontLigatures !== EditorFontLigatures.OFF, + null + ), sb); + + sb.appendASCIIString('
    '); + } + sb.appendASCIIString('
    '); + + Configuration.applyFontInfoSlow(domNode, fontInfo); + const html = sb.build(); + const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; + domNode.innerHTML = trustedhtml as string; + } +} + +function renderSingleLineText(text: string, lineStart: string, tabSize: number, renderWhitespace: boolean): string { + const newLine = lineStart + text; + const visibleColumnsByColumns = CursorColumns.visibleColumnsByColumns(newLine, tabSize); + + + let contentText = ''; + let curCol = lineStart.length + 1; + for (const c of text) { + if (c === '\t') { + const width = visibleColumnsByColumns[curCol + 1] - visibleColumnsByColumns[curCol]; + if (renderWhitespace) { + contentText += 'โ†’'; + for (let i = 1; i < width; i++) { + contentText += '\xa0'; + } + } else { + for (let i = 0; i < width; i++) { + contentText += '\xa0'; + } + } + } else if (c === ' ') { + if (renderWhitespace) { + contentText += 'ยท'; + } else { + contentText += '\xa0'; + } + } else { + contentText += c; + } + curCol += 1; + } + + return contentText; +} + +class ViewMoreLinesContentWidget extends Disposable implements IContentWidget { + readonly allowEditorOverflow = false; + readonly suppressMouseDown = false; + + constructor( + private editor: ICodeEditor, + private position: Position, + private domNode: HTMLElement, + disposableStore: DisposableStore + ) { + super(); + this._register(disposableStore); + this._register(toDisposable(() => { + this.editor.removeContentWidget(this); + })); + this.editor.addContentWidget(this); + } + + getId(): string { + return 'editor.widget.viewMoreLinesWidget'; + } + + getDomNode(): HTMLElement { + return this.domNode; + } + + getPosition(): IContentWidgetPosition | null { + return { + position: this.position, + preference: [ContentWidgetPositionPreference.EXACT] + }; + } +} + +registerThemingParticipant((theme, collector) => { + const foreground = theme.getColor(ghostTextForeground); + + if (foreground) { + function opaque(color: Color): Color { + const { r, b, g } = color.rgba; + return new Color(new RGBA(r, g, b, 255)); + } + + const opacity = String(foreground.rgba.a); + const color = Color.Format.CSS.format(opaque(foreground))!; + + // We need to override the only used token type .mtk1 + collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { opacity: ${opacity}; color: ${color}; }`); + } + + const border = theme.getColor(ghostTextBorder); + if (border) { + collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { border: 2px dashed ${border}; }`); + } +}); + +function createDisposableRef(object: T, disposable: IDisposable): IReference { + return { + object, + dispose: () => disposable.dispose(), + }; +} diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts b/lib/vscode/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts new file mode 100644 index 000000000000..a7ec7b3aa39b --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { commitInlineSuggestionAction, GhostTextController, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction } from 'vs/editor/contrib/inlineCompletions/ghostTextController'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITextContentData, IViewZoneData } from 'vs/editor/browser/controller/mouseTarget'; + +export class InlineCompletionsHover implements IHoverPart { + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant { + constructor( + private readonly _editor: ICodeEditor, + hover: IEditorHover, + @ICommandService private readonly _commandService: ICommandService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + ) { } + + suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null { + const controller = GhostTextController.get(this._editor); + if (!controller) { + return null; + } + if (mouseEvent.target.type === MouseTargetType.CONTENT_VIEW_ZONE) { + // handle the case where the mouse is over the view zone + const viewZoneData = mouseEvent.target.detail; + if (controller.shouldShowHoverAtViewZone(viewZoneData.viewZoneId)) { + return new HoverForeignElementAnchor(1000, this, Range.fromPositions(viewZoneData.positionBefore || viewZoneData.position, viewZoneData.positionBefore || viewZoneData.position)); + } + } + if (mouseEvent.target.type === MouseTargetType.CONTENT_EMPTY && mouseEvent.target.range) { + // handle the case where the mouse is over the empty portion of a line following ghost text + if (controller.shouldShowHoverAt(mouseEvent.target.range)) { + return new HoverForeignElementAnchor(1000, this, mouseEvent.target.range); + } + } + if (mouseEvent.target.type === MouseTargetType.CONTENT_TEXT && mouseEvent.target.range && mouseEvent.target.detail) { + // handle the case where the mouse is directly over ghost text + const mightBeForeignElement = (mouseEvent.target.detail).mightBeForeignElement; + if (mightBeForeignElement && controller.shouldShowHoverAt(mouseEvent.target.range)) { + return new HoverForeignElementAnchor(1000, this, mouseEvent.target.range); + } + } + return null; + } + + computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): InlineCompletionsHover[] { + const controller = GhostTextController.get(this._editor); + if (controller && controller.shouldShowHoverAt(anchor.range)) { + return [new InlineCompletionsHover(this, anchor.range)]; + } + return []; + } + + renderHoverParts(hoverParts: InlineCompletionsHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable { + const menu = this._menuService.createMenu( + MenuId.InlineCompletionsActions, + this._contextKeyService + ); + + statusBar.addAction({ + label: nls.localize('showNextInlineSuggestion', "Next"), + commandId: ShowNextInlineSuggestionAction.ID, + run: () => this._commandService.executeCommand(ShowNextInlineSuggestionAction.ID) + }); + statusBar.addAction({ + label: nls.localize('showPreviousInlineSuggestion', "Previous"), + commandId: ShowPreviousInlineSuggestionAction.ID, + run: () => this._commandService.executeCommand(ShowPreviousInlineSuggestionAction.ID) + }); + statusBar.addAction({ + label: nls.localize('acceptInlineSuggestion', "Accept"), + commandId: commitInlineSuggestionAction.id, + run: () => this._commandService.executeCommand(commitInlineSuggestionAction.id) + }); + + for (const [_, group] of menu.getActions()) { + for (const action of group) { + if (action instanceof MenuItemAction) { + statusBar.addAction({ + label: action.label, + commandId: action.item.id, + run: () => this._commandService.executeCommand(action.item.id) + }); + } + } + } + + return Disposable.None; + } +} diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts b/lib/vscode/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts new file mode 100644 index 000000000000..612991e5b939 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts @@ -0,0 +1,593 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as strings from 'vs/base/common/strings'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/modes'; +import { BaseGhostTextWidgetModel, GhostText, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { MutableDisposable } from 'vs/editor/contrib/inlineCompletions/utils'; +import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; + +export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel { + protected readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + private readonly completionSession = this._register(new MutableDisposable()); + + private active: boolean = false; + + constructor( + private readonly editor: IActiveCodeEditor, + private readonly commandService: ICommandService + ) { + super(); + + this._register(commandService.onDidExecuteCommand(e => { + // These commands don't trigger onDidType. + const commands = new Set([ + UndoCommand.id, + RedoCommand.id, + CoreEditingCommands.Tab.id, + CoreEditingCommands.DeleteLeft.id, + CoreEditingCommands.DeleteRight.id + ]); + if (commands.has(e.commandId) && editor.hasTextFocus()) { + this.handleUserInput(); + } + })); + + this._register(this.editor.onDidType((e) => { + this.handleUserInput(); + })); + + this._register(this.editor.onDidChangeCursorPosition((e) => { + if (this.session && !this.session.isValid) { + this.hide(); + } + })); + } + + private handleUserInput() { + if (this.session && !this.session.isValid) { + this.hide(); + } + setTimeout(() => { + // Wait for the cursor update that happens in the same iteration loop iteration + this.startSessionIfTriggered(); + }, 0); + } + + private get session(): InlineCompletionsSession | undefined { + return this.completionSession.value; + } + + public get ghostText(): GhostText | undefined { + return this.session?.ghostText; + } + + public get minReservedLineCount(): number { + return this.session ? this.session.minReservedLineCount : 0; + } + + public get expanded(): boolean { + return this.session ? this.session.expanded : false; + } + + public setExpanded(expanded: boolean): void { + this.session?.setExpanded(expanded); + } + + public setActive(active: boolean) { + this.active = active; + if (active) { + this.session?.scheduleAutomaticUpdate(); + } + } + + private startSessionIfTriggered(): void { + const suggestOptions = this.editor.getOption(EditorOption.inlineSuggest); + if (!suggestOptions.enabled) { + return; + } + + if (this.session && this.session.isValid) { + return; + } + + this.startSession(); + } + + public startSession(): void { + if (this.completionSession.value) { + return; + } + this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService); + this.completionSession.value.takeOwnership( + this.completionSession.value.onDidChange(() => { + this.onDidChangeEmitter.fire(); + }) + ); + } + + public hide(): void { + this.completionSession.clear(); + this.onDidChangeEmitter.fire(); + } + + public commitCurrentSuggestion(): void { + // Don't dispose the session, so that after committing, more suggestions are shown. + this.session?.commitCurrentCompletion(); + } + + public showNext(): void { + this.session?.showNextInlineCompletion(); + } + + public showPrevious(): void { + this.session?.showPreviousInlineCompletion(); + } +} + +class InlineCompletionsSession extends BaseGhostTextWidgetModel { + public readonly minReservedLineCount = 0; + + private readonly updateOperation = this._register(new MutableDisposable()); + private readonly cache = this._register(new MutableDisposable()); + + private updateSoon = this._register(new RunOnceScheduler(() => this.update(InlineCompletionTriggerKind.Automatic), 50)); + private readonly textModel = this.editor.getModel(); + + constructor( + editor: IActiveCodeEditor, + private readonly triggerPosition: Position, + private readonly shouldUpdate: () => boolean, + private readonly commandService: ICommandService, + ) { + super(editor); + + let lastCompletionItem: InlineCompletion | undefined = undefined; + this._register(this.onDidChange(() => { + const currentCompletion = this.currentCompletion; + if (currentCompletion && currentCompletion.sourceInlineCompletion !== lastCompletionItem) { + lastCompletionItem = currentCompletion.sourceInlineCompletion; + + const provider = currentCompletion.sourceProvider; + if (provider.handleItemDidShow) { + provider.handleItemDidShow(currentCompletion.sourceInlineCompletions, lastCompletionItem); + } + } + })); + + this._register(this.editor.onDidChangeModelContent((e) => { + if (this.cache.value) { + let hasChanged = false; + for (const c of this.cache.value.completions) { + const newRange = this.textModel.getDecorationRange(c.decorationId); + if (!newRange) { + onUnexpectedError(new Error('Decoration has no range')); + continue; + } + if (!c.synchronizedRange.equalsRange(newRange)) { + hasChanged = true; + c.synchronizedRange = newRange; + } + } + if (hasChanged) { + this.onDidChangeEmitter.fire(); + } + } + + this.scheduleAutomaticUpdate(); + })); + + this.scheduleAutomaticUpdate(); + } + + //#region Selection + + // We use a semantic id to track the selection even if the cache changes. + private currentlySelectedCompletionId: string | undefined = undefined; + + private fixAndGetIndexOfCurrentSelection(): number { + if (!this.currentlySelectedCompletionId || !this.cache.value) { + return 0; + } + if (this.cache.value.completions.length === 0) { + // don't reset the selection in this case + return 0; + } + + const idx = this.cache.value.completions.findIndex(v => v.semanticId === this.currentlySelectedCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this.currentlySelectedCompletionId = undefined; + return 0; + } + return idx; + } + + private get currentCachedCompletion(): CachedInlineCompletion | undefined { + if (!this.cache.value) { + return undefined; + } + return this.cache.value.completions[this.fixAndGetIndexOfCurrentSelection()]; + } + + public async showNextInlineCompletion(): Promise { + await this.ensureUpdateWithExplicitContext(); + + const completions = this.cache.value?.completions || []; + if (completions.length > 0) { + const newIdx = (this.fixAndGetIndexOfCurrentSelection() + 1) % completions.length; + this.currentlySelectedCompletionId = completions[newIdx].semanticId; + } else { + this.currentlySelectedCompletionId = undefined; + } + this.onDidChangeEmitter.fire(); + } + + public async showPreviousInlineCompletion(): Promise { + await this.ensureUpdateWithExplicitContext(); + + const completions = this.cache.value?.completions || []; + if (completions.length > 0) { + const newIdx = (this.fixAndGetIndexOfCurrentSelection() + completions.length - 1) % completions.length; + this.currentlySelectedCompletionId = completions[newIdx].semanticId; + } else { + this.currentlySelectedCompletionId = undefined; + } + this.onDidChangeEmitter.fire(); + } + + private async ensureUpdateWithExplicitContext(): Promise { + if (this.updateOperation.value) { + // Restart or wait for current update operation + if (this.updateOperation.value.triggerKind === InlineCompletionTriggerKind.Explicit) { + await this.updateOperation.value.promise; + } else { + await this.update(InlineCompletionTriggerKind.Explicit); + } + } else if (this.cache.value?.triggerKind !== InlineCompletionTriggerKind.Explicit) { + // Refresh cache + await this.update(InlineCompletionTriggerKind.Explicit); + } + } + + //#endregion + + public get ghostText(): GhostText | undefined { + const currentCompletion = this.currentCompletion; + return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel()) : undefined; + } + + get currentCompletion(): LiveInlineCompletion | undefined { + const completion = this.currentCachedCompletion; + if (!completion) { + return undefined; + } + return { + text: completion.inlineCompletion.text, + range: completion.synchronizedRange, + command: completion.inlineCompletion.command, + sourceProvider: completion.inlineCompletion.sourceProvider, + sourceInlineCompletions: completion.inlineCompletion.sourceInlineCompletions, + sourceInlineCompletion: completion.inlineCompletion.sourceInlineCompletion, + }; + } + + get isValid(): boolean { + return this.editor.getPosition().lineNumber === this.triggerPosition.lineNumber; + } + + public scheduleAutomaticUpdate(): void { + // Since updateSoon debounces, starvation can happen. + // To prevent stale cache, we clear the current update operation. + this.updateOperation.clear(); + this.updateSoon.schedule(); + } + + private async update(triggerKind: InlineCompletionTriggerKind): Promise { + if (!this.shouldUpdate()) { + return; + } + + const position = this.editor.getPosition(); + + const promise = createCancelablePromise(async token => { + let result; + try { + result = await provideInlineCompletions(position, + this.editor.getModel(), + { triggerKind }, + token + ); + } catch (e) { + onUnexpectedError(e); + return; + } + + if (token.isCancellationRequested) { + return; + } + + this.cache.value = new SynchronizedInlineCompletionsCache( + this.editor, + result, + () => this.onDidChangeEmitter.fire(), + triggerKind + ); + this.onDidChangeEmitter.fire(); + }); + const operation = new UpdateOperation(promise, triggerKind); + this.updateOperation.value = operation; + await promise; + if (this.updateOperation.value === operation) { + this.updateOperation.clear(); + } + } + + public takeOwnership(disposable: IDisposable): void { + this._register(disposable); + } + + public commitCurrentCompletion(): void { + const completion = this.currentCompletion; + if (completion) { + this.commit(completion); + } + } + + public commit(completion: LiveInlineCompletion): void { + // Mark the cache as stale, but don't dispose it yet, + // otherwise command args might get disposed. + const cache = this.cache.replace(undefined); + + this.editor.executeEdits( + 'inlineSuggestion.accept', + [ + EditOperation.replaceMove(completion.range, completion.text) + ] + ); + if (completion.command) { + this.commandService + .executeCommand(completion.command.id, ...(completion.command.arguments || [])) + .finally(() => { + cache?.dispose(); + }) + .then(undefined, onUnexpectedExternalError); + } else { + cache?.dispose(); + } + + this.onDidChangeEmitter.fire(); + } +} + +class UpdateOperation implements IDisposable { + constructor(public readonly promise: CancelablePromise, public readonly triggerKind: InlineCompletionTriggerKind) { + } + + dispose() { + this.promise.cancel(); + } +} + +/** + * The cache keeps itself in sync with the editor. + * It also owns the completions result and disposes it when the cache is diposed. +*/ +class SynchronizedInlineCompletionsCache extends Disposable { + public readonly completions: readonly CachedInlineCompletion[]; + + constructor( + editor: IActiveCodeEditor, + completionsSource: LiveInlineCompletions, + onChange: () => void, + public readonly triggerKind: InlineCompletionTriggerKind, + ) { + super(); + + const decorationIds = editor.deltaDecorations( + [], + completionsSource.items.map(i => ({ + range: i.range, + options: { + description: 'inline-completion-tracking-range' + }, + })) + ); + this._register(toDisposable(() => { + editor.deltaDecorations(decorationIds, []); + })); + + this.completions = completionsSource.items.map((c, idx) => new CachedInlineCompletion(c, decorationIds[idx])); + + this._register(editor.onDidChangeModelContent(() => { + let hasChanged = false; + const model = editor.getModel(); + for (const c of this.completions) { + const newRange = model.getDecorationRange(c.decorationId); + if (!newRange) { + onUnexpectedError(new Error('Decoration has no range')); + continue; + } + if (!c.synchronizedRange.equalsRange(newRange)) { + hasChanged = true; + c.synchronizedRange = newRange; + } + } + if (hasChanged) { + onChange(); + } + })); + + this._register(completionsSource); + } +} + +class CachedInlineCompletion { + public readonly semanticId: string = JSON.stringify({ + text: this.inlineCompletion.text, + startLine: this.inlineCompletion.range.startLineNumber, + startColumn: this.inlineCompletion.range.startColumn, + command: this.inlineCompletion.command + }); + /** + * The range, synchronized with text model changes. + */ + public synchronizedRange: Range; + + constructor( + public readonly inlineCompletion: LiveInlineCompletion, + public readonly decorationId: string, + ) { + this.synchronizedRange = inlineCompletion.range; + } +} + +export interface NormalizedInlineCompletion extends InlineCompletion { + range: Range; +} + +function leftTrim(str: string): string { + return str.replace(/^\s+/, ''); +} + +export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel): GhostText | undefined { + // This is a single line string + const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range); + + let remainingInsertText: string; + + // Consider these cases + // valueToBeReplaced -> inlineCompletion.text + // "\t\tfoo" -> "\t\tfoobar" (+"bar") + // "\t" -> "\t\tfoobar" (+"\tfoobar") + // "\t\tfoo" -> "\t\t\tfoobar" (+"\t", +"bar") + // "\t\tfoo" -> "\tfoobar" (-"\t", +"\bar") + + const firstNonWsCol = textModel.getLineFirstNonWhitespaceColumn(inlineCompletion.range.startLineNumber); + + if (inlineCompletion.text.startsWith(valueToBeReplaced)) { + remainingInsertText = inlineCompletion.text.substr(valueToBeReplaced.length); + } else if (firstNonWsCol === 0 || inlineCompletion.range.startColumn < firstNonWsCol) { + // Only allow ignoring leading whitespace in indentation. + // This prevents flickering when the user types whitespace that extends an empty range. + const valueToBeReplacedTrimmed = leftTrim(valueToBeReplaced); + const insertTextTrimmed = leftTrim(inlineCompletion.text); + if (!insertTextTrimmed.startsWith(valueToBeReplacedTrimmed)) { + return undefined; + } + remainingInsertText = insertTextTrimmed.substr(valueToBeReplacedTrimmed.length); + } else { + return undefined; + } + + const position = inlineCompletion.range.getEndPosition(); + + const lines = strings.splitLines(remainingInsertText); + + if (lines.length > 1 && textModel.getLineMaxColumn(position.lineNumber) !== position.column) { + // Such ghost text is not supported. + return undefined; + } + + return { + lines, + position + }; +} + +export interface LiveInlineCompletion extends NormalizedInlineCompletion { + sourceProvider: InlineCompletionsProvider; + sourceInlineCompletion: InlineCompletion; + sourceInlineCompletions: InlineCompletions; +} + +/** + * Contains no duplicated items. +*/ +export interface LiveInlineCompletions extends InlineCompletions { + dispose(): void; +} + +function getDefaultRange(position: Position, model: ITextModel): Range { + const word = model.getWordAtPosition(position); + const maxColumn = model.getLineMaxColumn(position.lineNumber); + // By default, always replace up until the end of the current line. + // This default might be subject to change! + return word + ? new Range(position.lineNumber, word.startColumn, position.lineNumber, maxColumn) + : Range.fromPositions(position, position.with(undefined, maxColumn)); +} + +async function provideInlineCompletions( + position: Position, + model: ITextModel, + context: InlineCompletionContext, + token: CancellationToken = CancellationToken.None +): Promise { + const defaultReplaceRange = getDefaultRange(position, model); + + const providers = InlineCompletionsProviderRegistry.all(model); + const results = await Promise.all( + providers.map( + async provider => { + const completions = await provider.provideInlineCompletions(model, position, context, token); + return ({ + completions, + provider, + dispose: () => { + if (completions) { + provider.freeInlineCompletions(completions); + } + } + }); + } + ) + ); + + const itemsByHash = new Map(); + for (const result of results) { + const completions = result.completions; + if (completions) { + for (const item of completions.items.map(item => ({ + text: item.text, + range: item.range ? Range.lift(item.range) : defaultReplaceRange, + command: item.command, + sourceProvider: result.provider, + sourceInlineCompletions: completions, + sourceInlineCompletion: item + }))) { + if (item.range.startLineNumber !== item.range.endLineNumber) { + // Ignore invalid ranges. + continue; + } + itemsByHash.set(JSON.stringify({ text: item.text, range: item.range }), item); + } + } + } + + return { + items: [...itemsByHash.values()], + dispose: () => { + for (const result of results) { + result.dispose(); + } + }, + }; +} diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts b/lib/vscode/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts new file mode 100644 index 000000000000..eac1ad30f538 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { CompletionItemInsertTextRule } from 'vs/editor/common/modes'; +import { BaseGhostTextWidgetModel, GhostText } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; +import { inlineCompletionToGhostText, NormalizedInlineCompletion } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget'; + +export class SuggestWidgetAdapterModel extends BaseGhostTextWidgetModel { + private isSuggestWidgetVisible: boolean = false; + private currentGhostText: GhostText | undefined = undefined; + private _isActive: boolean = false; + + public override minReservedLineCount: number = 0; + + public get isActive() { return this._isActive; } + + // This delay fixes an suggest widget issue when typing "." immediately restarts the suggestion session. + private setInactiveDelayed = this._register(new RunOnceScheduler(() => { + if (!this.isSuggestWidgetVisible) { + if (this.isActive) { + this._isActive = false; + this.onDidChangeEmitter.fire(); + } + } + }, 100)); + + constructor( + editor: IActiveCodeEditor + ) { + super(editor); + + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + let isBoundToSuggestWidget = false; + const bindToSuggestWidget = () => { + if (isBoundToSuggestWidget) { + return; + } + isBoundToSuggestWidget = true; + + this._register(suggestController.widget.value.onDidShow(() => { + this.isSuggestWidgetVisible = true; + this._isActive = true; + this.updateFromSuggestion(); + })); + this._register(suggestController.widget.value.onDidHide(() => { + this.isSuggestWidgetVisible = false; + this.setInactiveDelayed.schedule(); + this.minReservedLineCount = 0; + this.updateFromSuggestion(); + })); + this._register(suggestController.widget.value.onDidFocus(() => { + this.isSuggestWidgetVisible = true; + this._isActive = true; + this.updateFromSuggestion(); + })); + }; + + this._register(Event.once(suggestController.model.onDidTrigger)(e => { + bindToSuggestWidget(); + })); + } + this.updateFromSuggestion(); + + this._register(toDisposable(() => { + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + suggestController.stopForceRenderingAbove(); + } + })); + } + + public override setExpanded(expanded: boolean): void { + super.setExpanded(expanded); + this.updateFromSuggestion(); + } + + private isSuggestionPreviewEnabled(): boolean { + const suggestOptions = this.editor.getOption(EditorOption.suggest); + return suggestOptions.preview; + } + + private updateFromSuggestion(): void { + const suggestController = SuggestController.get(this.editor); + if (!suggestController) { + this.setCurrentInlineCompletion(undefined); + return; + } + if (!this.isSuggestWidgetVisible) { + this.setCurrentInlineCompletion(undefined); + return; + } + const focusedItem = suggestController.widget.value.getFocusedItem(); + if (!focusedItem) { + this.setCurrentInlineCompletion(undefined); + return; + } + + // TODO: item.isResolved + this.setCurrentInlineCompletion( + getInlineCompletion( + suggestController, + this.editor.getPosition(), + focusedItem + ) + ); + } + + private setCurrentInlineCompletion(completion: NormalizedInlineCompletion | undefined): void { + this.currentGhostText = completion + ? ( + inlineCompletionToGhostText(completion, this.editor.getModel()) || + // Show an invisible ghost text to reserve space + { + lines: [], + position: completion.range.getEndPosition(), + } + ) : undefined; + + if (this.currentGhostText && this.expanded) { + this.minReservedLineCount = Math.max(this.minReservedLineCount, this.currentGhostText.lines.length - 1); + } + + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + if (this.minReservedLineCount >= 1 && this.isSuggestionPreviewEnabled()) { + suggestController.forceRenderingAbove(); + } else { + suggestController.stopForceRenderingAbove(); + } + } + + this.onDidChangeEmitter.fire(); + } + + public override get ghostText(): GhostText | undefined { + return this.isSuggestionPreviewEnabled() + ? this.currentGhostText + : undefined; + } +} + +function getInlineCompletion(suggestController: SuggestController, position: Position, suggestion: ISelectedSuggestion): NormalizedInlineCompletion { + const item = suggestion.item; + + if (Array.isArray(item.completion.additionalTextEdits)) { + // cannot represent additional text edits + return { + text: '', + range: Range.fromPositions(position, position), + }; + } + + let { insertText } = item.completion; + if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) { + const snippet = new SnippetParser().parse(insertText); + const model = suggestController.editor.getModel()!; + SnippetSession.adjustWhitespace( + model, position, snippet, + true, + true + ); + insertText = snippet.toString(); + } + + const info = suggestController.getOverwriteInfo(item, false); + return { + text: insertText, + range: Range.fromPositions( + position.delta(0, -info.overwriteBefore), + position.delta(0, Math.max(info.overwriteAfter, 0)) + ), + }; +} diff --git a/lib/vscode/src/vs/editor/contrib/inlineCompletions/utils.ts b/lib/vscode/src/vs/editor/contrib/inlineCompletions/utils.ts new file mode 100644 index 000000000000..9a3b1a074699 --- /dev/null +++ b/lib/vscode/src/vs/editor/contrib/inlineCompletions/utils.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, trackDisposable } from 'vs/base/common/lifecycle'; + +// TODO: merge this class into Matt's MutableDisposable. +/** + * Manages the lifecycle of a disposable value that may be changed. + * + * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can + * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. + */ +export class MutableDisposable implements IDisposable { + private _value?: T; + private _isDisposed = false; + + constructor() { + trackDisposable(this); + } + + get value(): T | undefined { + return this._isDisposed ? undefined : this._value; + } + + set value(value: T | undefined) { + if (this._isDisposed || value === this._value) { + return; + } + + this._value?.dispose(); + this._value = value; + } + + clear() { + this.value = undefined; + } + + dispose(): void { + this._isDisposed = true; + this._value?.dispose(); + this._value = undefined; + } + + replace(newValue: T | undefined): T | undefined { + const oldValue = this._value; + this._value = newValue; + return oldValue; + } +} diff --git a/lib/vscode/src/vs/editor/contrib/linesOperations/linesOperations.ts b/lib/vscode/src/vs/editor/contrib/linesOperations/linesOperations.ts index 64b689a5fc25..99abacb0715c 100644 --- a/lib/vscode/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/lib/vscode/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -1045,7 +1045,41 @@ export class TitleCaseAction extends AbstractCaseAction { } } +class BackwardsCompatibleRegExp { + + private _actual: RegExp | null; + private _evaluated: boolean; + + constructor( + private readonly _pattern: string, + private readonly _flags: string + ) { + this._actual = null; + this._evaluated = false; + } + + public get(): RegExp | null { + if (!this._evaluated) { + this._evaluated = true; + try { + this._actual = new RegExp(this._pattern, this._flags); + } catch (err) { + // this browser does not support this regular expression + } + } + return this._actual; + } + + public isSupported(): boolean { + return (this.get() !== null); + } +} + export class SnakeCaseAction extends AbstractCaseAction { + + public static regExp1 = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu'); + public static regExp2 = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu'); + constructor() { super({ id: 'editor.action.transformToSnakecase', @@ -1056,9 +1090,15 @@ export class SnakeCaseAction extends AbstractCaseAction { } protected _modifyText(text: string, wordSeparators: string): string { + const regExp1 = SnakeCaseAction.regExp1.get(); + const regExp2 = SnakeCaseAction.regExp2.get(); + if (!regExp1 || !regExp2) { + // cannot support this + return text; + } return (text - .replace(/(\p{Ll})(\p{Lu})/gmu, '$1_$2') - .replace(/(\p{Lu}|\p{N})(\p{Lu})(\p{Ll})/gmu, '$1_$2$3') + .replace(regExp1, '$1_$2') + .replace(regExp2, '$1_$2$3') .toLocaleLowerCase() ); } @@ -1084,4 +1124,7 @@ registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); registerEditorAction(TitleCaseAction); -registerEditorAction(SnakeCaseAction); + +if (SnakeCaseAction.regExp1.isSupported() && SnakeCaseAction.regExp2.isSupported()) { + registerEditorAction(SnakeCaseAction); +} diff --git a/lib/vscode/src/vs/editor/contrib/linkedEditing/linkedEditing.ts b/lib/vscode/src/vs/editor/contrib/linkedEditing/linkedEditing.ts index 8a586c1b13a0..7b5fa2d99e3e 100644 --- a/lib/vscode/src/vs/editor/contrib/linkedEditing/linkedEditing.ts +++ b/lib/vscode/src/vs/editor/contrib/linkedEditing/linkedEditing.ts @@ -39,6 +39,7 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont public static readonly ID = 'editor.contrib.linkedEditing'; private static readonly DECORATION = ModelDecorationOptions.register({ + description: 'linked-editing', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: DECORATION_CLASS_NAME }); diff --git a/lib/vscode/src/vs/editor/contrib/links/links.ts b/lib/vscode/src/vs/editor/contrib/links/links.ts index 5cfe69127dbf..a79bc4d3dd84 100644 --- a/lib/vscode/src/vs/editor/contrib/links/links.ts +++ b/lib/vscode/src/vs/editor/contrib/links/links.ts @@ -66,11 +66,13 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { const decoration = { general: ModelDecorationOptions.register({ + description: 'detected-link', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true, inlineClassName: 'detected-link' }), active: ModelDecorationOptions.register({ + description: 'detected-link-active', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true, inlineClassName: 'detected-link-active' diff --git a/lib/vscode/src/vs/editor/contrib/multicursor/multicursor.ts b/lib/vscode/src/vs/editor/contrib/multicursor/multicursor.ts index cfbd4b2d2082..5edc94877d74 100644 --- a/lib/vscode/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/lib/vscode/src/vs/editor/contrib/multicursor/multicursor.ts @@ -1047,6 +1047,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ + description: 'selection-highlight-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'selectionHighlight', overviewRuler: { @@ -1056,6 +1057,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }); private static readonly _SELECTION_HIGHLIGHT = ModelDecorationOptions.register({ + description: 'selection-highlight', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'selectionHighlight', }); diff --git a/lib/vscode/src/vs/editor/contrib/peekView/media/peekViewWidget.css b/lib/vscode/src/vs/editor/contrib/peekView/media/peekViewWidget.css index ed8f673ab4bc..387852d6214d 100644 --- a/lib/vscode/src/vs/editor/contrib/peekView/media/peekViewWidget.css +++ b/lib/vscode/src/vs/editor/contrib/peekView/media/peekViewWidget.css @@ -33,6 +33,7 @@ .monaco-editor .peekview-widget .head .peekview-title .filename { overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } .monaco-editor .peekview-widget .head .peekview-title .meta:not(:empty)::before { diff --git a/lib/vscode/src/vs/editor/contrib/peekView/peekView.ts b/lib/vscode/src/vs/editor/contrib/peekView/peekView.ts index 6dbcde25dac0..3df39461d7b5 100644 --- a/lib/vscode/src/vs/editor/contrib/peekView/peekView.ts +++ b/lib/vscode/src/vs/editor/contrib/peekView/peekView.ts @@ -219,7 +219,7 @@ export abstract class PeekViewWidget extends ZoneWidget { setTitle(primaryHeading: string, secondaryHeading?: string): void { if (this._primaryHeading && this._secondaryHeading) { this._primaryHeading.innerText = primaryHeading; - this._primaryHeading.setAttribute('aria-label', primaryHeading); + this._primaryHeading.setAttribute('title', primaryHeading); if (secondaryHeading) { this._secondaryHeading.innerText = secondaryHeading; } else { diff --git a/lib/vscode/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/lib/vscode/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts index ec7d2ab922e0..90681ff6aac0 100644 --- a/lib/vscode/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts +++ b/lib/vscode/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -8,7 +8,7 @@ import { IEditor } from 'vs/editor/common/editorCommon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { stripIcons } from 'vs/base/common/iconLabels'; @@ -20,9 +20,9 @@ export abstract class AbstractEditorCommandsQuickAccessProvider extends Abstract keybindingService: IKeybindingService, commandService: ICommandService, telemetryService: ITelemetryService, - notificationService: INotificationService + dialogService: IDialogService ) { - super(options, instantiationService, keybindingService, commandService, telemetryService, notificationService); + super(options, instantiationService, keybindingService, commandService, telemetryService, dialogService); } /** diff --git a/lib/vscode/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/lib/vscode/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index ef8cf91b8ab2..d22faaeac288 100644 --- a/lib/vscode/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/lib/vscode/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -195,6 +195,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu { range, options: { + description: 'quick-access-range-highlight', className: 'rangeHighlight', isWholeLine: true } @@ -204,6 +205,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu { range, options: { + description: 'quick-access-range-highlight-overview', overviewRuler: { color: themeColorFromId(overviewRulerRangeHighlight), position: OverviewRulerLane.Full diff --git a/lib/vscode/src/vs/editor/contrib/snippet/snippetController2.ts b/lib/vscode/src/vs/editor/contrib/snippet/snippetController2.ts index 95bd300f0636..8a686a5d152f 100644 --- a/lib/vscode/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/lib/vscode/src/vs/editor/contrib/snippet/snippetController2.ts @@ -43,7 +43,7 @@ const _defaultOptions: ISnippetInsertOptions = { export class SnippetController2 implements IEditorContribution { - public static ID = 'snippetController2'; + public static readonly ID = 'snippetController2'; static get(editor: ICodeEditor): SnippetController2 { return editor.getContribution(SnippetController2.ID); diff --git a/lib/vscode/src/vs/editor/contrib/snippet/snippetParser.ts b/lib/vscode/src/vs/editor/contrib/snippet/snippetParser.ts index 8986345e970b..ab85481a3a8a 100644 --- a/lib/vscode/src/vs/editor/contrib/snippet/snippetParser.ts +++ b/lib/vscode/src/vs/editor/contrib/snippet/snippetParser.ts @@ -388,11 +388,11 @@ export class FormatString extends Marker { } private _toPascalCase(value: string): string { - const match = value.match(/[a-z]+/gi); + const match = value.match(/[a-z0-9]+/gi); if (!match) { return value; } - return match.map(function (word) { + return match.map(word => { return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); }) diff --git a/lib/vscode/src/vs/editor/contrib/snippet/snippetSession.ts b/lib/vscode/src/vs/editor/contrib/snippet/snippetSession.ts index 90abafa2549f..9b875e4051fb 100644 --- a/lib/vscode/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/lib/vscode/src/vs/editor/contrib/snippet/snippetSession.ts @@ -45,10 +45,10 @@ export class OneSnippet { _nestingLevel: number = 1; private static readonly _decor = { - active: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), - inactive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), - activeFinal: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), - inactiveFinal: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), + active: ModelDecorationOptions.register({ description: 'snippet-placeholder-1', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), + inactive: ModelDecorationOptions.register({ description: 'snippet-placeholder-2', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'snippet-placeholder' }), + activeFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-3', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), + inactiveFinal: ModelDecorationOptions.register({ description: 'snippet-placeholder-4', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'finish-snippet-placeholder' }), }; constructor( diff --git a/lib/vscode/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/lib/vscode/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index a9d41faf92f3..21438244b864 100644 --- a/lib/vscode/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/lib/vscode/src/vs/editor/contrib/snippet/test/snippetParser.test.ts @@ -655,6 +655,7 @@ suite('SnippetParser', () => { assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar'), 'Bar'); assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat'); assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo'); assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input'); // if diff --git a/lib/vscode/src/vs/editor/contrib/suggest/media/suggest.css b/lib/vscode/src/vs/editor/contrib/suggest/media/suggest.css index 29fa4575fa9f..cf73d2720119 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/media/suggest.css +++ b/lib/vscode/src/vs/editor/contrib/suggest/media/suggest.css @@ -306,6 +306,10 @@ margin-right: 4px; } +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .suggest-icon { + color: inherit; +} + .monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .icon, .monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .suggest-icon::before { display: none; } diff --git a/lib/vscode/src/vs/editor/contrib/suggest/resizable.ts b/lib/vscode/src/vs/editor/contrib/suggest/resizable.ts index 88e1e73ed201..f0e7ead560e3 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/resizable.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/resizable.ts @@ -121,6 +121,8 @@ export class ResizableHTMLElement { this._eastSash.dispose(); this._westSash.dispose(); this._sashListener.dispose(); + this._onDidResize.dispose(); + this._onDidWillResize.dispose(); this.domNode.remove(); } diff --git a/lib/vscode/src/vs/editor/contrib/suggest/suggest.ts b/lib/vscode/src/vs/editor/contrib/suggest/suggest.ts index c8e46227448a..ec192ae88cfe 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/suggest.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/suggest.ts @@ -155,6 +155,7 @@ export class CompletionOptions { readonly snippetSortOrder = SnippetSortOrder.Bottom, readonly kindFilter = new Set(), readonly providerFilter = new Set(), + readonly showDeprecated = true ) { } } @@ -216,6 +217,10 @@ export async function provideSuggestionItems( } for (let suggestion of container.suggestions) { if (!options.kindFilter.has(suggestion.kind)) { + // skip if not showing deprecated suggestions + if (!options.showDeprecated && suggestion?.tags?.includes(modes.CompletionItemTag.Deprecated)) { + continue; + } // fill in default range when missing if (!suggestion.range) { suggestion.range = defaultRange; diff --git a/lib/vscode/src/vs/editor/contrib/suggest/suggestController.ts b/lib/vscode/src/vs/editor/contrib/suggest/suggestController.ts index bca0fd625576..eff083ad88c0 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/suggestController.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/suggestController.ts @@ -61,7 +61,7 @@ class LineSuffix { const end = _model.getPositionAt(offset + 1); this._marker = _model.deltaDecorations([], [{ range: Range.fromPositions(_position, end), - options: { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } + options: { description: 'suggest-line-suffix', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }]); } } @@ -587,6 +587,14 @@ export class SuggestController implements IEditorContribution { resetWidgetSize(): void { this.widget.value.resetPersistedSize(); } + + forceRenderingAbove() { + this.widget.value.forceRenderingAbove(); + } + + stopForceRenderingAbove() { + this.widget.value.stopForceRenderingAbove(); + } } export class TriggerSuggestAction extends EditorAction { diff --git a/lib/vscode/src/vs/editor/contrib/suggest/suggestModel.ts b/lib/vscode/src/vs/editor/contrib/suggest/suggestModel.ts index 747f78a4b4c8..7089173d9547 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/suggestModel.ts @@ -24,6 +24,8 @@ import { isLowSurrogate, isHighSurrogate, getLeadingWhitespace } from 'vs/base/c import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ILogService } from 'vs/platform/log/common/log'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export interface ICancelEvent { readonly retrigger: boolean; @@ -95,6 +97,20 @@ export const enum State { Auto = 2 } +function shouldPreventQuickSuggest(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean { + return ( + Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible')) + && !Boolean(configurationService.getValue('editor.inlineSuggest.allowQuickSuggestions')) + ); +} + +function shouldPreventSuggestOnTriggerCharacters(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean { + return ( + Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible')) + && !Boolean(configurationService.getValue('editor.inlineSuggest.allowSuggestOnTriggerCharacters')) + ); +} + export class SuggestModel implements IDisposable { private readonly _toDispose = new DisposableStore(); @@ -123,6 +139,8 @@ export class SuggestModel implements IDisposable { @IClipboardService private readonly _clipboardService: IClipboardService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { this._currentSelection = this._editor.getSelection() || new Selection(1, 1, 1, 1); @@ -213,6 +231,10 @@ export class SuggestModel implements IDisposable { const checkTriggerCharacter = (text?: string) => { + if (shouldPreventSuggestOnTriggerCharacters(this._contextKeyService, this._configurationService)) { + return; + } + if (!text) { // came here from the compositionEnd-event const position = this._editor.getPosition()!; @@ -351,6 +373,11 @@ export class SuggestModel implements IDisposable { } } + if (shouldPreventQuickSuggest(this._contextKeyService, this._configurationService)) { + // do not trigger quick suggestions if inline suggestions are shown + return; + } + // we made it till here -> trigger now this.trigger({ auto: true, shy: false }); @@ -428,13 +455,13 @@ export class SuggestModel implements IDisposable { break; } - const itemKindFilter = SuggestModel._createItemKindFilter(this._editor); + const { itemKind: itemKindFilter, showDeprecated } = SuggestModel._createSuggestFilter(this._editor); const wordDistance = WordDistance.create(this._editorWorkerService, this._editor); const completions = provideSuggestionItems( model, this._editor.getPosition(), - new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom), + new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom, showDeprecated), suggestCtx, this._requestToken.token ); @@ -502,7 +529,7 @@ export class SuggestModel implements IDisposable { }); } - private static _createItemKindFilter(editor: ICodeEditor): Set { + private static _createSuggestFilter(editor: ICodeEditor): { itemKind: Set; showDeprecated: boolean } { // kind filter and snippet sort rules const result = new Set(); @@ -543,7 +570,7 @@ export class SuggestModel implements IDisposable { if (!suggestOptions.showUsers) { result.add(CompletionItemKind.User); } if (!suggestOptions.showIssues) { result.add(CompletionItemKind.Issue); } - return result; + return { itemKind: result, showDeprecated: suggestOptions.showDeprecated }; } private _onNewContext(ctx: LineContext): void { diff --git a/lib/vscode/src/vs/editor/contrib/suggest/suggestWidget.ts b/lib/vscode/src/vs/editor/contrib/suggest/suggestWidget.ts index 8e0171cfac7f..2bcba6043b01 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -21,7 +21,7 @@ import { Context as SuggestContext, CompletionItem } from './suggest'; import { CompletionModel } from './completionModel'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { registerColor, editorWidgetBackground, quickInputListFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorWidgetBackground, quickInputListFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground, quickInputListFocusForeground, listFocusHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -40,8 +40,10 @@ import { clamp } from 'vs/base/common/numbers'; export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hc: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); +export const editorSuggestWidgetSelectedForeground = registerColor('editorSuggestWidget.selectedForeground', { dark: quickInputListFocusForeground, light: quickInputListFocusForeground, hc: quickInputListFocusForeground }, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hc: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); +export const editorSuggestWidgetHighlightFocusForeground = registerColor('editorSuggestWidget.focusHighlightForeground', { dark: listFocusHighlightForeground, light: listFocusHighlightForeground, hc: listFocusHighlightForeground }, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); const enum State { Hidden, @@ -104,6 +106,7 @@ export class SuggestWidget implements IDisposable { private _ignoreFocusEvents: boolean = false; private _completionModel?: CompletionModel; private _cappedHeight?: { wanted: number; capped: number; }; + private _forceRenderingAbove: boolean = false; private _explainMode: boolean = false; readonly element: ResizableHTMLElement; @@ -804,7 +807,7 @@ export class SuggestWidget implements IDisposable { height = maxHeight; } - if (height > maxHeightBelow) { + if (height > maxHeightBelow || this._forceRenderingAbove) { this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE); this.element.enableSashes(true, true, false, false); maxHeight = maxHeightAbove; @@ -875,6 +878,17 @@ export class SuggestWidget implements IDisposable { private _setDetailsVisible(value: boolean) { this._storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL, StorageTarget.USER); } + + forceRenderingAbove() { + if (!this._forceRenderingAbove) { + this._forceRenderingAbove = true; + this._layout(this._persistedSize.restore()); + } + } + + stopForceRenderingAbove() { + this._forceRenderingAbove = false; + } } export class SuggestContentWidget implements IContentWidget { @@ -972,11 +986,22 @@ registerThemingParticipant((theme, collector) => { if (matchHighlight) { collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`); } + + const matchHighlightFocus = theme.getColor(editorSuggestWidgetHighlightFocusForeground); + if (matchHighlight) { + collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { color: ${matchHighlightFocus}; }`); + } + const foreground = theme.getColor(editorSuggestWidgetForeground); if (foreground) { collector.addRule(`.monaco-editor .suggest-widget, .monaco-editor .suggest-details { color: ${foreground}; }`); } + const selectedForeground = theme.getColor(editorSuggestWidgetSelectedForeground); + if (selectedForeground) { + collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused { color: ${selectedForeground}; }`); + } + const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.monaco-editor .suggest-details a { color: ${link}; }`); diff --git a/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts b/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts index 2734906e876f..148e2b97d1c1 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts @@ -320,6 +320,7 @@ export class SuggestDetailsOverlay implements IOverlayWidget { } dispose(): void { + this._resizable.dispose(); this._disposables.dispose(); this.hide(); } diff --git a/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index a3763880de2a..0eae16979859 100644 --- a/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -209,13 +209,13 @@ export class ItemRenderer implements IListRenderer this.update())); this.update(); } diff --git a/lib/vscode/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts b/lib/vscode/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts index f4839b34f218..97c3244e8912 100644 --- a/lib/vscode/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts +++ b/lib/vscode/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts @@ -15,7 +15,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -32,9 +32,9 @@ export class StandaloneCommandsQuickAccessProvider extends AbstractEditorCommand @IKeybindingService keybindingService: IKeybindingService, @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, - @INotificationService notificationService: INotificationService + @IDialogService dialogService: IDialogService ) { - super({ showAlias: false }, instantiationService, keybindingService, commandService, telemetryService, notificationService); + super({ showAlias: false }, instantiationService, keybindingService, commandService, telemetryService, dialogService); } protected async getCommandPicks(): Promise> { diff --git a/lib/vscode/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css b/lib/vscode/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css index f2d4dbcecb2b..c48737cfaf48 100644 --- a/lib/vscode/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css +++ b/lib/vscode/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css @@ -12,6 +12,11 @@ color: #0066BF; } +.vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight, +.vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight { + color: #9DDDFF; +} + .vs-dark .quick-input-widget .monaco-highlighted-label .highlight, .vs-dark .quick-input-widget .monaco-highlighted-label .highlight { color: #0097fb; diff --git a/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 89dff41d626b..77884d5ffde9 100644 --- a/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -34,6 +34,7 @@ import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standal import { IModelService } from 'vs/editor/common/services/modelService'; import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; import { URI } from 'vs/base/common/uri'; +import { StandaloneCodeEditorServiceImpl } from 'vs/editor/standalone/browser/standaloneCodeServiceImpl'; /** * Description of an action contribution @@ -363,6 +364,20 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon return toDispose; } + + protected override _triggerCommand(handlerId: string, payload: any): void { + if (this._codeEditorService instanceof StandaloneCodeEditorServiceImpl) { + // Help commands find this editor as the active editor + try { + this._codeEditorService.setActiveCodeEditor(this); + super._triggerCommand(handlerId, payload); + } finally { + this._codeEditorService.setActiveCodeEditor(null); + } + } else { + super._triggerCommand(handlerId, payload); + } + } } export class StandaloneEditor extends StandaloneCodeEditor implements IStandaloneCodeEditor { diff --git a/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts b/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts index f7808cfa6fc4..8bc8c9d7c103 100644 --- a/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts +++ b/lib/vscode/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts @@ -12,12 +12,13 @@ import { IRange } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { private readonly _editorIsOpen: IContextKey; + private _activeCodeEditor: ICodeEditor | null; constructor( styleSheet: GlobalStyleSheet | null, @@ -28,6 +29,7 @@ export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { this.onCodeEditorAdd(() => this._checkContextKey()); this.onCodeEditorRemove(() => this._checkContextKey()); this._editorIsOpen = contextKeyService.createKey('editorIsOpen', false); + this._activeCodeEditor = null; } private _checkContextKey(): void { @@ -41,8 +43,12 @@ export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { this._editorIsOpen.set(hasCodeEditor); } + public setActiveCodeEditor(activeCodeEditor: ICodeEditor | null): void { + this._activeCodeEditor = activeCodeEditor; + } + public getActiveCodeEditor(): ICodeEditor | null { - return null; // not supported in the standalone case + return this._activeCodeEditor; } public openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { @@ -53,7 +59,7 @@ export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { return Promise.resolve(this.doOpenEditor(source, input)); } - private doOpenEditor(editor: ICodeEditor, input: IResourceEditorInput): ICodeEditor | null { + private doOpenEditor(editor: ICodeEditor, input: ITextResourceEditorInput): ICodeEditor | null { const model = this.findModel(editor, input.resource); if (!model) { if (input.resource) { diff --git a/lib/vscode/src/vs/editor/standalone/browser/standaloneLanguages.ts b/lib/vscode/src/vs/editor/standalone/browser/standaloneLanguages.ts index dc1125f7f3a8..e4b78662521a 100644 --- a/lib/vscode/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/lib/vscode/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -547,6 +547,13 @@ export function registerDocumentRangeSemanticTokensProvider(languageId: string, return modes.DocumentRangeSemanticTokensProviderRegistry.register(languageId, provider); } +/** + * Register an inline completions provider. + */ +export function registerInlineCompletionsProvider(languageId: string, provider: modes.InlineCompletionsProvider): IDisposable { + return modes.InlineCompletionsProviderRegistry.register(languageId, provider); +} + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -613,6 +620,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { registerSelectionRangeProvider: registerSelectionRangeProvider, registerDocumentSemanticTokensProvider: registerDocumentSemanticTokensProvider, registerDocumentRangeSemanticTokensProvider: registerDocumentRangeSemanticTokensProvider, + registerInlineCompletionsProvider: registerInlineCompletionsProvider, // enums DocumentHighlightKind: standaloneEnums.DocumentHighlightKind, @@ -624,7 +632,8 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { IndentAction: standaloneEnums.IndentAction, CompletionTriggerKind: standaloneEnums.CompletionTriggerKind, SignatureHelpTriggerKind: standaloneEnums.SignatureHelpTriggerKind, - InlineHintKind: standaloneEnums.InlineHintKind, + InlayHintKind: standaloneEnums.InlayHintKind, + InlineCompletionTriggerKind: standaloneEnums.InlineCompletionTriggerKind, // classes FoldingRangeKind: modes.FoldingRangeKind, diff --git a/lib/vscode/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/lib/vscode/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 180961e72fdb..ece509fa0d03 100644 --- a/lib/vscode/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/lib/vscode/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -233,7 +233,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._updateCSS(); }); - window.matchMedia('(forced-colors: active)').addEventListener('change', () => { + dom.addMatchMediaChangeListener('(forced-colors: active)', () => { this._updateActualTheme(); }); } diff --git a/lib/vscode/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/lib/vscode/src/vs/editor/standalone/common/monarch/monarchCompile.ts index 1357baefea74..04dc7241beba 100644 --- a/lib/vscode/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/lib/vscode/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -86,15 +86,14 @@ function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): * @example /@@text/ will not be replaced and will become /@text/. */ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { + // @@ must be interpreted as a literal @, so we replace all occurences of @@ with a placeholder character + str = str.replace(/@@/g, `\x01`); + let n = 0; let hadExpansion: boolean; do { hadExpansion = false; - str = str.replace(/(.|^)@(\w+)/g, function (s, charBeforeAtSign, attr?) { - if (charBeforeAtSign === '@') { - // do not expand @@ - return s; - } + str = str.replace(/@(\w+)/g, function (s, attr?) { hadExpansion = true; let sub = ''; if (typeof (lexer[attr]) === 'string') { @@ -108,13 +107,13 @@ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { throw monarchCommon.createError(lexer, 'attribute reference \'' + attr + '\' must be a string, used at: ' + str); } } - return charBeforeAtSign + (monarchCommon.empty(sub) ? '' : '(?:' + sub + ')'); + return (monarchCommon.empty(sub) ? '' : '(?:' + sub + ')'); }); n++; } while (hadExpansion && n < 5); // handle escaped @@ - str = str.replace(/@@/g, '@'); + str = str.replace(/\x01/g, '@'); let flags = (lexer.ignoreCase ? 'i' : '') + (lexer.unicode ? 'u' : ''); return new RegExp(str, flags); diff --git a/lib/vscode/src/vs/editor/standalone/common/themes.ts b/lib/vscode/src/vs/editor/standalone/common/themes.ts index 9e07df46bac5..cf0b7945091e 100644 --- a/lib/vscode/src/vs/editor/standalone/common/themes.ts +++ b/lib/vscode/src/vs/editor/standalone/common/themes.ts @@ -5,7 +5,7 @@ import { editorActiveIndentGuides, editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; import { IStandaloneThemeData } from 'vs/editor/standalone/common/standaloneThemeService'; -import { editorBackground, editorForeground, editorInactiveSelection, editorSelectionHighlight } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, editorForeground, editorInactiveSelection, editorSelectionHighlight, listFocusHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; /* -------------------------------- Begin vs theme -------------------------------- */ export const vs: IStandaloneThemeData = { @@ -73,7 +73,8 @@ export const vs: IStandaloneThemeData = { [editorInactiveSelection]: '#E5EBF1', [editorIndentGuides]: '#D3D3D3', [editorActiveIndentGuides]: '#939393', - [editorSelectionHighlight]: '#ADD6FF4D' + [editorSelectionHighlight]: '#ADD6FF4D', + [listFocusHighlightForeground]: '#9DDDFF' } }; /* -------------------------------- End vs theme -------------------------------- */ diff --git a/lib/vscode/src/vs/editor/standalone/test/monarch/monarch.test.ts b/lib/vscode/src/vs/editor/standalone/test/monarch/monarch.test.ts index fd57288fdca7..95686726d7e2 100644 --- a/lib/vscode/src/vs/editor/standalone/test/monarch/monarch.test.ts +++ b/lib/vscode/src/vs/editor/standalone/test/monarch/monarch.test.ts @@ -264,4 +264,31 @@ suite('Monarch', () => { ]); }); + test('microsoft/monaco-editor#2424: Allow to target @@', () => { + const modeService = new ModeServiceImpl(); + + const tokenizer = createMonarchTokenizer(modeService, 'test', { + ignoreCase: false, + tokenizer: { + root: [ + { + regex: /@@@@/, + action: { token: 'ham' } + }, + ], + }, + }); + + const lines = [ + `@@` + ]; + + const actualTokens = getTokens(tokenizer, lines); + assert.deepStrictEqual(actualTokens, [ + [ + new Token(0, 'ham.test', 'test'), + ] + ]); + }); + }); diff --git a/lib/vscode/src/vs/editor/test/browser/controller/cursor.test.ts b/lib/vscode/src/vs/editor/test/browser/controller/cursor.test.ts index c5e45cc8351b..b490ef71c792 100644 --- a/lib/vscode/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/lib/vscode/src/vs/editor/test/browser/controller/cursor.test.ts @@ -2608,6 +2608,186 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + + test('issue #122914: Left delete behavior in some languages is changed (useTabStops: false)', () => { + let model = createTextModel( + [ + 'เธชเธงเธฑเธชเธ”เธต' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { + editor.setSelections([ + new Selection(1, 7, 1, 7) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'เธชเธงเธฑเธชเธ”'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'เธชเธงเธฑเธช'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'เธชเธงเธฑ'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'เธชเธง'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'เธช'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); + + test('issue #99629: Emoji modifiers in text treated separately when using backspace', () => { + const model = createTextModel( + [ + '๐Ÿ‘ถ๐Ÿพ' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { + const len = model.getValueLength(); + editor.setSelections([ + new Selection(1, 1 + len, 1, 1 + len) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); + + test('issue #99629: Emoji modifiers in text treated separately when using backspace (ZWJ sequence)', () => { + let model = createTextModel( + [ + '๐Ÿ‘จโ€๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ‘งโ€๐Ÿ‘ฆ' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { + const len = model.getValueLength(); + editor.setSelections([ + new Selection(1, 1 + len, 1, 1 + len) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '๐Ÿ‘จโ€๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ‘ง'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '๐Ÿ‘จโ€๐Ÿ‘ฉ๐Ÿฝ'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '๐Ÿ‘จ'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); + + test('issue #105730: move left behaves differently for multiple cursors', () => { + const model = createTextModel('asdfghjkl, asdfghjkl, asdfghjkl, '); + + withTestCodeEditor( + null, + { + model: model, + wordWrap: 'wordWrapColumn', + wordWrapColumn: 24 + }, + (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 10, 1, 12), + new Selection(1, 21, 1, 23), + new Selection(1, 32, 1, 34) + ]); + moveLeft(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 10, 1, 10), + new Selection(1, 21, 1, 21), + new Selection(1, 32, 1, 32) + ]); + + viewModel.setSelections('test', [ + new Selection(1, 10, 1, 12), + new Selection(1, 21, 1, 23), + new Selection(1, 32, 1, 34) + ]); + moveLeft(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 10, 1, 11), + new Selection(1, 21, 1, 22), + new Selection(1, 32, 1, 33) + ]); + }); + }); + + test('issue #105730: move right should always skip wrap point', () => { + const model = createTextModel('asdfghjkl, asdfghjkl, asdfghjkl, \nasdfghjkl,'); + + withTestCodeEditor( + null, + { + model: model, + wordWrap: 'wordWrapColumn', + wordWrapColumn: 24 + }, + (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 22, 1, 22) + ]); + moveRight(editor, viewModel, false); + moveRight(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 24, 1, 24), + ]); + + viewModel.setSelections('test', [ + new Selection(1, 22, 1, 22) + ]); + moveRight(editor, viewModel, true); + moveRight(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 22, 1, 24), + ]); + } + ); + }); + + test('issue #123178: sticky tab in consecutive wrapped lines', () => { + const model = createTextModel(' aaaa aaaa', { tabSize: 4 }); + + withTestCodeEditor( + null, + { + model: model, + wordWrap: 'wordWrapColumn', + wordWrapColumn: 8, + stickyTabStops: true, + }, + (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 9, 1, 9) + ]); + moveRight(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 10, 1, 10), + ]); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, [ + new Selection(1, 9, 1, 9), + ]); + } + ); + }); }); suite('Editor Controller - Cursor Configuration', () => { @@ -6103,4 +6283,62 @@ suite('Undo stops', () => { assert.strictEqual(model.getValue(), 'hello world\nhello world'); }); }); + + test('there is a single undo stop for consecutive whitespaces', () => { + let model = createTextModel( + [ + '' + ].join('\n'), + { + insertSpaces: false, + } + ); + + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('a', 'keyboard'); + viewModel.type('b', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type('c', 'keyboard'); + viewModel.type('d', 'keyboard'); + + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab cd', 'assert1'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab ', 'assert2'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab', 'assert3'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '', 'assert4'); + }); + }); + + test('there is no undo stop after a single whitespace', () => { + let model = createTextModel( + [ + '' + ].join('\n'), + { + insertSpaces: false, + } + ); + + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('a', 'keyboard'); + viewModel.type('b', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type('c', 'keyboard'); + viewModel.type('d', 'keyboard'); + + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab cd', 'assert1'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ab', 'assert3'); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '', 'assert4'); + }); + }); }); diff --git a/lib/vscode/src/vs/editor/test/browser/editorTestServices.ts b/lib/vscode/src/vs/editor/test/browser/editorTestServices.ts index 55ceb3734a21..f69b4a613161 100644 --- a/lib/vscode/src/vs/editor/test/browser/editorTestServices.ts +++ b/lib/vscode/src/vs/editor/test/browser/editorTestServices.ts @@ -19,9 +19,9 @@ export class TestCodeEditorService extends AbstractCodeEditorService { this.lastInput = input; return Promise.resolve(null); } - public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void { } + public registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void { } public removeDecorationType(key: string): void { } - public resolveDecorationOptions(decorationTypeKey: string, writable: boolean): IModelDecorationOptions { return {}; } + public resolveDecorationOptions(decorationTypeKey: string, writable: boolean): IModelDecorationOptions { return { description: 'test' }; } public resolveDecorationCSSRules(decorationTypeKey: string): CSSRuleList | null { return null; } } diff --git a/lib/vscode/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/lib/vscode/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 55be31487cea..e995b8de89c0 100644 --- a/lib/vscode/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/lib/vscode/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -59,12 +59,12 @@ suite('Decoration Render Options', () => { }; test('register and resolve decoration type', () => { let s = new TestCodeEditorServiceImpl(null, themeServiceMock); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); }); test('remove decoration type', () => { let s = new TestCodeEditorServiceImpl(null, themeServiceMock); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); s.removeDecorationType('example'); assert.throws(() => s.resolveDecorationOptions('example', false)); @@ -77,7 +77,7 @@ suite('Decoration Render Options', () => { test('css properties', () => { const styleSheet = new TestGlobalStyleSheet(); const s = new TestCodeEditorServiceImpl(styleSheet, themeServiceMock); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); const sheet = readStyleSheet(styleSheet); assert(sheet.indexOf(`{background:url('https://github.com/microsoft/vscode/blob/main/resources/linux/code.png') center center no-repeat;background-size:contain;}`) >= 0); assert(sheet.indexOf(`{background-color:red;border-color:yellow;box-sizing: border-box;}`) >= 0); @@ -94,7 +94,7 @@ suite('Decoration Render Options', () => { editorBackground: '#FF0000' })); const s = new TestCodeEditorServiceImpl(styleSheet, themeService); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); themeService.setTheme(new TestColorTheme({ @@ -127,7 +127,7 @@ suite('Decoration Render Options', () => { infoForeground: '#444444' })); const s = new TestCodeEditorServiceImpl(styleSheet, themeService); - s.registerDecorationType('example', options); + s.registerDecorationType('test', 'example', options); const expected = [ '.vs-dark.monaco-editor .ced-example-4::after, .hc-black.monaco-editor .ced-example-4::after {color:#444444 !important;}', '.vs-dark.monaco-editor .ced-example-1, .hc-black.monaco-editor .ced-example-1 {color:#000000 !important;}', @@ -145,7 +145,7 @@ suite('Decoration Render Options', () => { const s = new TestCodeEditorServiceImpl(styleSheet, themeServiceMock); // URI, only minimal encoding - s.registerDecorationType('example', { gutterIconPath: URI.parse('data:image/svg+xml;base64,PHN2ZyB4b+') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('data:image/svg+xml;base64,PHN2ZyB4b+') }); assert(readStyleSheet(styleSheet).indexOf(`{background:url('data:image/svg+xml;base64,PHN2ZyB4b+') center center no-repeat;}`) > 0); s.removeDecorationType('example'); @@ -159,27 +159,27 @@ suite('Decoration Render Options', () => { if (platform.isWindows) { // windows file path (used as string) - s.registerDecorationType('example', { gutterIconPath: URI.file('c:\\files\\miles\\more.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('c:\\files\\miles\\more.png') }); assertBackground('file:///c:/files/miles/more.png', 'vscode-file://vscode-app/c:/files/miles/more.png'); s.removeDecorationType('example'); // single quote must always be escaped/encoded - s.registerDecorationType('example', { gutterIconPath: URI.file('c:\\files\\foo\\b\'ar.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('c:\\files\\foo\\b\'ar.png') }); assertBackground('file:///c:/files/foo/b%27ar.png', 'vscode-file://vscode-app/c:/files/foo/b%27ar.png'); s.removeDecorationType('example'); } else { // unix file path (used as string) - s.registerDecorationType('example', { gutterIconPath: URI.file('/Users/foo/bar.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('/Users/foo/bar.png') }); assertBackground('file:///Users/foo/bar.png', 'vscode-file://vscode-app/Users/foo/bar.png'); s.removeDecorationType('example'); // single quote must always be escaped/encoded - s.registerDecorationType('example', { gutterIconPath: URI.file('/Users/foo/b\'ar.png') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('/Users/foo/b\'ar.png') }); assertBackground('file:///Users/foo/b%27ar.png', 'vscode-file://vscode-app/Users/foo/b%27ar.png'); s.removeDecorationType('example'); } - s.registerDecorationType('example', { gutterIconPath: URI.parse('http://test/pa\'th') }); + s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('http://test/pa\'th') }); assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa%27th') center center no-repeat;}`) > 0); s.removeDecorationType('example'); }); diff --git a/lib/vscode/src/vs/editor/test/browser/services/openerService.test.ts b/lib/vscode/src/vs/editor/test/browser/services/openerService.test.ts index 08c904f07649..fe11e4745096 100644 --- a/lib/vscode/src/vs/editor/test/browser/services/openerService.test.ts +++ b/lib/vscode/src/vs/editor/test/browser/services/openerService.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { matchesScheme } from 'vs/platform/opener/common/opener'; suite('OpenerService', function () { @@ -32,28 +33,28 @@ suite('OpenerService', function () { test('delegate to editorService, scheme:///fff', async function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('another:///somepath')); - assert.strictEqual(editorService.lastInput!.options!.selection, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection, undefined); }); test('delegate to editorService, scheme:///fff#L123', async function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('file:///somepath#L23')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 1); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 1); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); await openerService.open(URI.parse('another:///somepath#L23')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 1); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 1); await openerService.open(URI.parse('another:///somepath#L23,45')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 45); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 45); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); }); @@ -61,17 +62,17 @@ suite('OpenerService', function () { const openerService = new OpenerService(editorService, NullCommandService); await openerService.open(URI.parse('file:///somepath#23')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 1); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 1); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); await openerService.open(URI.parse('file:///somepath#23,45')); - assert.strictEqual(editorService.lastInput!.options!.selection!.startLineNumber, 23); - assert.strictEqual(editorService.lastInput!.options!.selection!.startColumn, 45); - assert.strictEqual(editorService.lastInput!.options!.selection!.endLineNumber, undefined); - assert.strictEqual(editorService.lastInput!.options!.selection!.endColumn, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startLineNumber, 23); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.startColumn, 45); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endLineNumber, undefined); + assert.strictEqual((editorService.lastInput!.options as ITextEditorOptions)!.selection!.endColumn, undefined); assert.strictEqual(editorService.lastInput!.resource.fragment, ''); }); @@ -245,4 +246,25 @@ suite('OpenerService', function () { assert.ok(!matchesScheme(URI.parse('htt://microsoft.com'), 'http')); assert.ok(!matchesScheme(URI.parse('z://microsoft.com'), 'http')); }); + + test('resolveExternalUri', async function () { + const openerService = new OpenerService(editorService, NullCommandService); + + try { + await openerService.resolveExternalUri(URI.parse('file:///Users/user/folder')); + assert.fail('Should not reach here'); + } catch { + // OK + } + + const disposable = openerService.registerExternalUriResolver({ + async resolveExternalUri(uri) { + return { resolved: uri, dispose() { } }; + } + }); + + const result = await openerService.resolveExternalUri(URI.parse('file:///Users/user/folder')); + assert.deepStrictEqual(result.resolved.toString(), 'file:///Users/user/folder'); + disposable.dispose(); + }); }); diff --git a/lib/vscode/src/vs/editor/test/common/model/modelDecorations.test.ts b/lib/vscode/src/vs/editor/test/common/model/modelDecorations.test.ts index da7b00939101..b38ee055688e 100644 --- a/lib/vscode/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/lib/vscode/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -45,6 +45,7 @@ function modelHasNoDecorations(model: TextModel) { function addDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string): string { return model.changeDecorations((changeAccessor) => { return changeAccessor.addDecoration(new Range(startLineNumber, startColumn, endLineNumber, endColumn), { + description: 'test', className: className }); })!; @@ -407,7 +408,7 @@ suite('Editor Model - Model Decorations', () => { }); test('removeAllDecorationsWithOwnerId works', () => { - thisModel.deltaDecorations([], [{ range: new Range(1, 2, 4, 1), options: { className: 'myType1' } }], 1); + thisModel.deltaDecorations([], [{ range: new Range(1, 2, 4, 1), options: { description: 'test', className: 'myType1' } }], 1); thisModel.removeAllDecorationsWithOwnerId(1); modelHasNoDecorations(thisModel); }); @@ -422,7 +423,7 @@ suite('Decorations and editing', () => { 'Third Line' ].join('\n')); - const id = model.deltaDecorations([], [{ range: decRange, options: { stickiness: stickiness } }])[0]; + const id = model.deltaDecorations([], [{ range: decRange, options: { description: 'test', stickiness: stickiness } }])[0]; model.applyEdits([{ range: editRange, text: editText, @@ -1123,6 +1124,7 @@ suite('deltaDecorations', () => { return { range: dec.range, options: { + description: 'test', className: dec.id } }; @@ -1276,6 +1278,7 @@ suite('deltaDecorations', () => { endColumn: 1 }, options: { + description: 'test', hoverMessage: { value: 'hello1' } } }]); @@ -1288,6 +1291,7 @@ suite('deltaDecorations', () => { endColumn: 1 }, options: { + description: 'test', hoverMessage: { value: 'hello2' } } }]); @@ -1312,9 +1316,11 @@ suite('deltaDecorations', () => { startColumn: 1, endLineNumber: 1, endColumn: 1 - }, { - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - } + }, + { + description: 'test', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + } ); }); model.changeDecorations((changeAccessor) => { @@ -1349,16 +1355,16 @@ suite('deltaDecorations', () => { ].join('\n')); model.deltaDecorations([], [ - { range: new Range(1, 1, 1, 1), options: { className: '1' } }, - { range: new Range(1, 13, 1, 13), options: { className: '2' } }, - { range: new Range(2, 1, 2, 1), options: { className: '3' } }, - { range: new Range(2, 1, 2, 4), options: { className: '4' } }, - { range: new Range(2, 8, 2, 13), options: { className: '5' } }, - { range: new Range(3, 1, 4, 6), options: { className: '6' } }, - { range: new Range(1, 1, 3, 6), options: { className: 'x1' } }, - { range: new Range(2, 5, 2, 8), options: { className: 'x2' } }, - { range: new Range(1, 1, 2, 8), options: { className: 'x3' } }, - { range: new Range(2, 5, 3, 1), options: { className: 'x4' } }, + { range: new Range(1, 1, 1, 1), options: { description: 'test', className: '1' } }, + { range: new Range(1, 13, 1, 13), options: { description: 'test', className: '2' } }, + { range: new Range(2, 1, 2, 1), options: { description: 'test', className: '3' } }, + { range: new Range(2, 1, 2, 4), options: { description: 'test', className: '4' } }, + { range: new Range(2, 8, 2, 13), options: { description: 'test', className: '5' } }, + { range: new Range(3, 1, 4, 6), options: { description: 'test', className: '6' } }, + { range: new Range(1, 1, 3, 6), options: { description: 'test', className: 'x1' } }, + { range: new Range(2, 5, 2, 8), options: { description: 'test', className: 'x2' } }, + { range: new Range(1, 1, 2, 8), options: { description: 'test', className: 'x3' } }, + { range: new Range(2, 5, 3, 1), options: { description: 'test', className: 'x4' } }, ]); let inRange = model.getDecorationsInRange(new Range(2, 6, 2, 6)); @@ -1376,7 +1382,7 @@ suite('deltaDecorations', () => { 'My First Line' ].join('\n')); - const id = model.deltaDecorations([], [{ range: new Range(1, 2, 1, 14), options: { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true } }])[0]; + const id = model.deltaDecorations([], [{ range: new Range(1, 2, 1, 14), options: { description: 'test', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true } }])[0]; model.applyEdits([{ range: new Range(1, 1, 1, 14), text: 'Some new text that is longer than the previous one', diff --git a/lib/vscode/src/vs/editor/test/common/modes/linkComputer.test.ts b/lib/vscode/src/vs/editor/test/common/modes/linkComputer.test.ts index 5bb34c911212..67e46bd8e629 100644 --- a/lib/vscode/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/lib/vscode/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -230,4 +230,25 @@ suite('Editor Modes - Link Computer', () => { ' http://tree-mark.chips.jp/ใƒฌใƒผใ‚บใƒณ๏ผ†ใƒ™ใƒชใƒผใƒŸใƒƒใ‚ฏใ‚น ' ); }); + + test('issue #121438: Link detection stops atใ€...ใ€‘', () => { + assertLink( + 'aa https://zh.wikipedia.org/wiki/ใ€ๆˆ‘ๆŽจ็š„ๅญฉๅญใ€‘ aa', + ' https://zh.wikipedia.org/wiki/ใ€ๆˆ‘ๆŽจ็š„ๅญฉๅญใ€‘ ' + ); + }); + + test('issue #121438: Link detection stops atใ€Š...ใ€‹', () => { + assertLink( + 'aa https://zh.wikipedia.org/wiki/ใ€Šๆ–ฐ้’ๅนดใ€‹็ผ–่พ‘้ƒจๆ—งๅ€ aa', + ' https://zh.wikipedia.org/wiki/ใ€Šๆ–ฐ้’ๅนดใ€‹็ผ–่พ‘้ƒจๆ—งๅ€ ' + ); + }); + + test('issue #121438: Link detection stops at โ€œ...โ€', () => { + assertLink( + 'aa https://zh.wikipedia.org/wiki/โ€œๅธธๅ‡ฏ็”ณโ€่ฏฏ่ฏ‘ไบ‹ไปถ aa', + ' https://zh.wikipedia.org/wiki/โ€œๅธธๅ‡ฏ็”ณโ€่ฏฏ่ฏ‘ไบ‹ไปถ ' + ); + }); }); diff --git a/lib/vscode/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/lib/vscode/src/vs/editor/test/common/modes/supports/onEnter.test.ts index 1af36e9ef898..ea8b81da269a 100644 --- a/lib/vscode/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/lib/vscode/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -47,6 +47,40 @@ suite('OnEnter', () => { testIndentAction('begin', '', IndentAction.Indent); }); + + test('Issue #121125: onEnterRules with global modifier', () => { + const support = new OnEnterSupport({ + onEnterRules: [ + { + action: { + appendText: '/// ', + indentAction: IndentAction.Outdent + }, + beforeText: /^\s*\/{3}.*$/gm + } + ] + }); + + let testIndentAction = (previousLineText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { + let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, previousLineText, beforeText, afterText); + if (expectedIndentAction === null) { + assert.strictEqual(actual, null, 'isNull:' + beforeText); + } else { + assert.strictEqual(actual !== null, true, 'isNotNull:' + beforeText); + assert.strictEqual(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); + if (expectedAppendText !== null) { + assert.strictEqual(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); + } + if (removeText !== 0) { + assert.strictEqual(actual!.removeText, removeText, 'removeText:' + beforeText); + } + } + }; + + testIndentAction('/// line', '/// line', '', IndentAction.Outdent, '/// '); + testIndentAction('/// line', '/// line', '', IndentAction.Outdent, '/// '); + }); + test('uses regExpRules', () => { let support = new OnEnterSupport({ onEnterRules: javascriptOnEnterRules diff --git a/lib/vscode/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/lib/vscode/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 9f94af260fe9..ed5e03b5bd8d 100644 --- a/lib/vscode/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/lib/vscode/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -809,63 +809,63 @@ suite('viewLineRenderer.renderLine', () => { const _expected = decodeCharacterMapping(expected); assert.deepStrictEqual(_actual, _expected); } +}); - function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { - - assertCharPartOffsets(actual, expectedCharPartOffsets); +function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { - let expectedCharAbsoluteOffset: number[] = [], currentPartAbsoluteOffset = 0; - for (let partIndex = 0; partIndex < expectedCharPartOffsets.length; partIndex++) { - const part = expectedCharPartOffsets[partIndex]; + assertCharPartOffsets(actual, expectedCharPartOffsets); - for (const charIndex of part) { - expectedCharAbsoluteOffset.push(currentPartAbsoluteOffset + charIndex); - } + let expectedCharAbsoluteOffset: number[] = [], currentPartAbsoluteOffset = 0; + for (let partIndex = 0; partIndex < expectedCharPartOffsets.length; partIndex++) { + const part = expectedCharPartOffsets[partIndex]; - currentPartAbsoluteOffset += expectedPartLengths[partIndex]; + for (const charIndex of part) { + expectedCharAbsoluteOffset.push(currentPartAbsoluteOffset + charIndex); } - let actualCharOffset: number[] = []; - let tmp = actual.getAbsoluteOffsets(); - for (let i = 0; i < tmp.length; i++) { - actualCharOffset[i] = tmp[i]; - } - assert.deepStrictEqual(actualCharOffset, expectedCharAbsoluteOffset); + currentPartAbsoluteOffset += expectedPartLengths[partIndex]; } - function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void { - - let charOffset = 0; - for (let partIndex = 0; partIndex < expected.length; partIndex++) { - let part = expected[partIndex]; - for (const charIndex of part) { - // here - let _actualPartData = actual.charOffsetToPartData(charOffset); - let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); - let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); - - assert.deepStrictEqual( - { partIndex: actualPartIndex, charIndex: actualCharIndex }, - { partIndex: partIndex, charIndex: charIndex }, - `character mapping for offset ${charOffset}` - ); - - // here - let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); - - assert.strictEqual( - actualOffset, - charOffset, - `character mapping for part ${partIndex}, ${charIndex}` - ); - - charOffset++; - } - } + let actualCharOffset: number[] = []; + let tmp = actual.getAbsoluteOffsets(); + for (let i = 0; i < tmp.length; i++) { + actualCharOffset[i] = tmp[i]; + } + assert.deepStrictEqual(actualCharOffset, expectedCharAbsoluteOffset); +} - assert.strictEqual(actual.length, charOffset); +function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void { + + let charOffset = 0; + for (let partIndex = 0; partIndex < expected.length; partIndex++) { + let part = expected[partIndex]; + for (const charIndex of part) { + // here + let _actualPartData = actual.charOffsetToPartData(charOffset); + let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); + let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); + + assert.deepStrictEqual( + { partIndex: actualPartIndex, charIndex: actualCharIndex }, + { partIndex: partIndex, charIndex: charIndex }, + `character mapping for offset ${charOffset}` + ); + + // here + let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); + + assert.strictEqual( + actualOffset, + charOffset, + `character mapping for part ${partIndex}, ${charIndex}` + ); + + charOffset++; + } } -}); + + assert.strictEqual(actual.length, charOffset); +} suite('viewLineRenderer.renderLine 2', () => { @@ -1739,7 +1739,7 @@ suite('viewLineRenderer.renderLine 2', () => { let expected = [ '', '\u00a0\u00a0\u00a0\u00a0}', - '', + '', '' ].join(''); @@ -2138,6 +2138,52 @@ suite('viewLineRenderer.renderLine 2', () => { assert.deepStrictEqual(actual.html, expected); }); + test('issue #124038: Multiple end-of-line text decorations get merged', () => { + const actual = renderViewLine(new RenderLineInput( + true, + false, + ' if', + false, + true, + false, + 0, + createViewLineTokens([createPart(4, 1), createPart(6, 2)]), + [ + new LineDecoration(7, 7, 'ced-1-TextEditorDecorationType2-17c14d98-3 ced-1-TextEditorDecorationType2-3', InlineDecorationType.Before), + new LineDecoration(7, 7, 'ced-1-TextEditorDecorationType2-17c14d98-4 ced-1-TextEditorDecorationType2-4', InlineDecorationType.After), + new LineDecoration(7, 7, 'ced-ghost-text-1-4', InlineDecorationType.After), + ], + 4, + 0, + 10, + 10, + 10, + 10000, + 'all', + false, + false, + null + )); + + const expected = [ + '', + 'ยทยทยทยทif', + '' + ].join(''); + + assert.deepStrictEqual(actual.html, expected); + assertCharacterMapping(actual.characterMapping, + [ + [0, 1, 2, 3], + [0, 1], + [], + [0], + [], + ], + [4, 2, 0, 0] + ); + }); + function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void { let renderLineOutput = renderViewLine(new RenderLineInput( diff --git a/lib/vscode/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/lib/vscode/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index 9b2862773433..f0b0996b5b46 100644 --- a/lib/vscode/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/lib/vscode/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -28,6 +28,7 @@ suite('ViewModelDecorations', () => { model.changeDecorations((accessor) => { let createOpts = (id: string) => { return { + description: 'test', className: id, inlineClassName: 'i-' + id, beforeContentClassName: 'b-' + id, @@ -165,6 +166,7 @@ suite('ViewModelDecorations', () => { accessor.addDecoration( new Range(1, 50, 1, 51), { + description: 'test', beforeContentClassName: 'dec1' } ); @@ -199,6 +201,7 @@ suite('ViewModelDecorations', () => { accessor.addDecoration( new Range(1, 1, 1, 1), { + description: 'test', beforeContentClassName: 'before1', afterContentClassName: 'after1' } diff --git a/lib/vscode/src/vs/ipc.d.ts b/lib/vscode/src/vs/ipc.d.ts deleted file mode 120000 index dff4f400f736..000000000000 --- a/lib/vscode/src/vs/ipc.d.ts +++ /dev/null @@ -1 +0,0 @@ -../../../../typings/ipc.d.ts \ No newline at end of file diff --git a/lib/vscode/src/vs/monaco.d.ts b/lib/vscode/src/vs/monaco.d.ts index a7b09f897c19..0958d5b1d685 100644 --- a/lib/vscode/src/vs/monaco.d.ts +++ b/lib/vscode/src/vs/monaco.d.ts @@ -1983,6 +1983,11 @@ declare namespace monaco.editor { * @event */ onDidChangeLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; + /** + * An event emitted when the model has been attached to the first editor or detached from the last editor. + * @event + */ + onDidChangeAttached(listener: () => void): IDisposable; /** * An event emitted right before disposing the model. * @event @@ -1993,6 +1998,10 @@ declare namespace monaco.editor { * and make all necessary clean-up to release this object to the GC. */ dispose(): void; + /** + * Returns if this model is attached to an editor or not. + */ + isAttachedToEditor(): boolean; } /** @@ -2964,8 +2973,9 @@ declare namespace monaco.editor { * Suggest options. */ suggest?: ISuggestOptions; + inlineSuggest?: IInlineSuggestOptions; /** - * Smart select opptions; + * Smart select options. */ smartSelect?: ISmartSelectOptions; /** @@ -3219,7 +3229,11 @@ declare namespace monaco.editor { /** * Control the behavior and rendering of the inline hints. */ - inlineHints?: IEditorInlineHintsOptions; + inlayHints?: IEditorInlayHintsOptions; + /** + * Control if the editor should use shadow DOM. + */ + useShadowDOM?: boolean; } /** @@ -3580,9 +3594,9 @@ declare namespace monaco.editor { export type EditorLightbulbOptions = Readonly>; /** - * Configuration options for editor inlineHints + * Configuration options for editor inlayHints */ - export interface IEditorInlineHintsOptions { + export interface IEditorInlayHintsOptions { /** * Enable the inline hints. * Defaults to true. @@ -3600,7 +3614,7 @@ declare namespace monaco.editor { fontFamily?: string; } - export type EditorInlineHintsOptions = Readonly>; + export type EditorInlayHintsOptions = Readonly>; /** * Configuration options for editor minimap @@ -3799,6 +3813,15 @@ declare namespace monaco.editor { readonly scrollByPage: boolean; } + export interface IInlineSuggestOptions { + /** + * Enable or disable the rendering of automatic inline completions. + */ + enabled?: boolean; + } + + export type InternalInlineSuggestOptions = Readonly>; + /** * Configuration options for editor suggest widget */ @@ -3831,6 +3854,10 @@ declare namespace monaco.editor { * Enable or disable the suggest status bar. */ showStatusBar?: boolean; + /** + * Enable or disable the rendering of the suggestion preview. + */ + preview?: boolean; /** * Show details inline with the label. Defaults to true. */ @@ -3847,6 +3874,10 @@ declare namespace monaco.editor { * Show constructor-suggestions. */ showConstructors?: boolean; + /** + * Show deprecated-suggestions. + */ + showDeprecated?: boolean; /** * Show field-suggestions. */ @@ -4035,82 +4066,84 @@ declare namespace monaco.editor { highlightActiveIndentGuide = 49, hover = 50, inDiffEditor = 51, - letterSpacing = 52, - lightbulb = 53, - lineDecorationsWidth = 54, - lineHeight = 55, - lineNumbers = 56, - lineNumbersMinChars = 57, - linkedEditing = 58, - links = 59, - matchBrackets = 60, - minimap = 61, - mouseStyle = 62, - mouseWheelScrollSensitivity = 63, - mouseWheelZoom = 64, - multiCursorMergeOverlapping = 65, - multiCursorModifier = 66, - multiCursorPaste = 67, - occurrencesHighlight = 68, - overviewRulerBorder = 69, - overviewRulerLanes = 70, - padding = 71, - parameterHints = 72, - peekWidgetDefaultFocus = 73, - definitionLinkOpensInPeek = 74, - quickSuggestions = 75, - quickSuggestionsDelay = 76, - readOnly = 77, - renameOnType = 78, - renderControlCharacters = 79, - renderIndentGuides = 80, - renderFinalNewline = 81, - renderLineHighlight = 82, - renderLineHighlightOnlyWhenFocus = 83, - renderValidationDecorations = 84, - renderWhitespace = 85, - revealHorizontalRightPadding = 86, - roundedSelection = 87, - rulers = 88, - scrollbar = 89, - scrollBeyondLastColumn = 90, - scrollBeyondLastLine = 91, - scrollPredominantAxis = 92, - selectionClipboard = 93, - selectionHighlight = 94, - selectOnLineNumbers = 95, - showFoldingControls = 96, - showUnused = 97, - snippetSuggestions = 98, - smartSelect = 99, - smoothScrolling = 100, - stickyTabStops = 101, - stopRenderingLineAfter = 102, - suggest = 103, - suggestFontSize = 104, - suggestLineHeight = 105, - suggestOnTriggerCharacters = 106, - suggestSelection = 107, - tabCompletion = 108, - tabIndex = 109, - unusualLineTerminators = 110, - useTabStops = 111, - wordSeparators = 112, - wordWrap = 113, - wordWrapBreakAfterCharacters = 114, - wordWrapBreakBeforeCharacters = 115, - wordWrapColumn = 116, - wordWrapOverride1 = 117, - wordWrapOverride2 = 118, - wrappingIndent = 119, - wrappingStrategy = 120, - showDeprecated = 121, - inlineHints = 122, - editorClassName = 123, - pixelRatio = 124, - tabFocusMode = 125, - layoutInfo = 126, - wrappingInfo = 127 + inlineSuggest = 52, + letterSpacing = 53, + lightbulb = 54, + lineDecorationsWidth = 55, + lineHeight = 56, + lineNumbers = 57, + lineNumbersMinChars = 58, + linkedEditing = 59, + links = 60, + matchBrackets = 61, + minimap = 62, + mouseStyle = 63, + mouseWheelScrollSensitivity = 64, + mouseWheelZoom = 65, + multiCursorMergeOverlapping = 66, + multiCursorModifier = 67, + multiCursorPaste = 68, + occurrencesHighlight = 69, + overviewRulerBorder = 70, + overviewRulerLanes = 71, + padding = 72, + parameterHints = 73, + peekWidgetDefaultFocus = 74, + definitionLinkOpensInPeek = 75, + quickSuggestions = 76, + quickSuggestionsDelay = 77, + readOnly = 78, + renameOnType = 79, + renderControlCharacters = 80, + renderIndentGuides = 81, + renderFinalNewline = 82, + renderLineHighlight = 83, + renderLineHighlightOnlyWhenFocus = 84, + renderValidationDecorations = 85, + renderWhitespace = 86, + revealHorizontalRightPadding = 87, + roundedSelection = 88, + rulers = 89, + scrollbar = 90, + scrollBeyondLastColumn = 91, + scrollBeyondLastLine = 92, + scrollPredominantAxis = 93, + selectionClipboard = 94, + selectionHighlight = 95, + selectOnLineNumbers = 96, + showFoldingControls = 97, + showUnused = 98, + snippetSuggestions = 99, + smartSelect = 100, + smoothScrolling = 101, + stickyTabStops = 102, + stopRenderingLineAfter = 103, + suggest = 104, + suggestFontSize = 105, + suggestLineHeight = 106, + suggestOnTriggerCharacters = 107, + suggestSelection = 108, + tabCompletion = 109, + tabIndex = 110, + unusualLineTerminators = 111, + useShadowDOM = 112, + useTabStops = 113, + wordSeparators = 114, + wordWrap = 115, + wordWrapBreakAfterCharacters = 116, + wordWrapBreakBeforeCharacters = 117, + wordWrapColumn = 118, + wordWrapOverride1 = 119, + wordWrapOverride2 = 120, + wrappingIndent = 121, + wrappingStrategy = 122, + showDeprecated = 123, + inlayHints = 124, + editorClassName = 125, + pixelRatio = 126, + tabFocusMode = 127, + layoutInfo = 128, + wrappingInfo = 129 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4213,12 +4246,13 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; - inlineHints: IEditorOption; + inlayHints: IEditorOption; snippetSuggestions: IEditorOption; smartSelect: IEditorOption; smoothScrolling: IEditorOption; stopRenderingLineAfter: IEditorOption; suggest: IEditorOption; + inlineSuggest: IEditorOption; suggestFontSize: IEditorOption; suggestLineHeight: IEditorOption; suggestOnTriggerCharacters: IEditorOption; @@ -4226,6 +4260,7 @@ declare namespace monaco.editor { tabCompletion: IEditorOption; tabIndex: IEditorOption; unusualLineTerminators: IEditorOption; + useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordSeparators: IEditorOption; wordWrap: IEditorOption; @@ -4771,7 +4806,7 @@ declare namespace monaco.editor { getRawOptions(): IEditorOptions; /** * Get value of the current model attached to this editor. - * @see `ITextModel.getValue` + * @see {@link ITextModel.getValue} */ getValue(options?: { preserveBOM: boolean; @@ -4779,7 +4814,7 @@ declare namespace monaco.editor { }): string; /** * Set the value of the current model attached to this editor. - * @see `ITextModel.setValue` + * @see {@link ITextModel.setValue} */ setValue(newValue: string): void; /** @@ -4861,7 +4896,7 @@ declare namespace monaco.editor { getLineDecorations(lineNumber: number): IModelDecoration[] | null; /** * All decorations added through this call will get the ownerId of this editor. - * @see `ITextModel.deltaDecorations` + * @see {@link ITextModel.deltaDecorations} */ deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; /** @@ -4966,7 +5001,7 @@ declare namespace monaco.editor { */ export interface IDiffEditor extends IEditor { /** - * @see ICodeEditor.getDomNode + * @see {@link ICodeEditor.getDomNode} */ getDomNode(): HTMLElement; /** @@ -5298,6 +5333,11 @@ declare namespace monaco.languages { */ export function registerDocumentRangeSemanticTokensProvider(languageId: string, provider: DocumentRangeSemanticTokensProvider): IDisposable; + /** + * Register an inline completions provider. + */ + export function registerInlineCompletionsProvider(languageId: string, provider: InlineCompletionsProvider): IDisposable; + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -5553,7 +5593,7 @@ declare namespace monaco.languages { } /** - * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * A provider result represents the values a provider, like the {@link HoverProvider}, * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. @@ -5688,13 +5728,13 @@ declare namespace monaco.languages { documentation?: string | IMarkdownString; /** * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. */ sortText?: string; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. */ filterText?: string; @@ -5718,11 +5758,11 @@ declare namespace monaco.languages { /** * A range of text that should be replaced by this completion item. * - * Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * Defaults to a range from the start of the {@link TextDocument.getWordRangeAtPosition current word} to the * current position. * - * *Note:* The range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note:* The range must be a {@link Range.isSingleLine single line} and it must + * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ range: IRange | { insert: IRange; @@ -5763,7 +5803,7 @@ declare namespace monaco.languages { /** * Contains additional information about the context in which - * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered. */ export interface CompletionContext { /** @@ -5784,10 +5824,10 @@ declare namespace monaco.languages { * * When computing *complete* completion items is expensive, providers can optionally implement * the `resolveCompletionItem`-function. In that case it is enough to return completion - * items with a [label](#CompletionItem.label) from the - * [provideCompletionItems](#CompletionItemProvider.provideCompletionItems)-function. Subsequently, + * items with a {@link CompletionItem.label label} from the + * {@link CompletionItemProvider.provideCompletionItems provideCompletionItems}-function. Subsequently, * when a completion item is shown in the UI and gains focus this provider is asked to resolve - * the item, like adding [doc-comment](#CompletionItem.documentation) or [details](#CompletionItem.detail). + * the item, like adding {@link CompletionItem.documentation doc-comment} or {@link CompletionItem.detail details}. */ export interface CompletionItemProvider { triggerCharacters?: string[]; @@ -5796,14 +5836,68 @@ declare namespace monaco.languages { */ provideCompletionItems(model: editor.ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult; /** - * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) - * or [details](#CompletionItem.detail). + * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment} + * or {@link CompletionItem.detail details}. * * The editor will only resolve a completion item once. */ resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; } + /** + * How an {@link InlineCompletionsProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1 + } + + export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + } + + export interface InlineCompletion { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + readonly text: string; + /** + * The range to replace. + * Must begin and end on the same line. + */ + readonly range?: IRange; + readonly command?: Command; + } + + export interface InlineCompletions { + readonly items: readonly TItem[]; + } + + export interface InlineCompletionsProvider { + provideInlineCompletions(model: editor.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** + * Will be called when an item is shown. + */ + handleItemDidShow?(completions: T, item: T['items'][number]): void; + /** + * Will be called when a completions list is no longer in use and can be garbage-collected. + */ + freeInlineCompletions(completions: T): void; + } + export interface CodeAction { title: string; command?: Command; @@ -5942,7 +6036,7 @@ declare namespace monaco.languages { */ range: IRange; /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ kind?: DocumentHighlightKind; } @@ -6269,12 +6363,12 @@ declare namespace monaco.languages { */ label: string; /** - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this presentation for the color. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when + * An optional array of additional {@link TextEdit text edits} that are applied when * selecting this color presentation. */ additionalTextEdits?: TextEdit[]; @@ -6346,10 +6440,10 @@ declare namespace monaco.languages { */ end: number; /** - * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or - * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + * {@link FoldingRangeKind} for an enumeration of standardized kinds. */ kind?: FoldingRangeKind; } @@ -6370,7 +6464,7 @@ declare namespace monaco.languages { */ static readonly Region: FoldingRangeKind; /** - * Creates a new [FoldingRangeKind](#FoldingRangeKind). + * Creates a new {@link FoldingRangeKind}. * * @param value of the kind. */ @@ -6450,24 +6544,23 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } - export enum InlineHintKind { + export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2 } - export interface InlineHint { + export interface InlayHint { text: string; - range: IRange; - kind: InlineHintKind; - description?: string | IMarkdownString; + position: IPosition; + kind: InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; } - export interface InlineHintsProvider { - onDidChangeInlineHints?: IEvent | undefined; - provideInlineHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; + export interface InlayHintsProvider { + onDidChangeInlayHints?: IEvent | undefined; + provideInlayHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } export interface SemanticTokensLegend { diff --git a/lib/vscode/src/vs/base/browser/ui/dropdown/dropdownWithPrimaryActionViewItem.ts b/lib/vscode/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts similarity index 64% rename from lib/vscode/src/vs/base/browser/ui/dropdown/dropdownWithPrimaryActionViewItem.ts rename to lib/vscode/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index eef52a5a0eda..3dc33976b985 100644 --- a/lib/vscode/src/vs/base/browser/ui/dropdown/dropdownWithPrimaryActionViewItem.ts +++ b/lib/vscode/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -4,37 +4,49 @@ *--------------------------------------------------------------------------------------------*/ import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { IAction } from 'vs/base/common/actions'; -import * as DOM from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { private _primaryAction: ActionViewItem; private _dropdown: DropdownMenuActionViewItem; private _container: HTMLElement | null = null; - private toDispose: IDisposable[]; + private _dropdownContainer: HTMLElement | null = null; + + get onDidChangeDropdownVisibility(): Event { + return this._dropdown.onDidChangeVisibility; + } constructor( - primaryAction: IAction, + primaryAction: MenuItemAction, dropdownAction: IAction, dropdownMenuActions: IAction[], - _className: string, + className: string, private readonly _contextMenuProvider: IContextMenuProvider, - dropdownIcon?: string + _keybindingService: IKeybindingService, + _notificationService: INotificationService ) { super(null, primaryAction); - this._primaryAction = new ActionViewItem(undefined, primaryAction, { - icon: true, - label: false - }); + this._primaryAction = new MenuEntryActionViewItem(primaryAction, _keybindingService, _notificationService); this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { - menuAsChild: true + menuAsChild: true, + classNames: ['codicon', 'codicon-chevron-down'] }); - this.toDispose = []; + } + + override setActionContext(newContext: unknown): void { + super.setActionContext(newContext); + this._primaryAction.setActionContext(newContext); + this._dropdown.setActionContext(newContext); } override render(container: HTMLElement): void { @@ -43,10 +55,9 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { this._container.classList.add('monaco-dropdown-with-primary'); const primaryContainer = DOM.$('.action-container'); this._primaryAction.render(DOM.append(this._container, primaryContainer)); - const dropdownContainer = DOM.$('.dropdown-action-container'); - this._dropdown.render(DOM.append(this._container, dropdownContainer)); - - this.toDispose.push(DOM.addDisposableListener(primaryContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + this._dropdownContainer = DOM.$('.dropdown-action-container'); + this._dropdown.render(DOM.append(this._container, this._dropdownContainer)); + this._register(DOM.addDisposableListener(primaryContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.RightArrow)) { this._primaryAction.element!.tabIndex = -1; @@ -54,7 +65,7 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { event.stopPropagation(); } })); - this.toDispose.push(DOM.addDisposableListener(dropdownContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + this._register(DOM.addDisposableListener(this._dropdownContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.LeftArrow)) { this._primaryAction.element!.tabIndex = 0; @@ -89,18 +100,20 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { } } - override dispose(): void { - this.toDispose = dispose(this.toDispose); - } - update(dropdownAction: IAction, dropdownMenuActions: IAction[], dropdownIcon?: string): void { - this._dropdown?.dispose(); + this._dropdown.dispose(); this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { menuAsChild: true, classNames: ['codicon', dropdownIcon || 'codicon-chevron-down'] }); - if (this.element) { - this._dropdown.render(this.element); + if (this._dropdownContainer) { + this._dropdown.render(this._dropdownContainer); } } + + override dispose() { + this._primaryAction.dispose(); + this._dropdown.dispose(); + super.dispose(); + } } diff --git a/lib/vscode/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/lib/vscode/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 169aece680c7..581b30f2879b 100644 --- a/lib/vscode/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/lib/vscode/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -24,14 +24,16 @@ export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuAct const groups = menu.getActions(options); const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey); - fillInActions(groups, target, useAlternativeActions, primaryGroup); + fillInActions(groups, target, useAlternativeActions, primaryGroup ? actionGroup => actionGroup === primaryGroup : actionGroup => actionGroup === 'navigation'); return asDisposable(groups); } -export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string, primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean): IDisposable { +export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string | ((actionGroup: string) => boolean), primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean): IDisposable { const groups = menu.getActions(options); + const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup; + // Action bars handle alternative actions on their own so the alternative actions should be ignored - fillInActions(groups, target, false, primaryGroup, primaryMaxCount, shouldInlineSubmenu); + fillInActions(groups, target, false, isPrimaryAction, primaryMaxCount, shouldInlineSubmenu, useSeparatorsInPrimaryActions); return asDisposable(groups); } @@ -49,9 +51,10 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, - primaryGroup = 'navigation', + isPrimaryAction: (actionGroup: string) => boolean = actionGroup => actionGroup === 'navigation', primaryMaxCount: number = Number.MAX_SAFE_INTEGER, - shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false + shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false, + useSeparatorsInPrimaryActions: boolean = false ): void { let primaryBucket: IAction[]; @@ -69,8 +72,11 @@ function fillInActions( for (const [group, actions] of groups) { let target: IAction[]; - if (group === primaryGroup) { + if (isPrimaryAction(group)) { target = primaryBucket; + if (target.length > 0 && useSeparatorsInPrimaryActions) { + target.push(new Separator()); + } } else { target = secondaryBucket; if (target.length > 0) { @@ -93,7 +99,7 @@ function fillInActions( // ask the outside if submenu should be inlined or not. only ask when // there would be enough space for (const { group, action, index } of submenuInfo) { - const target = group === primaryGroup ? primaryBucket : secondaryBucket; + const target = isPrimaryAction(group) ? primaryBucket : secondaryBucket; // inlining submenus with length 0 or 1 is easy, // larger submenus need to be checked with the overall limit @@ -133,13 +139,15 @@ export class MenuEntryActionViewItem extends ActionViewItem { return this._wantsAltCommand && this._menuItemAction.alt || this._menuItemAction; } - override onClick(event: MouseEvent): void { + override async onClick(event: MouseEvent): Promise { event.preventDefault(); event.stopPropagation(); - this.actionRunner - .run(this._commandAction, this._context) - .catch(err => this._notificationService.error(err)); + try { + await this.actionRunner.run(this._commandAction, this._context); + } catch (err) { + this._notificationService.error(err); + } } override render(container: HTMLElement): void { diff --git a/lib/vscode/src/vs/platform/actions/common/actions.ts b/lib/vscode/src/vs/platform/actions/common/actions.ts index 5f946cf2ea09..f0e548fea19b 100644 --- a/lib/vscode/src/vs/platform/actions/common/actions.ts +++ b/lib/vscode/src/vs/platform/actions/common/actions.ts @@ -41,6 +41,7 @@ export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; export interface ICommandAction { id: string; title: string | ICommandActionTitle; + shortTitle?: string | ICommandActionTitle; category?: string | ILocalizedString; tooltip?: string; icon?: Icon; @@ -87,6 +88,7 @@ export class MenuId { static readonly DebugWatchContext = new MenuId('DebugWatchContext'); static readonly DebugToolBar = new MenuId('DebugToolBar'); static readonly EditorContext = new MenuId('EditorContext'); + static readonly SimpleEditorContext = new MenuId('SimpleEditorContext'); static readonly EditorContextCopy = new MenuId('EditorContextCopy'); static readonly EditorContextPeek = new MenuId('EditorContextPeek'); static readonly EditorTitle = new MenuId('EditorTitle'); @@ -96,6 +98,7 @@ export class MenuId { static readonly ExplorerContext = new MenuId('ExplorerContext'); static readonly ExtensionContext = new MenuId('ExtensionContext'); static readonly GlobalActivity = new MenuId('GlobalActivity'); + static readonly MenubarMainMenu = new MenuId('MenubarMainMenu'); static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu'); static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu'); static readonly MenubarEditMenu = new MenuId('MenubarEditMenu'); @@ -125,9 +128,12 @@ export class MenuId { static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu'); static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu'); static readonly TestItem = new MenuId('TestItem'); + static readonly TestPeekElement = new MenuId('TestPeekElement'); + static readonly TestPeekTitle = new MenuId('TestPeekTitle'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TunnelContext = new MenuId('TunnelContext'); + static readonly TunnelProtocol = new MenuId('TunnelProtocol'); static readonly TunnelPortInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline'); @@ -142,6 +148,7 @@ export class MenuId { static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); + static readonly NotebookRightToolbar = new MenuId('NotebookRightToolbar'); static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); @@ -150,6 +157,7 @@ export class MenuId { static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle'); static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); + static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly TimelineItemContext = new MenuId('TimelineItemContext'); @@ -157,11 +165,13 @@ export class MenuId { static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly AccountsContext = new MenuId('AccountsContext'); static readonly PanelTitle = new MenuId('PanelTitle'); - static readonly TerminalContainerContext = new MenuId('TerminalContainerContext'); - static readonly TerminalToolbarContext = new MenuId('TerminalToolbarContext'); - static readonly TerminalTabsWidgetContext = new MenuId('TerminalTabsWidgetContext'); - static readonly TerminalTabsWidgetEmptyContext = new MenuId('TerminalTabsWidgetEmptyContext'); - static readonly TerminalSingleTabContext = new MenuId('TerminalSingleTabContext'); + static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext'); + static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext'); + static readonly TerminalTabContext = new MenuId('TerminalTabContext'); + static readonly TerminalTabEmptyAreaContext = new MenuId('TerminalTabEmptyAreaContext'); + static readonly TerminalInlineTabContext = new MenuId('TerminalInlineTabContext'); + static readonly WebviewContext = new MenuId('WebviewContext'); + static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); readonly id: number; readonly _debugName: string; @@ -175,6 +185,7 @@ export class MenuId { export interface IMenuActionOptions { arg?: any; shouldForwardArgs?: boolean; + renderShortTitle?: boolean; } export interface IMenu extends IDisposable { @@ -326,7 +337,7 @@ export class ExecuteCommandAction extends Action { super(id, label); } - override run(...args: any[]): Promise { + override run(...args: any[]): Promise { return this._commandService.executeCommand(this.id, ...args); } } @@ -384,7 +395,9 @@ export class MenuItemAction implements IAction { @ICommandService private _commandService: ICommandService ) { this.id = item.id; - this.label = typeof item.title === 'string' ? item.title : item.title.value; + this.label = options?.renderShortTitle && item.shortTitle + ? (typeof item.shortTitle === 'string' ? item.shortTitle : item.shortTitle.value) + : (typeof item.title === 'string' ? item.title : item.title.value); this.tooltip = item.tooltip ?? ''; this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); this.checked = false; @@ -417,7 +430,7 @@ export class MenuItemAction implements IAction { // to bridge into the rendering world. } - run(...args: any[]): Promise { + run(...args: any[]): Promise { let runArgs: any[] = []; if (this._options?.arg) { @@ -525,7 +538,7 @@ export interface IAction2Options extends ICommandAction { export abstract class Action2 { constructor(readonly desc: Readonly) { } - abstract run(accessor: ServicesAccessor, ...args: any[]): any; + abstract run(accessor: ServicesAccessor, ...args: any[]): void; } export function registerAction2(ctor: { new(): Action2 }): IDisposable { diff --git a/lib/vscode/src/vs/platform/backup/electron-main/backupMainService.ts b/lib/vscode/src/vs/platform/backup/electron-main/backupMainService.ts index ac5bb2489278..ac9d7f2a86da 100644 --- a/lib/vscode/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/lib/vscode/src/vs/platform/backup/electron-main/backupMainService.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import { createHash } from 'crypto'; import { join } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; -import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode, Promises } from 'vs/base/node/pfs'; import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -49,7 +49,7 @@ export class BackupMainService implements IBackupMainService { async initialize(): Promise { let backups: IBackupWorkspacesFormat; try { - backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here + backups = JSON.parse(await Promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here } catch (error) { backups = Object.create(null); } @@ -328,7 +328,7 @@ export class BackupMainService implements IBackupMainService { // Rename backupPath to new empty window backup path const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { - await fs.promises.rename(backupPath, newEmptyWindowBackupPath); + await Promises.rename(backupPath, newEmptyWindowBackupPath); } catch (error) { this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; diff --git a/lib/vscode/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/lib/vscode/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 9968de89189f..a2101b983e98 100644 --- a/lib/vscode/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/lib/vscode/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -107,7 +107,7 @@ flakySuite('BackupMainService', () => { environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product }); - await fs.promises.mkdir(backupHome, { recursive: true }); + await pfs.Promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); service = new class TestBackupMainService extends BackupMainService { @@ -446,7 +446,7 @@ flakySuite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -462,7 +462,7 @@ flakySuite('BackupMainService', () => { }; await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -484,7 +484,7 @@ flakySuite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { @@ -500,7 +500,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(fooFile); service.registerFolderBackupSync(barFile); assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); @@ -515,7 +515,7 @@ flakySuite('BackupMainService', () => { assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); @@ -528,7 +528,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); @@ -538,7 +538,7 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); @@ -549,12 +549,12 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(barFile); service.unregisterFolderBackupSync(fooFile); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); @@ -566,12 +566,12 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); service.unregisterWorkspaceBackupSync(ws1.workspace); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); @@ -581,12 +581,12 @@ flakySuite('BackupMainService', () => { service.registerEmptyWindowBackupSync('bar'); service.unregisterEmptyWindowBackupSync('foo'); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); @@ -600,7 +600,7 @@ flakySuite('BackupMainService', () => { await service.initialize(); service.unregisterFolderBackupSync(barFile); service.unregisterEmptyWindowBackupSync('test'); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -670,8 +670,8 @@ flakySuite('BackupMainService', () => { assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); - await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); + await pfs.Promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await pfs.Promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } diff --git a/lib/vscode/src/vs/platform/contextkey/common/contextkey.ts b/lib/vscode/src/vs/platform/contextkey/common/contextkey.ts index 749573dc451d..66e4c24e8674 100644 --- a/lib/vscode/src/vs/platform/contextkey/common/contextkey.ts +++ b/lib/vscode/src/vs/platform/contextkey/common/contextkey.ts @@ -21,7 +21,6 @@ STATIC_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0); STATIC_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0); STATIC_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0); STATIC_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0); -STATIC_VALUES.set('isIPad', _userAgent.indexOf('iPad') >= 0); const hasOwnProperty = Object.prototype.hasOwnProperty; diff --git a/lib/vscode/src/vs/platform/contextkey/common/contextkeys.ts b/lib/vscode/src/vs/platform/contextkey/common/contextkeys.ts index d98aa5ee62c3..4b40ea1d517f 100644 --- a/lib/vscode/src/vs/platform/contextkey/common/contextkeys.ts +++ b/lib/vscode/src/vs/platform/contextkey/common/contextkeys.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; +import { isMacintosh, isLinux, isWindows, isWeb, isIOS } from 'vs/base/common/platform'; export const IsMacContext = new RawContextKey('isMac', isMacintosh, localize('isMac', "Whether the operating system is macOS")); export const IsLinuxContext = new RawContextKey('isLinux', isLinux, localize('isLinux', "Whether the operating system is Linux")); @@ -13,6 +13,7 @@ export const IsWindowsContext = new RawContextKey('isWindows', isWindow export const IsWebContext = new RawContextKey('isWeb', isWeb, localize('isWeb', "Whether the platform is a web browser")); export const IsMacNativeContext = new RawContextKey('isMacNative', isMacintosh && !isWeb, localize('isMacNative', "Whether the operating system is macOS on a non-browser platform")); +export const IsIOSContext = new RawContextKey('isIOS', isIOS, localize('isIOS', "Whether the operating system is IOS")); export const IsDevelopmentContext = new RawContextKey('isDevelopment', false, true); diff --git a/lib/vscode/src/vs/platform/contextview/browser/contextMenuService.ts b/lib/vscode/src/vs/platform/contextview/browser/contextMenuService.ts index 77c92d6dd26b..afc0b1c930c0 100644 --- a/lib/vscode/src/vs/platform/contextview/browser/contextMenuService.ts +++ b/lib/vscode/src/vs/platform/contextview/browser/contextMenuService.ts @@ -12,12 +12,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Disposable } from 'vs/base/common/lifecycle'; import { ModifierKeyEmitter } from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; export class ContextMenuService extends Disposable implements IContextMenuService { declare readonly _serviceBrand: undefined; private contextMenuHandler: ContextMenuHandler; + readonly onDidShowContextMenu = new Emitter().event; + constructor( @ITelemetryService telemetryService: ITelemetryService, @INotificationService notificationService: INotificationService, diff --git a/lib/vscode/src/vs/platform/contextview/browser/contextView.ts b/lib/vscode/src/vs/platform/contextview/browser/contextView.ts index b33d652cf120..9a37ae690384 100644 --- a/lib/vscode/src/vs/platform/contextview/browser/contextView.ts +++ b/lib/vscode/src/vs/platform/contextview/browser/contextView.ts @@ -7,6 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { AnchorAlignment, AnchorAxisAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { Event } from 'vs/base/common/event'; export const IContextViewService = createDecorator('contextViewService'); @@ -40,5 +41,7 @@ export interface IContextMenuService { readonly _serviceBrand: undefined; + readonly onDidShowContextMenu: Event; + showContextMenu(delegate: IContextMenuDelegate): void; } diff --git a/lib/vscode/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/lib/vscode/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 164e8d7694c5..b06fa6789eb9 100644 --- a/lib/vscode/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/lib/vscode/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { MessageBoxOptions, MessageBoxReturnValue, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, dialog, FileFilter, BrowserWindow } from 'electron'; import { Queue } from 'vs/base/common/async'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { isMacintosh } from 'vs/base/common/platform'; import { dirname } from 'vs/base/common/path'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -55,7 +55,7 @@ export class DialogMainService implements IDialogMainService { private readonly noWindowDialogueQueue = new Queue(); constructor( - @IStateService private readonly stateService: IStateService + @IStateMainService private readonly stateMainService: IStateMainService ) { } @@ -89,8 +89,7 @@ export class DialogMainService implements IDialogMainService { }; // Ensure defaultPath - dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem(DialogMainService.workingDirPickerStorageKey); - + dialogOptions.defaultPath = options.defaultPath || this.stateMainService.getItem(DialogMainService.workingDirPickerStorageKey); // Ensure properties if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') { @@ -116,7 +115,7 @@ export class DialogMainService implements IDialogMainService { if (result && result.filePaths && result.filePaths.length > 0) { // Remember path in storage for next time - this.stateService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0])); + this.stateMainService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0])); return result.filePaths; } diff --git a/lib/vscode/src/vs/platform/dialogs/test/common/testDialogService.ts b/lib/vscode/src/vs/platform/dialogs/test/common/testDialogService.ts index 8d3d2465b76e..1d2b2f08f71b 100644 --- a/lib/vscode/src/vs/platform/dialogs/test/common/testDialogService.ts +++ b/lib/vscode/src/vs/platform/dialogs/test/common/testDialogService.ts @@ -10,8 +10,23 @@ export class TestDialogService implements IDialogService { declare readonly _serviceBrand: undefined; - confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } - show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } - input(): Promise { { return Promise.resolve({ choice: 0, values: [] }); } } - about(): Promise { return Promise.resolve(); } + private confirmResult: IConfirmationResult | undefined = undefined; + setConfirmResult(result: IConfirmationResult) { + this.confirmResult = result; + } + + async confirm(confirmation: IConfirmation): Promise { + if (this.confirmResult) { + const confirmResult = this.confirmResult; + this.confirmResult = undefined; + + return confirmResult; + } + + return { confirmed: false }; + } + + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { return { choice: 0 }; } + async input(): Promise { { return { choice: 0, values: [] }; } } + async about(): Promise { } } diff --git a/lib/vscode/src/vs/platform/editor/common/editor.ts b/lib/vscode/src/vs/platform/editor/common/editor.ts index c5611b87ae40..f4e2cffb7614 100644 --- a/lib/vscode/src/vs/platform/editor/common/editor.ts +++ b/lib/vscode/src/vs/platform/editor/common/editor.ts @@ -37,17 +37,17 @@ export interface IEditorModel { export interface IBaseResourceEditorInput { /** - * Optional options to use when opening the text input. + * Optional options to use when opening the input. */ - options?: ITextEditorOptions; + options?: IEditorOptions; /** - * Label to show for the diff editor + * Label to show for the input. */ readonly label?: string; /** - * Description to show for the diff editor + * Description to show for the input. */ readonly description?: string; @@ -70,21 +70,30 @@ export interface IBaseResourceEditorInput { readonly forceUntitled?: boolean; } -/** - * This identifier allows to uniquely identify an editor with a - * resource and type identifier. - */ -export interface IResourceEditorInputIdentifier { +export interface IBaseTextResourceEditorInput extends IBaseResourceEditorInput { /** - * The resource URI of the editor. + * Optional options to use when opening the text input. */ - readonly resource: URI; + options?: ITextEditorOptions; /** - * The type of the editor. + * The contents of the text input if known. If provided, + * the input will not attempt to load the contents from + * disk and may appear dirty. */ - readonly typeId: string; + contents?: string; + + /** + * The encoding of the text input if known. + */ + encoding?: string; + + /** + * The identifier of the language mode of the text input + * if known to use when displaying the contents. + */ + mode?: string; } export interface IResourceEditorInput extends IBaseResourceEditorInput { @@ -93,17 +102,31 @@ export interface IResourceEditorInput extends IBaseResourceEditorInput { * The resource URI of the resource to open. */ readonly resource: URI; +} + +export interface ITextResourceEditorInput extends IResourceEditorInput, IBaseTextResourceEditorInput { /** - * The encoding of the text input if known. + * Optional options to use when opening the text input. */ - readonly encoding?: string; + options?: ITextEditorOptions; +} + +/** + * This identifier allows to uniquely identify an editor with a + * resource and type identifier. + */ +export interface IResourceEditorInputIdentifier { /** - * The identifier of the language mode of the text input - * if known to use when displaying the contents. + * The resource URI of the editor. */ - readonly mode?: string; + readonly resource: URI; + + /** + * The type of the editor. + */ + readonly typeId: string; } export enum EditorActivation { @@ -169,7 +192,7 @@ export interface IEditorOptions { * Will also not activate the group the editor opens in unless the group is already * the active one. This behaviour can be overridden via the `activation` option. */ - readonly preserveFocus?: boolean; + preserveFocus?: boolean; /** * This option is only relevant if an editor is opened into a group that is not active @@ -179,14 +202,14 @@ export interface IEditorOptions { * By default, the editor group will become active unless `preserveFocus` or `inactive` * is specified. */ - readonly activation?: EditorActivation; + activation?: EditorActivation; /** * Tells the editor to reload the editor input in the editor even if it is identical to the one * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ - readonly forceReload?: boolean; + forceReload?: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. @@ -194,7 +217,7 @@ export interface IEditorOptions { * Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly * to the side of another one or into a specific editor group. */ - readonly revealIfVisible?: boolean; + revealIfVisible?: boolean; /** * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. @@ -202,24 +225,24 @@ export interface IEditorOptions { * Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly * to the side of another one or into a specific editor group. */ - readonly revealIfOpened?: boolean; + revealIfOpened?: boolean; /** * An editor that is pinned remains in the editor stack even when another editor is being opened. * An editor that is not pinned will always get replaced by another editor that is not pinned. */ - readonly pinned?: boolean; + pinned?: boolean; /** * An editor that is sticky moves to the beginning of the editors list within the group and will remain * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. */ - readonly sticky?: boolean; + sticky?: boolean; /** * The index in the document stack where to insert the editor into when opening. */ - readonly index?: number; + index?: number; /** * An active editor that is opened will show its contents directly. Set to true to open an editor @@ -228,13 +251,13 @@ export interface IEditorOptions { * Will also not activate the group the editor opens in unless the group is already * the active one. This behaviour can be overridden via the `activation` option. */ - readonly inactive?: boolean; + inactive?: boolean; /** * Will not show an error in case opening the editor fails and thus allows to show a custom error * message as needed. By default, an error will be presented as notification if opening was not possible. */ - readonly ignoreError?: boolean; + ignoreError?: boolean; /** * Allows to override the editor that should be used to display the input: @@ -242,7 +265,7 @@ export interface IEditorOptions { * - `string`: specific override by id * - `EditorOverride`: specific override handling */ - readonly override?: string | EditorOverride; + override?: string | EditorOverride; /** * A optional hint to signal in which context the editor opens. @@ -254,7 +277,7 @@ export interface IEditorOptions { * some background task, the notification would show in the background, * not as a modal dialog. */ - readonly context?: EditorOpenContext; + context?: EditorOpenContext; } export interface ITextEditorSelection { @@ -269,14 +292,17 @@ export const enum TextEditorSelectionRevealType { * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically. */ Center = 0, + /** * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport. */ CenterIfOutsideViewport = 1, + /** * Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top. */ NearTop = 2, + /** * Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top. * Only if it lies outside the viewport @@ -289,16 +315,16 @@ export interface ITextEditorOptions extends IEditorOptions { /** * Text editor selection. */ - readonly selection?: ITextEditorSelection; + selection?: ITextEditorSelection; /** * Text editor view state. */ - readonly viewState?: object; + viewState?: object; /** * Option to control the text editor selection reveal type. * Defaults to TextEditorSelectionRevealType.Center */ - readonly selectionRevealType?: TextEditorSelectionRevealType; + selectionRevealType?: TextEditorSelectionRevealType; } diff --git a/lib/vscode/src/vs/platform/environment/common/argv.ts b/lib/vscode/src/vs/platform/environment/common/argv.ts index 07cd8868338c..08a4afa55ba1 100644 --- a/lib/vscode/src/vs/platform/environment/common/argv.ts +++ b/lib/vscode/src/vs/platform/environment/common/argv.ts @@ -7,8 +7,6 @@ * A list of command line arguments we support natively. */ export interface NativeParsedArgs { - 'extra-extensions-dir'?: string[]; - 'extra-builtin-extensions-dir'?: string[]; _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more @@ -41,6 +39,8 @@ export interface NativeParsedArgs { 'extensions-dir'?: string; 'extensions-download-dir'?: string; 'builtin-extensions-dir'?: string; + 'extra-extensions-dir'?: string[]; // NOTE@coder: added extra extensions dir + 'extra-builtin-extensions-dir'?: string[]; // NOTE@coder: added extra builtin extensions dir extensionDevelopmentPath?: string[]; // undefined or array of 1 or more local paths or URIs extensionTestsPath?: string; // either a local path or a URI extensionDevelopmentKind?: string[]; @@ -67,6 +67,7 @@ export interface NativeParsedArgs { 'install-source'?: string; 'disable-updates'?: boolean; 'disable-keytar'?: boolean; + 'disable-workspace-trust'?: boolean; 'disable-crash-reporter'?: boolean; 'crash-reporter-directory'?: string; 'crash-reporter-id'?: string; diff --git a/lib/vscode/src/vs/platform/environment/common/environment.ts b/lib/vscode/src/vs/platform/environment/common/environment.ts index 3161ff354cab..f5d5fd956db8 100644 --- a/lib/vscode/src/vs/platform/environment/common/environment.ts +++ b/lib/vscode/src/vs/platform/environment/common/environment.ts @@ -66,6 +66,9 @@ export interface IEnvironmentService { extensionDevelopmentKind?: ExtensionKind[]; extensionTestsLocationURI?: URI; + // --- workspace trust + disableWorkspaceTrust: boolean; + // --- logging logsPath: string; logLevel?: string; @@ -121,8 +124,9 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsPath: string; extensionsDownloadPath: string; builtinExtensionsPath: string; - extraExtensionPaths: string[] - extraBuiltinExtensionPaths: string[] + // NOTE@coder: add extraExtensionPaths/extraBuiltinExtensionPaths + extraExtensionPaths: string[]; + extraBuiltinExtensionPaths: string[]; // --- smoke test support driverHandle?: string; diff --git a/lib/vscode/src/vs/platform/environment/common/environmentService.ts b/lib/vscode/src/vs/platform/environment/common/environmentService.ts index cee37bfe3390..001e09358a72 100644 --- a/lib/vscode/src/vs/platform/environment/common/environmentService.ts +++ b/lib/vscode/src/vs/platform/environment/common/environmentService.ts @@ -16,22 +16,6 @@ import { ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { env } from 'vs/base/common/process'; -function parsePathArg(arg: string | undefined, process: NodeJS.Process): string | undefined { - if (!arg) { - return undefined; - } - - // Determine if the arg is relative or absolute, if relative use the original CWD - // (VSCODE_CWD), not the potentially overridden one (process.cwd()). - const resolved = resolve(arg); - - if (normalize(arg) === resolved) { - return resolved; - } - - return resolve(process.env['VSCODE_CWD'] || process.cwd(), arg); -} - export interface INativeEnvironmentPaths { /** @@ -174,6 +158,19 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron return joinPath(this.userHome, this.productService.dataFolderName, 'extensions').fsPath; } + /** + * NOTE@coder: add extraExtensionPaths and extraBuiltinExtensionPaths + */ + @memoize + get extraExtensionPaths(): string[] { + return (this._args['extra-extensions-dir'] || []).map((p) => resolve(p)); + } + + @memoize + get extraBuiltinExtensionPaths(): string[] { + return (this._args['extra-builtin-extensions-dir'] || []).map((p) => resolve(p)); + } + @memoize get extensionDevelopmentLocationURI(): URI[] | undefined { const extensionDevelopmentPaths = this.args.extensionDevelopmentPath; @@ -190,19 +187,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron return undefined; } - /** - * NOTE@coder: add extraExtensionPaths and extraBuiltinExtensionPaths - */ - @memoize - get extraExtensionPaths(): string[] { - return (this._args['extra-extensions-dir'] || []).map((p) => parsePathArg(p, process)); - } - - @memoize - get extraBuiltinExtensionPaths(): string[] { - return (this._args['extra-builtin-extensions-dir'] || []).map((p) => parsePathArg(p, process)); - } - @memoize get extensionDevelopmentKind(): ExtensionKind[] | undefined { return this.args.extensionDevelopmentKind?.map(kind => kind === 'ui' || kind === 'workspace' || kind === 'web' ? kind : 'workspace'); @@ -261,6 +245,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); } get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; } + @memoize + get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } + get args(): NativeParsedArgs { return this._args; } constructor( diff --git a/lib/vscode/src/vs/platform/environment/node/argv.ts b/lib/vscode/src/vs/platform/environment/node/argv.ts index faf8cfa00cbd..6b65cf668fd9 100644 --- a/lib/vscode/src/vs/platform/environment/node/argv.ts +++ b/lib/vscode/src/vs/platform/environment/node/argv.ts @@ -52,11 +52,12 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, - 'extra-builtin-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra builtin extension directory.' }, + // NOTE@coder: add extra-extensions-dir and extra-builtin-extensions-dir 'extra-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra user extension directory.' }, + 'extra-builtin-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra builtin extension directory.' }, 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") }, - 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") }, + 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' }, 'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") }, 'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, 'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, @@ -65,18 +66,18 @@ export const OPTIONS: OptionDescriptions> = { 'verbose': { type: 'boolean', cat: 't', description: localize('verbose', "Print verbose output (implies --wait).") }, 'log': { type: 'string', cat: 't', args: 'level', description: localize('log', "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'.") }, 'status': { type: 'boolean', alias: 's', cat: 't', description: localize('status', "Print process usage and diagnostics information.") }, - 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") }, + 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup.") }, 'prof-append-timers': { type: 'string' }, 'prof-startup-prefix': { type: 'string' }, 'prof-v8-extensions': { type: 'boolean' }, 'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") }, 'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") }, - 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, + 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off."), args: ['on', 'off'] }, 'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, 'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") }, - 'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes).") }, + 'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes)."), args: 'memory' }, 'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") }, 'remote': { type: 'string' }, @@ -99,6 +100,7 @@ export const OPTIONS: OptionDescriptions> = { 'disable-telemetry': { type: 'boolean' }, 'disable-updates': { type: 'boolean' }, 'disable-keytar': { type: 'boolean' }, + 'disable-workspace-trust': { type: 'boolean' }, 'disable-crash-reporter': { type: 'boolean' }, 'crash-reporter-directory': { type: 'string' }, 'crash-reporter-id': { type: 'string' }, diff --git a/lib/vscode/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/lib/vscode/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 45d3b68c4836..068a23e52dbe 100644 --- a/lib/vscode/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/lib/vscode/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -25,48 +25,48 @@ import { optional } from 'vs/platform/instantiation/common/instantiation'; import { joinPath } from 'vs/base/common/resources'; interface IRawGalleryExtensionFile { - assetType: string; - source: string; + readonly assetType: string; + readonly source: string; } interface IRawGalleryExtensionProperty { - key: string; - value: string; + readonly key: string; + readonly value: string; } interface IRawGalleryExtensionVersion { - version: string; - lastUpdated: string; - assetUri: string; - fallbackAssetUri: string; - files: IRawGalleryExtensionFile[]; - properties?: IRawGalleryExtensionProperty[]; + readonly version: string; + readonly lastUpdated: string; + readonly assetUri: string; + readonly fallbackAssetUri: string; + readonly files: IRawGalleryExtensionFile[]; + readonly properties?: IRawGalleryExtensionProperty[]; } interface IRawGalleryExtensionStatistics { - statisticName: string; - value: number; + readonly statisticName: string; + readonly value: number; } interface IRawGalleryExtension { - extensionId: string; - extensionName: string; - displayName: string; - shortDescription: string; - publisher: { displayName: string, publisherId: string, publisherName: string; }; - versions: IRawGalleryExtensionVersion[]; - statistics: IRawGalleryExtensionStatistics[]; - flags: string; + readonly extensionId: string; + readonly extensionName: string; + readonly displayName: string; + readonly shortDescription: string; + readonly publisher: { displayName: string, publisherId: string, publisherName: string; }; + readonly versions: IRawGalleryExtensionVersion[]; + readonly statistics: IRawGalleryExtensionStatistics[]; + readonly flags: string; } interface IRawGalleryQueryResult { - results: { - extensions: IRawGalleryExtension[]; - resultMetadata: { - metadataType: string; - metadataItems: { - name: string; - count: number; + readonly results: { + readonly extensions: IRawGalleryExtension[]; + readonly resultMetadata: { + readonly metadataType: string; + readonly metadataItems: { + readonly name: string; + readonly count: number; }[]; }[] }[]; @@ -121,20 +121,20 @@ const PropertyType = { }; interface ICriterium { - filterType: FilterType; - value?: string; + readonly filterType: FilterType; + readonly value?: string; } const DefaultPageSize = 10; interface IQueryState { - pageNumber: number; - pageSize: number; - sortBy: SortBy; - sortOrder: SortOrder; - flags: Flags; - criteria: ICriterium[]; - assetTypes: string[]; + readonly pageNumber: number; + readonly pageSize: number; + readonly sortBy: SortBy; + readonly sortOrder: SortOrder; + readonly flags: Flags; + readonly criteria: ICriterium[]; + readonly assetTypes: string[]; } const DefaultQueryState: IQueryState = { @@ -148,32 +148,32 @@ const DefaultQueryState: IQueryState = { }; type GalleryServiceQueryClassification = { - filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true }; - success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true }; + readonly success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + readonly count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; type QueryTelemetryData = { - filterTypes: string[]; - sortBy: string; - sortOrder: string; + readonly filterTypes: string[]; + readonly sortBy: string; + readonly sortOrder: string; }; type GalleryServiceQueryEvent = QueryTelemetryData & { - duration: number; - success: boolean; - requestBodySize: string; - responseBodySize?: string; - statusCode?: string; - errorCode?: string; - count?: string; + readonly duration: number; + readonly success: boolean; + readonly requestBodySize: string; + readonly responseBodySize?: string; + readonly statusCode?: string; + readonly errorCode?: string; + readonly count?: string; }; class Query { @@ -358,13 +358,11 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller /* __GDPR__FRAGMENT__ "GalleryExtensionTelemetryData2" : { "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "searchText": { "classification": "CustomerContent", "purpose": "FeatureInsight" }, "querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ telemetryData: { index: ((query.pageNumber - 1) * query.pageSize) + index, - searchText: query.searchText, querySource }, preview: getIsPreview(galleryExtension.flags) @@ -435,7 +433,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { private async getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1; - if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) { return extension; } const { id, uuid } = extension ? extension.identifier : arg1; @@ -460,7 +458,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const versionAsset = rawExtension.versions.filter(v => v.version === version)[0]; if (versionAsset) { const extension = toExtension(rawExtension, versionAsset, 0, query); - if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) { return extension; } } @@ -713,7 +711,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { try { engine = await this.getEngine(v); } catch (error) { /* Ignore error and skip version */ } - if (engine && isEngineValid(engine, this.productService.version)) { + if (engine && isEngineValid(engine, this.productService.version, this.productService.date)) { result.push({ version: v!.version, date: v!.lastUpdated }); } })); @@ -776,7 +774,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!engine) { return null; } - if (isEngineValid(engine, this.productService.version)) { + if (isEngineValid(engine, this.productService.version, this.productService.date)) { return version; } } @@ -811,13 +809,14 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const version = versions[0]; const engine = await this.getEngine(version); - if (!isEngineValid(engine, this.productService.version)) { + if (!isEngineValid(engine, this.productService.version, this.productService.date)) { return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1)); } - version.properties = version.properties || []; - version.properties.push({ key: PropertyType.Engine, value: engine }); - return version; + return { + ...version, + properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }] + }; } async getExtensionsReport(): Promise { diff --git a/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagement.ts b/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagement.ts index e8c40c65d8e0..79b96e3434c7 100644 --- a/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -203,6 +203,7 @@ export class ExtensionManagementError extends Error { } export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean }; +export type InstallVSIXOptions = InstallOptions & { installOnlyNewlyAddedFromExtensionPack?: boolean }; export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean }; export const IExtensionManagementService = createDecorator('extensionManagementService'); @@ -217,7 +218,7 @@ export interface IExtensionManagementService { zip(extension: ILocalExtension): Promise; unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; - install(vsix: URI, options?: InstallOptions): Promise; + install(vsix: URI, options?: InstallVSIXOptions): Promise; canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; diff --git a/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 75422f963b26..1c18ea2121d6 100644 --- a/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/lib/vscode/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Emitter, Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; @@ -62,7 +62,7 @@ export class ExtensionManagementChannel implements IServerChannel { switch (command) { case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer)); case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); - case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); + case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer), args[1]); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); case 'canInstall': return this.service.canInstall(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]); @@ -112,8 +112,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('unzip', [zipLocation])); } - install(vsix: URI): Promise { - return Promise.resolve(this.channel.call('install', [vsix])).then(local => transformIncomingExtension(local, null)); + install(vsix: URI, options?: InstallVSIXOptions): Promise { + return Promise.resolve(this.channel.call('install', [vsix, options])).then(local => transformIncomingExtension(local, null)); } getManifest(vsix: URI): Promise { diff --git a/lib/vscode/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/lib/vscode/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 53b5f311e54b..97fcf5cc46d6 100644 --- a/lib/vscode/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/lib/vscode/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; +import { Promises as FSPromises } from 'vs/base/node/pfs'; import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -77,7 +77,7 @@ export class ExtensionsDownloader extends Disposable { private async rename(from: URI, to: URI, retryUntil: number): Promise { try { - await promises.rename(from.fsPath, to.fsPath); + await FSPromises.rename(from.fsPath, to.fsPath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`); @@ -90,7 +90,7 @@ export class ExtensionsDownloader extends Disposable { private async cleanUp(): Promise { try { if (!(await this.fileService.exists(this.extensionsDownloadDir))) { - this.logService.trace('Extension VSIX downloads cache dir does not exist'); + this.logService.trace('Extension VSIX downlads cache dir does not exist'); return; } const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true }); diff --git a/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts index ed907661490f..7410e3572fc9 100644 --- a/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; @@ -22,7 +21,8 @@ import { INSTALL_ERROR_INCOMPATIBLE, ExtensionManagementError, InstallOptions, - UninstallOptions + UninstallOptions, + InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -140,7 +140,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); + const stats = await Promise.all(entries.map(e => pfs.Promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; @@ -160,7 +160,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); } - async install(vsix: URI, options: InstallOptions = {}): Promise { + async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); return createCancelablePromise(async token => { @@ -170,7 +170,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const manifest = await getManifest(zipPath); const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; let operation: InstallOperation = InstallOperation.Install; - if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version)) { + if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version, product.date)) { throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", identifier.id, product.version)); } @@ -212,7 +212,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } catch (e) { /* Ignore */ } try { - const local = await this.installFromZipPath(identifierWithVersion, zipPath, { ...(metadata || {}), ...options }, options, operation, token); + const local = await this.installFromZipPath(identifierWithVersion, zipPath, options.installOnlyNewlyAddedFromExtensionPack ? existing : undefined, { ...(metadata || {}), ...options }, options, operation, token); this.logService.info('Successfully installed the extension:', identifier.id); return local; } catch (e) { @@ -235,11 +235,13 @@ export class ExtensionManagementService extends Disposable implements IExtension return downloadedLocation; } - private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, options: InstallOptions, operation: InstallOperation, token: CancellationToken): Promise { + private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, existing: ILocalExtension | undefined, metadata: IMetadata | undefined, options: InstallOptions, operation: InstallOperation, token: CancellationToken): Promise { try { const local = await this.installExtension({ zipPath, identifierWithVersion, metadata }, token); try { - await this.installDependenciesAndPackExtensions(local, undefined, options); + if (!options.donotIncludePackAndDependencies) { + await this.installDependenciesAndPackExtensions(local, existing, options); + } } catch (error) { if (isNonEmptyArray(local.manifest.extensionDependencies)) { this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message); diff --git a/lib/vscode/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/lib/vscode/src/vs/platform/extensionManagement/node/extensionsScanner.ts index ffec784ab3fa..01b0d7720db4 100644 --- a/lib/vscode/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/lib/vscode/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as semver from 'vs/base/common/semver/semver'; import { Disposable } from 'vs/base/common/lifecycle'; import * as pfs from 'vs/base/node/pfs'; @@ -98,7 +97,7 @@ export class ExtensionsScanner extends Disposable { } async scanAllUserExtensions(): Promise { - return this.scanExtensionsInDirs(this.extensionsPath, this.environmentService.extraExtensionPaths, ExtensionType.User); + return this.scanExtensionsInDirs([this.extensionsPath, ...this.environmentService.extraExtensionPaths], ExtensionType.User); } async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { @@ -159,7 +158,7 @@ export class ExtensionsScanner extends Disposable { storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined; storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined; const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await fs.promises.readFile(manifestPath, 'utf8'); + const raw = await pfs.Promises.readFile(manifestPath, 'utf8'); const { manifest } = await this.parseManifest(raw); (manifest as ILocalExtensionManifest).__metadata = storedMetadata; await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); @@ -192,7 +191,7 @@ export class ExtensionsScanner extends Disposable { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - raw = await fs.promises.readFile(this.uninstalledPath, 'utf8'); + raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') { throw err; @@ -251,7 +250,7 @@ export class ExtensionsScanner extends Disposable { private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { try { - await fs.promises.rename(extractPath, renamePath); + await pfs.Promises.rename(extractPath, renamePath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); @@ -315,7 +314,7 @@ export class ExtensionsScanner extends Disposable { } private async scanDefaultSystemExtensions(): Promise { - const result = await this.scanExtensionsInDirs(this.systemExtensionsPath, this.environmentService.extraBuiltinExtensionPaths, ExtensionType.System); + const result = await this.scanExtensionsInDirs([this.systemExtensionsPath, ...this.environmentService.extraBuiltinExtensionPaths], ExtensionType.System); this.logService.trace('Scanned system extensions:', result.length); return result; } @@ -394,9 +393,9 @@ export class ExtensionsScanner extends Disposable { private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> { const promises = [ - fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') + pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') .then(raw => this.parseManifest(raw)), - fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') + pfs.Promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') .then(raw => JSON.parse(raw)) ]; @@ -420,8 +419,8 @@ export class ExtensionsScanner extends Disposable { }); } - private async scanExtensionsInDirs(dir: string, dirs: string[], type: ExtensionType): Promise{ - const results = await Promise.all([dir, ...dirs].map((path) => this.scanExtensionsInDir(path, type))); + private async scanExtensionsInDirs(dirs: string[], type: ExtensionType): Promise{ + const results = await Promise.all(dirs.map((path) => this.scanExtensionsInDir(path, type))); return results.reduce((flat, current) => flat.concat(current), []); } } diff --git a/lib/vscode/src/vs/platform/extensions/common/extensionValidator.ts b/lib/vscode/src/vs/platform/extensions/common/extensionValidator.ts index 77f125ff2201..89d668cbac07 100644 --- a/lib/vscode/src/vs/platform/extensions/common/extensionValidator.ts +++ b/lib/vscode/src/vs/platform/extensions/common/extensionValidator.ts @@ -24,10 +24,12 @@ export interface INormalizedVersion { minorMustEqual: boolean; patchBase: number; patchMustEqual: boolean; + notBefore: number; /* milliseconds timestamp, or 0 */ isMinimum: boolean; } const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/; +const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})$/; export function isValidVersionStr(version: string): boolean { version = version.trim(); @@ -93,6 +95,15 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer } } + let notBefore = 0; + if (version.preRelease) { + const match = NOT_BEFORE_REGEXP.exec(version.preRelease); + if (match) { + const [, year, month, day] = match; + notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day)); + } + } + return { majorBase: majorBase, majorMustEqual: majorMustEqual, @@ -100,16 +111,24 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer minorMustEqual: minorMustEqual, patchBase: patchBase, patchMustEqual: patchMustEqual, - isMinimum: version.hasGreaterEquals + isMinimum: version.hasGreaterEquals, + notBefore, }; } -export function isValidVersion(_version: string | INormalizedVersion, _desiredVersion: string | INormalizedVersion): boolean { +export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean { let version: INormalizedVersion | null; - if (typeof _version === 'string') { - version = normalizeVersion(parseVersion(_version)); + if (typeof _inputVersion === 'string') { + version = normalizeVersion(parseVersion(_inputVersion)); } else { - version = _version; + version = _inputVersion; + } + + let productTs: number | undefined; + if (_inputDate instanceof Date) { + productTs = _inputDate.getTime(); + } else if (typeof _inputDate === 'string') { + productTs = new Date(_inputDate).getTime(); } let desiredVersion: INormalizedVersion | null; @@ -130,6 +149,7 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe let desiredMajorBase = desiredVersion.majorBase; let desiredMinorBase = desiredVersion.minorBase; let desiredPatchBase = desiredVersion.patchBase; + let desiredNotBefore = desiredVersion.notBefore; let majorMustEqual = desiredVersion.majorMustEqual; let minorMustEqual = desiredVersion.minorMustEqual; @@ -152,6 +172,10 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe return false; } + if (productTs && productTs < desiredNotBefore) { + return false; + } + return patchBase >= desiredPatchBase; } @@ -200,6 +224,11 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe } // at this point, patchBase are equal + + if (productTs && productTs < desiredNotBefore) { + return false; + } + return true; } @@ -211,22 +240,24 @@ export interface IReducedExtensionDescription { main?: string; } -export function isValidExtensionVersion(version: string, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean { +type ProductDate = string | Date | undefined; + +export function isValidExtensionVersion(version: string, date: ProductDate, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean { if (extensionDesc.isBuiltin || typeof extensionDesc.main === 'undefined') { // No version check for builtin or declarative extensions return true; } - return isVersionValid(version, extensionDesc.engines.vscode, notices); + return isVersionValid(version, date, extensionDesc.engines.vscode, notices); } -export function isEngineValid(engine: string, version: string): boolean { +export function isEngineValid(engine: string, version: string, date: ProductDate): boolean { // TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version - return engine === '*' || isVersionValid(version, engine); + return engine === '*' || isVersionValid(version, date, engine); } -export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean { +function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean { let desiredVersion = normalizeVersion(parseVersion(requestedVersion)); if (!desiredVersion) { @@ -251,7 +282,7 @@ export function isVersionValid(currentVersion: string, requestedVersion: string, } } - if (!isValidVersion(currentVersion, desiredVersion)) { + if (!isValidVersion(currentVersion, date, desiredVersion)) { notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion)); return false; } diff --git a/lib/vscode/src/vs/platform/extensions/common/extensions.ts b/lib/vscode/src/vs/platform/extensions/common/extensions.ts index dde7a4c4b00d..663d8934e4c6 100644 --- a/lib/vscode/src/vs/platform/extensions/common/extensions.ts +++ b/lib/vscode/src/vs/platform/extensions/common/extensions.ts @@ -116,10 +116,12 @@ export interface IAuthenticationContribution { export interface IWalkthroughStep { readonly id: string; readonly title: string; - readonly description: string; + readonly description: string | undefined; readonly media: - | { path: string | { dark: string, light: string, hc: string }, altText: string } - | { path: string, }, + | { image: string | { dark: string, light: string, hc: string }, altText: string, markdown?: never } + | { markdown: string, image?: never } + readonly completionEvents?: string[]; + /** @deprecated use `completionEvents: 'onCommand:...'` */ readonly doneOn?: { command: string }; readonly when?: string; } @@ -129,7 +131,6 @@ export interface IWalkthrough { readonly title: string; readonly description: string; readonly steps: IWalkthroughStep[]; - readonly primary?: boolean; readonly when?: string; } @@ -166,13 +167,30 @@ export interface IExtensionContributions { } export interface IExtensionCapabilities { - readonly virtualWorkspaces?: boolean; + readonly virtualWorkspaces?: ExtensionVirtualWorkpaceSupport; readonly untrustedWorkspaces?: ExtensionUntrustedWorkspaceSupport; } + + export type ExtensionKind = 'ui' | 'workspace' | 'web'; -export type ExtensionUntrustedWorkpaceSupportType = boolean | 'limited'; -export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: 'limited', description: string, restrictedConfigurations?: string[] }; + +export type LimitedWorkpaceSupportType = 'limited'; +export type ExtensionUntrustedWorkpaceSupportType = boolean | LimitedWorkpaceSupportType; +export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: LimitedWorkpaceSupportType, description: string, restrictedConfigurations?: string[] }; + +export type ExtensionVirtualWorkpaceSupportType = boolean | LimitedWorkpaceSupportType; +export type ExtensionVirtualWorkpaceSupport = boolean | { supported: true; } | { supported: false | LimitedWorkpaceSupportType, description: string }; + +export function getWorkpaceSupportTypeMessage(supportType: ExtensionUntrustedWorkspaceSupport | ExtensionVirtualWorkpaceSupport | undefined): string | undefined { + if (typeof supportType === 'object' && supportType !== null) { + if (supportType.supported !== true) { + return supportType.description; + } + } + return undefined; +} + export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing diff --git a/lib/vscode/src/vs/platform/extensions/test/common/extensionValidator.test.ts b/lib/vscode/src/vs/platform/extensions/test/common/extensionValidator.test.ts index 999789c85a61..5fd309aac0f8 100644 --- a/lib/vscode/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/lib/vscode/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; suite('Extension Version Validator', () => { + const productVersion = '2021-05-11T21:54:30.577Z'; test('isValidVersionStr', () => { assert.strictEqual(isValidVersionStr('0.10.0-dev'), true); @@ -53,13 +54,16 @@ suite('Extension Version Validator', () => { }); test('normalizeVersion', () => { - function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void { + function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean, notBefore = 0): void { const actual = normalizeVersion(parseVersion(version)); - const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum }; + const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum, notBefore }; assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version); } - assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false); + assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false, 0); + assertNormalizeVersion('0.10.0-222222222', 0, true, 10, true, 0, true, false, 0); + assertNormalizeVersion('0.10.0-20210511', 0, true, 10, true, 0, true, false, new Date('2021-05-11T00:00:00Z').getTime()); + assertNormalizeVersion('0.10.0', 0, true, 10, true, 0, true, false); assertNormalizeVersion('0.10.1', 0, true, 10, true, 1, true, false); assertNormalizeVersion('0.10.100', 0, true, 10, true, 100, true, false); @@ -75,11 +79,12 @@ suite('Extension Version Validator', () => { assertNormalizeVersion('>=0.0.1', 0, true, 0, true, 1, true, true); assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true); + assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true); }); test('isValidVersion', () => { function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void { - let actual = isValidVersion(version, desiredVersion); + let actual = isValidVersion(version, productVersion, desiredVersion); assert.strictEqual(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult); } @@ -211,7 +216,7 @@ suite('Extension Version Validator', () => { main: hasMain ? 'something' : undefined }; let reasons: string[] = []; - let actual = isValidExtensionVersion(version, desc, reasons); + let actual = isValidExtensionVersion(version, productVersion, desc, reasons); assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons)); } @@ -390,5 +395,12 @@ suite('Extension Version Validator', () => { testIsValidVersion('2.0.0', '^1.100.0', false); testIsValidVersion('2.0.0', '^2.0.0', true); testIsValidVersion('2.0.0', '*', false); // fails due to lack of specificity + + // date tags + testIsValidVersion('1.10.0', '^1.10.0-20210511', true); // current date + testIsValidVersion('1.10.0', '^1.10.0-20210510', true); // before date + testIsValidVersion('1.10.0', '^1.10.0-20210512', false); // future date + testIsValidVersion('1.10.1', '^1.10.0-20200101', true); // before date, but ahead version + testIsValidVersion('1.11.0', '^1.10.0-20200101', true); }); }); diff --git a/lib/vscode/src/vs/workbench/contrib/externalTerminal/common/externalTerminal.ts b/lib/vscode/src/vs/platform/externalTerminal/common/externalTerminal.ts similarity index 65% rename from lib/vscode/src/vs/workbench/contrib/externalTerminal/common/externalTerminal.ts rename to lib/vscode/src/vs/platform/externalTerminal/common/externalTerminal.ts index 3aab081a27a2..df1a6e20a2ee 100644 --- a/lib/vscode/src/vs/workbench/contrib/externalTerminal/common/externalTerminal.ts +++ b/lib/vscode/src/vs/platform/externalTerminal/common/externalTerminal.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; -export const IExternalTerminalService = createDecorator('nativeTerminalService'); +export const IExternalTerminalService = createDecorator('externalTerminal'); export interface IExternalTerminalSettings { linuxExec?: string; @@ -14,10 +14,17 @@ export interface IExternalTerminalSettings { windowsExec?: string; } +export interface ITerminalForPlatform { + windows: string, + linux: string, + osx: string +} + export interface IExternalTerminalService { readonly _serviceBrand: undefined; - openTerminal(path: string): void; + openTerminal(configuration: IExternalTerminalSettings, path: string): Promise; runInTerminal(title: string, cwd: string, args: string[], env: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise; + getDefaultTerminalForPlatforms(): Promise; } export interface IExternalTerminalConfiguration { @@ -26,3 +33,11 @@ export interface IExternalTerminalConfiguration { external: IExternalTerminalSettings; }; } + +export const DEFAULT_TERMINAL_OSX = 'Terminal.app'; + +export const IExternalTerminalMainService = createDecorator('externalTerminal'); + +export interface IExternalTerminalMainService extends IExternalTerminalService { + readonly _serviceBrand: undefined; +} diff --git a/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.test.ts b/lib/vscode/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts similarity index 87% rename from lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.test.ts rename to lib/vscode/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts index 264e8a93275f..86970909b500 100644 --- a/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.test.ts +++ b/lib/vscode/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepEqual, equal } from 'assert'; -import { WindowsExternalTerminalService, LinuxExternalTerminalService, MacExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; -import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node/externalTerminal'; +import { DEFAULT_TERMINAL_OSX } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; suite('ExternalTerminalService', () => { let mockOnExit: Function; @@ -42,7 +42,7 @@ suite('ExternalTerminalService', () => { }; } }; - let testService = new WindowsExternalTerminalService(mockConfig); + let testService = new WindowsExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -67,7 +67,7 @@ suite('ExternalTerminalService', () => { } }; mockConfig.terminal.external.windowsExec = undefined; - let testService = new WindowsExternalTerminalService(mockConfig); + let testService = new WindowsExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -91,7 +91,7 @@ suite('ExternalTerminalService', () => { }; } }; - let testService = new WindowsExternalTerminalService(mockConfig); + let testService = new WindowsExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -115,7 +115,7 @@ suite('ExternalTerminalService', () => { return { on: (evt: any) => evt }; } }; - let testService = new WindowsExternalTerminalService(mockConfig); + let testService = new WindowsExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -137,7 +137,7 @@ suite('ExternalTerminalService', () => { return { on: (evt: any) => evt }; } }; - let testService = new WindowsExternalTerminalService(mockConfig); + let testService = new WindowsExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -160,7 +160,7 @@ suite('ExternalTerminalService', () => { }; } }; - let testService = new MacExternalTerminalService(mockConfig); + let testService = new MacExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -183,7 +183,7 @@ suite('ExternalTerminalService', () => { } }; mockConfig.terminal.external.osxExec = undefined; - let testService = new MacExternalTerminalService(mockConfig); + let testService = new MacExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -206,7 +206,7 @@ suite('ExternalTerminalService', () => { }; } }; - let testService = new LinuxExternalTerminalService(mockConfig); + let testService = new LinuxExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, @@ -230,7 +230,7 @@ suite('ExternalTerminalService', () => { } }; mockConfig.terminal.external.linuxExec = undefined; - let testService = new LinuxExternalTerminalService(mockConfig); + let testService = new LinuxExternalTerminalService(); (testService).spawnTerminal( mockSpawner, mockConfig, diff --git a/lib/vscode/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts b/lib/vscode/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts new file mode 100644 index 000000000000..3b1ce4c60c7b --- /dev/null +++ b/lib/vscode/src/vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; + +export const IExternalTerminalMainService = createDecorator('externalTerminal'); + +export interface IExternalTerminalMainService extends IExternalTerminalService { + readonly _serviceBrand: undefined; +} + +registerMainProcessRemoteService(IExternalTerminalMainService, 'externalTerminal', { supportsDelayedInstantiation: true }); diff --git a/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts b/lib/vscode/src/vs/platform/externalTerminal/node/externalTerminalService.ts similarity index 73% rename from lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts rename to lib/vscode/src/vs/platform/externalTerminal/node/externalTerminalService.ts index eb3b37ab5697..c90429f95092 100644 --- a/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts +++ b/lib/vscode/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -9,72 +9,32 @@ import * as processes from 'vs/base/node/processes'; import * as nls from 'vs/nls'; import * as pfs from 'vs/base/node/pfs'; import * as env from 'vs/base/common/platform'; -import { IExternalTerminalService, IExternalTerminalConfiguration, IExternalTerminalSettings } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; -import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node/externalTerminal'; +import { IExternalTerminalSettings, DEFAULT_TERMINAL_OSX, ITerminalForPlatform, IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { FileAccess } from 'vs/base/common/network'; import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; +import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); -export class WindowsExternalTerminalService implements IExternalTerminalService { +abstract class ExternalTerminalService { public _serviceBrand: undefined; - private static readonly CMD = 'cmd.exe'; - - private readonly _configurationService?: IConfigurationService; - - constructor( - @optional(IConfigurationService) configurationService: IConfigurationService - ) { - this._configurationService = configurationService; + async getDefaultTerminalForPlatforms(): Promise { + const linuxTerminal = await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); + return { windows: WindowsExternalTerminalService.getDefaultTerminalWindows(), linux: linuxTerminal, osx: 'xterm' }; } +} - public openTerminal(cwd?: string): void { - if (this._configurationService) { - const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd); - } - } - - public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { - - const exec = settings.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); - - return new Promise((resolve, reject) => { - - const title = `"${dir} - ${TERMINAL_TITLE}"`; - const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code - - const cmdArgs = [ - '/c', 'start', title, '/wait', exec, '/c', command - ]; - - // merge environment variables into a copy of the process.env - const env = Object.assign({}, process.env, envVars); - - // delete environment variables that have a null value - Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); - - const options: any = { - cwd: dir, - env: env, - windowsVerbatimArguments: true - }; - - const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options); - cmd.on('error', err => { - reject(improveError(err)); - }); +export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { + private static readonly CMD = 'cmd.exe'; + private static _DEFAULT_TERMINAL_WINDOWS: string; - resolve(undefined); - }); + public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { + return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd); } - private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise { - const terminalConfig = configuration.terminal.external; - const exec = terminalConfig.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); + public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, command: string, cwd?: string): Promise { + const exec = configuration.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); // Make the drive letter uppercase on Windows (see #9448) if (cwd && cwd[1] === ':') { @@ -102,14 +62,45 @@ export class WindowsExternalTerminalService implements IExternalTerminalService } return new Promise((c, e) => { - const env = cwd ? { cwd: cwd } : undefined; - const child = spawner.spawn(command, cmdArgs, env); + const env = getSanitizedEnvironment(process); + const child = spawner.spawn(command, cmdArgs, { cwd, env }); child.on('error', e); child.on('exit', () => c()); }); } - private static _DEFAULT_TERMINAL_WINDOWS: string; + public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { + const exec = 'windowsExec' in settings && settings.windowsExec ? settings.windowsExec : WindowsExternalTerminalService.getDefaultTerminalWindows(); + + return new Promise((resolve, reject) => { + + const title = `"${dir} - ${TERMINAL_TITLE}"`; + const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code + + const cmdArgs = [ + '/c', 'start', title, '/wait', exec, '/c', command + ]; + + // merge environment variables into a copy of the process.env + const env = Object.assign({}, getSanitizedEnvironment(process), envVars); + + // delete environment variables that have a null value + Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); + + const options: any = { + cwd: dir, + env: env, + windowsVerbatimArguments: true + }; + + const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options); + cmd.on('error', err => { + reject(improveError(err)); + }); + + resolve(undefined); + }); + } public static getDefaultTerminalWindows(): string { if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) { @@ -120,24 +111,11 @@ export class WindowsExternalTerminalService implements IExternalTerminalService } } -export class MacExternalTerminalService implements IExternalTerminalService { - public _serviceBrand: undefined; - +export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X - private readonly _configurationService?: IConfigurationService; - - constructor( - @optional(IConfigurationService) configurationService: IConfigurationService - ) { - this._configurationService = configurationService; - } - - public openTerminal(cwd?: string): void { - if (this._configurationService) { - const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, cwd); - } + public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { + return this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { @@ -204,9 +182,8 @@ export class MacExternalTerminalService implements IExternalTerminalService { }); } - private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { - const terminalConfig = configuration.terminal.external; - const terminalApp = terminalConfig.osxExec || DEFAULT_TERMINAL_OSX; + spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise { + const terminalApp = configuration.osxExec || DEFAULT_TERMINAL_OSX; return new Promise((c, e) => { const args = ['-a', terminalApp]; @@ -220,24 +197,12 @@ export class MacExternalTerminalService implements IExternalTerminalService { } } -export class LinuxExternalTerminalService implements IExternalTerminalService { - public _serviceBrand: undefined; +export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService { private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue..."); - private readonly _configurationService?: IConfigurationService; - - constructor( - @optional(IConfigurationService) configurationService: IConfigurationService - ) { - this._configurationService = configurationService; - } - - public openTerminal(cwd?: string): void { - if (this._configurationService) { - const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, cwd); - } + public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { + return this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { @@ -296,20 +261,6 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { }); } - private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { - const terminalConfig = configuration.terminal.external; - const execPromise = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); - - return new Promise((c, e) => { - execPromise.then(exec => { - const env = cwd ? { cwd } : undefined; - const child = spawner.spawn(exec, [], env); - child.on('error', e); - child.on('exit', () => c()); - }); - }); - } - private static _DEFAULT_TERMINAL_LINUX_READY: Promise; public static async getDefaultTerminalLinuxReady(): Promise { @@ -337,6 +288,25 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { } return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY; } + + spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise { + const execPromise = configuration.linuxExec ? Promise.resolve(configuration.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); + + return new Promise((c, e) => { + execPromise.then(exec => { + const env = getSanitizedEnvironment(process); + const child = spawner.spawn(exec, [], { cwd, env }); + child.on('error', e); + child.on('exit', () => c()); + }); + }); + } +} + +function getSanitizedEnvironment(process: NodeJS.Process) { + const env = process.env; + sanitizeProcessEnvironment(env); + return env; } /** diff --git a/lib/vscode/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/lib/vscode/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index cd2c43ebbaca..a2c59739067e 100644 --- a/lib/vscode/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/lib/vscode/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -23,7 +23,7 @@ const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirector const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown); // Arbitrary Internal Errors (should never be thrown in production) -const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); +const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occurred in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); export class IndexedDB { @@ -99,7 +99,7 @@ class IndexedDBFileSystemNode { } - read(path: string) { + read(path: string): IndexedDBFileSystemEntry | undefined { return this.doRead(path.split('/').filter(p => p.length)); } diff --git a/lib/vscode/src/vs/platform/files/common/fileService.ts b/lib/vscode/src/vs/platform/files/common/fileService.ts index 0453e6f4c60d..5cfc79d94ef5 100644 --- a/lib/vscode/src/vs/platform/files/common/fileService.ts +++ b/lib/vscode/src/vs/platform/files/common/fileService.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { mark } from 'vs/base/common/performance'; import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files'; +import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent, IReadFileStreamOptions, FileDeleteOptions, FilePermission, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources'; @@ -234,6 +234,7 @@ export class FileService extends Disposable implements IFileService { mtime: stat.mtime, ctime: stat.ctime, size: stat.size, + readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly), etag: etag({ mtime: stat.mtime, size: stat.size }) }; @@ -401,6 +402,9 @@ export class FileService extends Disposable implements IFileService { throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } + // File cannot be readonly + this.throwIfFileIsReadonly(resource, stat); + // Dirty write prevention: if the file on disk has been changed and does not match our expected // mtime and etag, we bail out to prevent dirty writing. // @@ -526,7 +530,14 @@ export class FileService extends Disposable implements IFileService { await consumeStream(fileStream); } - throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); + // Re-throw errors as file operation errors but preserve + // specific errors (such as not modified since) + const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()); + if (error instanceof NotModifiedSinceFileOperationError) { + throw new NotModifiedSinceFileOperationError(message, error.stat, options); + } else { + throw new FileOperationError(message, toFileOperationResult(error), options); + } } } @@ -594,7 +605,7 @@ export class FileService extends Disposable implements IFileService { // Throw if file not modified since (unless disabled) if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) { - throw new FileOperationError(localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, options); + throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options); } // Throw if file is too large to load @@ -912,14 +923,22 @@ export class FileService extends Disposable implements IFileService { } // Validate delete - const exists = await this.exists(resource); - if (!exists) { + let stat: IStat | undefined = undefined; + try { + stat = await provider.stat(resource); + } catch (error) { + // Handled later + } + + if (stat) { + this.throwIfFileIsReadonly(resource, stat); + } else { throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete non-existing file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Validate recursive const recursive = !!options?.recursive; - if (!recursive && exists) { + if (!recursive) { const stat = await this.resolve(resource); if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) { throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource))); @@ -1227,6 +1246,12 @@ export class FileService extends Disposable implements IFileService { return provider; } + private throwIfFileIsReadonly(resource: URI, stat: IStat): void { + if ((stat.permissions ?? 0) & FilePermission.Readonly) { + throw new FileOperationError(localize('err.readonly', "Unable to modify readonly file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED); + } + } + private resourceForError(resource: URI): string { if (resource.scheme === Schemas.file) { return resource.fsPath; diff --git a/lib/vscode/src/vs/platform/files/common/files.ts b/lib/vscode/src/vs/platform/files/common/files.ts index f2543a85c6f7..774f4e22c000 100644 --- a/lib/vscode/src/vs/platform/files/common/files.ts +++ b/lib/vscode/src/vs/platform/files/common/files.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isNumber, isUndefinedOrNull } from 'vs/base/common/types'; +import { isNumber } from 'vs/base/common/types'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ReadableStreamEvents } from 'vs/base/common/stream'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -315,6 +315,14 @@ export enum FileType { SymbolicLink = 64 } +export enum FilePermission { + + /** + * File is readonly. + */ + Readonly = 1 +} + export interface IStat { /** @@ -335,7 +343,12 @@ export interface IStat { /** * The size of the file in bytes. */ - size: number; + readonly size: number; + + /** + * The file permissions. + */ + readonly permissions?: FilePermission; } export interface IWatchOptions { @@ -400,7 +413,7 @@ export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event; - readonly onDidErrorOccur?: Event; // TODO@bpasero remove once file watchers are solid + readonly onDidErrorOccur?: Event; readonly onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; @@ -475,7 +488,7 @@ export enum FileSystemProviderErrorCode { export class FileSystemProviderError extends Error { - constructor(message: string, public readonly code: FileSystemProviderErrorCode) { + constructor(message: string, readonly code: FileSystemProviderErrorCode) { super(message); } } @@ -592,7 +605,7 @@ export class FileOperationEvent { constructor(resource: URI, operation: FileOperation.DELETE); constructor(resource: URI, operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, target: IFileStatWithMetadata); - constructor(public readonly resource: URI, public readonly operation: FileOperation, public readonly target?: IFileStatWithMetadata) { } + constructor(readonly resource: URI, readonly operation: FileOperation, readonly target?: IFileStatWithMetadata) { } isOperation(operation: FileOperation.DELETE): boolean; isOperation(operation: FileOperation.MOVE | FileOperation.COPY | FileOperation.CREATE): this is { readonly target: IFileStatWithMetadata }; @@ -868,6 +881,11 @@ interface IBaseStat { * it is optional. */ readonly etag?: string; + + /** + * The file is read-only. + */ + readonly readonly?: boolean; } export interface IBaseStatWithMetadata extends Required { } @@ -906,6 +924,7 @@ export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata readonly ctime: number; readonly etag: string; readonly size: number; + readonly readonly: boolean; readonly children?: IFileStatWithMetadata[]; } @@ -956,7 +975,7 @@ export interface IReadFileOptions extends IBaseReadFileOptions { * * Typically you should not need to use this flag but if * for example you are quickly reading a file right after - * a file event occured and the file changes a lot, there + * a file event occurred and the file changes a lot, there * is a chance that a read returns an empty or partial file * because a pending write has not finished yet. * @@ -1019,12 +1038,23 @@ export interface ICreateFileOptions { } export class FileOperationError extends Error { - constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions) { + constructor( + message: string, + readonly fileOperationResult: FileOperationResult, + readonly options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions + ) { super(message); } +} + +export class NotModifiedSinceFileOperationError extends FileOperationError { - static isFileOperationError(obj: unknown): obj is FileOperationError { - return obj instanceof Error && !isUndefinedOrNull((obj as FileOperationError).fileOperationResult); + constructor( + message: string, + readonly stat: IFileStatWithMetadata, + options?: IReadFileOptions + ) { + super(message, FileOperationResult.FILE_NOT_MODIFIED_SINCE, options); } } diff --git a/lib/vscode/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts b/lib/vscode/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts index 9cdde03ee714..635d995865ce 100644 --- a/lib/vscode/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts +++ b/lib/vscode/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider'; -import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; -import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; +import { isWindows } from 'vs/base/common/platform'; import { basename } from 'vs/base/common/path'; +import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider'; +import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; @@ -34,8 +34,11 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { return super.doDelete(filePath, opts); } - const result = await this.nativeHostService.moveItemToTrash(filePath); - if (!result) { + try { + await this.nativeHostService.moveItemToTrash(filePath); + } catch (error) { + this.logService.error(error); + throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath))); } } diff --git a/lib/vscode/src/vs/platform/files/node/diskFileSystemProvider.ts b/lib/vscode/src/vs/platform/files/node/diskFileSystemProvider.ts index ff111367d43e..c950ddc09d45 100644 --- a/lib/vscode/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/lib/vscode/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { open, close, read, write, fdatasync, Stats, promises } from 'fs'; -import { promisify } from 'util'; +import { Stats } from 'fs'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs'; +import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent, Promises } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; @@ -47,7 +46,7 @@ export class DiskFileSystemProvider extends Disposable implements private readonly BUFFER_SIZE = this.options?.bufferSize || 64 * 1024; constructor( - private readonly logService: ILogService, + protected readonly logService: ILogService, private readonly options?: IDiskFileSystemProviderOptions ) { super(); @@ -152,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const filePath = this.toFilePath(resource); - return await promises.readFile(filePath); + return await Promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -216,7 +215,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const { stat } = await SymlinkSupport.stat(filePath); if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) { - await promises.chmod(filePath, stat.mode | 0o200); + await Promises.chmod(filePath, stat.mode | 0o200); } } catch (error) { this.logService.trace(error); // ignore any errors here and try to just write @@ -232,7 +231,7 @@ export class DiskFileSystemProvider extends Disposable implements // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await promises.truncate(filePath, 0); + await Promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; @@ -256,7 +255,7 @@ export class DiskFileSystemProvider extends Disposable implements flags = 'r'; } - const handle = await promisify(open)(filePath, flags); + const handle = await Promises.open(filePath, flags); // remember this handle to track file position of the handle // we init the position to 0 since the file descriptor was @@ -290,7 +289,7 @@ export class DiskFileSystemProvider extends Disposable implements // to flush the contents to disk if possible. if (this.writeHandles.delete(fd) && this.canFlush) { try { - await promisify(fdatasync)(fd); + await Promises.fdatasync(fd); } catch (error) { // In some exotic setups it is well possible that node fails to sync // In that case we disable flushing and log the error to our logger @@ -299,7 +298,7 @@ export class DiskFileSystemProvider extends Disposable implements } } - return await promisify(close)(fd); + return await Promises.close(fd); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -310,7 +309,7 @@ export class DiskFileSystemProvider extends Disposable implements let bytesRead: number | null = null; try { - const result = await promisify(read)(fd, data, offset, length, normalizedPos); + const result = await Promises.read(fd, data, offset, length, normalizedPos); if (typeof result === 'number') { bytesRead = result; // node.d.ts fail @@ -396,7 +395,7 @@ export class DiskFileSystemProvider extends Disposable implements let bytesWritten: number | null = null; try { - const result = await promisify(write)(fd, data, offset, length, normalizedPos); + const result = await Promises.write(fd, data, offset, length, normalizedPos); if (typeof result === 'number') { bytesWritten = result; // node.d.ts fail @@ -418,7 +417,7 @@ export class DiskFileSystemProvider extends Disposable implements async mkdir(resource: URI): Promise { try { - await promises.mkdir(this.toFilePath(resource)); + await Promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -438,7 +437,7 @@ export class DiskFileSystemProvider extends Disposable implements if (opts.recursive) { await rimraf(filePath, RimRafMode.MOVE); } else { - await promises.unlink(filePath); + await Promises.unlink(filePath); } } @@ -542,7 +541,7 @@ export class DiskFileSystemProvider extends Disposable implements return this.watchRecursive(resource, opts.excludes); } - return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases + return this.watchNonRecursive(resource); } private watchRecursive(resource: URI, excludes: string[]): IDisposable { diff --git a/lib/vscode/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/lib/vscode/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index ebdd45d8622f..00c4d13a547b 100644 --- a/lib/vscode/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/lib/vscode/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -10,10 +10,10 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { flakySuite, getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { join, basename, dirname, posix } from 'vs/base/common/path'; -import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs'; +import { copy, Promises, rimraf, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs'; -import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions } from 'vs/platform/files/common/files'; +import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; +import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions, FilePermission, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -56,6 +56,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { private invalidStatSize: boolean = false; private smallStatSize: boolean = false; + private readonly: boolean = false; private _testCapabilities!: FileSystemProviderCapabilities; override get capabilities(): FileSystemProviderCapabilities { @@ -88,13 +89,19 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { this.smallStatSize = enabled; } + setReadonly(readonly: boolean): void { + this.readonly = readonly; + } + override async stat(resource: URI): Promise { const res = await super.stat(resource); if (this.invalidStatSize) { - res.size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909 + (res as any).size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909 } else if (this.smallStatSize) { - res.size = 1; + (res as any).size = 1; + } else if (this.readonly) { + (res as any).permissions = FilePermission.Readonly; } return res; @@ -213,6 +220,7 @@ flakySuite('Disk File Service', function () { assert.strictEqual(resolved.name, 'index.html'); assert.strictEqual(resolved.isFile, true); assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.readonly, false); assert.strictEqual(resolved.isSymbolicLink, false); assert.strictEqual(resolved.resource.toString(), resource.toString()); assert.strictEqual(resolved.children, undefined); @@ -233,6 +241,7 @@ flakySuite('Disk File Service', function () { assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); + assert.strictEqual(result.readonly, false); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); assert.strictEqual(result.children!.length, testsElements.length); @@ -408,7 +417,7 @@ flakySuite('Disk File Service', function () { test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); + await Promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); assert.strictEqual(resolved.children!.length, 4); @@ -418,7 +427,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); assert.strictEqual(resolved.isDirectory, false); @@ -426,7 +435,7 @@ flakySuite('Disk File Service', function () { }); test('resolve - symbolic link pointing to non-existing file does not break', async () => { - await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); + await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); assert.strictEqual(resolved.isDirectory, true); @@ -477,7 +486,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(target.fsPath, link.fsPath); + await Promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); @@ -499,7 +508,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); - await promises.symlink(target.fsPath, link.fsPath); + await Promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); @@ -1482,6 +1491,7 @@ flakySuite('Disk File Service', function () { assert.ok(error); assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); + assert.ok(error instanceof NotModifiedSinceFileOperationError && error.stat); assert.strictEqual(fileProvider.totalBytesRead, 0); } @@ -1592,7 +1602,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { const link = URI.file(join(testDir, 'small.js-link')); - await promises.symlink(join(testDir, 'small.js'), link.fsPath); + await Promises.symlink(join(testDir, 'small.js'), link.fsPath); let error: FileOperationError | undefined = undefined; try { @@ -1928,8 +1938,8 @@ flakySuite('Disk File Service', function () { await service.writeFile(lockedFile, VSBuffer.fromString('Locked File')); - const stats = await promises.stat(lockedFile.fsPath); - await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); + const stats = await Promises.stat(lockedFile.fsPath); + await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); let error; const newContent = 'Updates to locked file'; @@ -2091,7 +2101,7 @@ flakySuite('Disk File Service', function () { (runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => { const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); + await Promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); @@ -2219,7 +2229,7 @@ flakySuite('Disk File Service', function () { (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); + await Promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); @@ -2390,4 +2400,32 @@ flakySuite('Disk File Service', function () { await fileProvider.close(fdWrite); await fileProvider.close(fdRead); }); + + test('readonly - is handled properly for a single resource', async () => { + fileProvider.setReadonly(true); + + const resource = URI.file(join(testDir, 'index.html')); + + const resolveResult = await service.resolve(resource); + assert.strictEqual(resolveResult.readonly, true); + + const readResult = await service.readFile(resource); + assert.strictEqual(readResult.readonly, true); + + let writeFileError: Error | undefined = undefined; + try { + await service.writeFile(resource, VSBuffer.fromString('Hello Test')); + } catch (error) { + writeFileError = error; + } + assert.ok(writeFileError); + + let deleteFileError: Error | undefined = undefined; + try { + await service.del(resource); + } catch (error) { + deleteFileError = error; + } + assert.ok(deleteFileError); + }); }); diff --git a/lib/vscode/src/vs/platform/issue/common/issue.ts b/lib/vscode/src/vs/platform/issue/common/issue.ts index 58e1883dcf34..0f398932e6d8 100644 --- a/lib/vscode/src/vs/platform/issue/common/issue.ts +++ b/lib/vscode/src/vs/platform/issue/common/issue.ts @@ -58,6 +58,7 @@ export interface IssueReporterData extends WindowData { issueType?: IssueType; extensionId?: string; experiments?: string; + restrictedMode: boolean; githubAccessToken: string; readonly issueTitle?: string; readonly issueBody?: string; @@ -70,8 +71,14 @@ export interface ISettingSearchResult { } export interface ProcessExplorerStyles extends WindowStyles { - hoverBackground?: string; - hoverForeground?: string; + listHoverBackground?: string; + listHoverForeground?: string; + listFocusBackground?: string; + listFocusForeground?: string; + listFocusOutline?: string; + listActiveSelectionBackground?: string; + listActiveSelectionForeground?: string; + listHoverOutline?: string; } export interface ProcessExplorerData extends WindowData { diff --git a/lib/vscode/src/vs/platform/issue/electron-main/issueMainService.ts b/lib/vscode/src/vs/platform/issue/electron-main/issueMainService.ts index e6be46456109..1eb0ac224bc6 100644 --- a/lib/vscode/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/lib/vscode/src/vs/platform/issue/electron-main/issueMainService.ts @@ -25,6 +25,13 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; export const IIssueMainService = createDecorator('issueMainService'); +interface IBrowserWindowOptions { + backgroundColor: string | undefined; + title: string; + zoomLevel: number; + alwaysOnTop: boolean; +} + export interface IIssueMainService extends ICommonIssueService { } export class IssueMainService implements ICommonIssueService { @@ -189,7 +196,12 @@ export class IssueMainService implements ICommonIssueService { const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl()); const position = this.getWindowPosition(this.issueReporterParentWindow, 700, 800); - this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, data.styles.backgroundColor, localize('issueReporter', "Issue Reporter"), data.zoomLevel); + this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('issueReporter', "Issue Reporter"), + zoomLevel: data.zoomLevel, + alwaysOnTop: false + }); // Store into config object URL issueReporterWindowConfigUrl.update({ @@ -239,7 +251,12 @@ export class IssueMainService implements ICommonIssueService { const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); const position = this.getWindowPosition(this.processExplorerParentWindow, 800, 500); - this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, data.styles.backgroundColor, localize('processExplorer', "Process Explorer"), data.zoomLevel); + this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + zoomLevel: data.zoomLevel, + alwaysOnTop: true + }); // Store into config object URL processExplorerWindowConfigUrl.update({ @@ -273,7 +290,7 @@ export class IssueMainService implements ICommonIssueService { this.processExplorerWindow?.focus(); } - private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, backgroundColor: string | undefined, title: string, zoomLevel: number): BrowserWindow { + private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions): BrowserWindow { const window = new BrowserWindow({ fullscreen: false, skipTaskbar: true, @@ -284,8 +301,8 @@ export class IssueMainService implements ICommonIssueService { minHeight: 200, x: position.x, y: position.y, - title, - backgroundColor: backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR, + title: options.title, + backgroundColor: options.backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`, '--context-isolation' /* TODO@bpasero: Use process.contextIsolateed when 13-x-y is adopted (https://github.com/electron/electron/pull/28030) */], @@ -294,10 +311,11 @@ export class IssueMainService implements ICommonIssueService { enableRemoteModule: false, spellcheck: false, nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(zoomLevel), + zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), sandbox: true, - contextIsolation: true - } + contextIsolation: true, + }, + alwaysOnTop: options.alwaysOnTop }); window.setMenuBarVisibility(false); diff --git a/lib/vscode/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/lib/vscode/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 689431ec33e9..6de8a0e61e58 100644 --- a/lib/vscode/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/lib/vscode/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -107,8 +107,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK ); } - public lookupKeybinding(commandId: string): ResolvedKeybinding | undefined { - const result = this._getResolver().lookupPrimaryKeybinding(commandId); + public lookupKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybinding | undefined { + const result = this._getResolver().lookupPrimaryKeybinding(commandId, context); if (!result) { return undefined; } diff --git a/lib/vscode/src/vs/platform/keybinding/common/keybinding.ts b/lib/vscode/src/vs/platform/keybinding/common/keybinding.ts index a88b35753de5..7c7b4b2bb1ab 100644 --- a/lib/vscode/src/vs/platform/keybinding/common/keybinding.ts +++ b/lib/vscode/src/vs/platform/keybinding/common/keybinding.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Keybinding, KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -85,7 +85,7 @@ export interface IKeybindingService { * Look up the preferred (last defined) keybinding for a command. * @returns The preferred keybinding or null if the command is not bound. */ - lookupKeybinding(commandId: string): ResolvedKeybinding | undefined; + lookupKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybinding | undefined; getDefaultKeybindingsContent(): string; diff --git a/lib/vscode/src/vs/platform/keybinding/common/keybindingResolver.ts b/lib/vscode/src/vs/platform/keybinding/common/keybindingResolver.ts index ca31e62e54e0..753e607ef07a 100644 --- a/lib/vscode/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/lib/vscode/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; export interface IResolveResult { @@ -247,13 +247,15 @@ export class KeybindingResolver { return result; } - public lookupPrimaryKeybinding(commandId: string): ResolvedKeybindingItem | null { + public lookupPrimaryKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybindingItem | null { let items = this._lookupMap.get(commandId); if (typeof items === 'undefined' || items.length === 0) { return null; } - return items[items.length - 1]; + const itemMatchingContext = context && + Array.from(items).reverse().find(item => context.contextMatchesRules(item.when)); + return itemMatchingContext ?? items[items.length - 1]; } public resolve(context: IContext, currentChord: string | null, keypress: string): IResolveResult | null { diff --git a/lib/vscode/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/lib/vscode/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index c5d69ba4f5fa..2d18175d2658 100644 --- a/lib/vscode/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/lib/vscode/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -5,7 +5,7 @@ import { ipcMain, app, BrowserWindow } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { Event, Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; @@ -115,12 +115,12 @@ export interface ILifecycleMainService { /** * Restart the application with optional arguments (CLI). All lifecycle event handlers are triggered. */ - relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void; + relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise; /** * Shutdown the application normally. All lifecycle event handlers are triggered. */ - quit(fromUpdate?: boolean): Promise; + quit(willRestart?: boolean): Promise; /** * Forcefully shutdown the application. No livecycle event handlers are triggered. @@ -158,7 +158,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe declare readonly _serviceBrand: undefined; - private static readonly QUIT_FROM_RESTART_MARKER = 'quit.from.restart'; // use a marker to find out if the session was restarted + private static readonly QUIT_AND_RESTART_KEY = 'lifecycle.quitAndRestart'; private readonly _onBeforeShutdown = this._register(new Emitter()); readonly onBeforeShutdown = this._onBeforeShutdown.event; @@ -188,28 +188,29 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private oneTimeListenerTokenGenerator = 0; private windowCounter = 0; - private pendingQuitPromise: Promise | null = null; - private pendingQuitPromiseResolve: { (veto: boolean): void } | null = null; + private pendingQuitPromise: Promise | undefined = undefined; + private pendingQuitPromiseResolve: { (veto: boolean): void } | undefined = undefined; - private pendingWillShutdownPromise: Promise | null = null; + private pendingWillShutdownPromise: Promise | undefined = undefined; private readonly phaseWhen = new Map(); constructor( @ILogService private readonly logService: ILogService, - @IStateService private readonly stateService: IStateService + @IStateMainService private readonly stateMainService: IStateMainService ) { super(); - this.handleRestarted(); + this.resolveRestarted(); this.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); } - private handleRestarted(): void { - this._wasRestarted = !!this.stateService.getItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER); + private resolveRestarted(): void { + this._wasRestarted = !!this.stateMainService.getItem(LifecycleMainService.QUIT_AND_RESTART_KEY); if (this._wasRestarted) { - this.stateService.removeItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER); // remove the marker right after if found + // remove the marker right after if found + this.stateMainService.removeItem(LifecycleMainService.QUIT_AND_RESTART_KEY); } } @@ -294,7 +295,23 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } }); - this.pendingWillShutdownPromise = Promises.settled(joiners).then(() => undefined, err => this.logService.error(err)); + this.pendingWillShutdownPromise = (async () => { + + // Settle all shutdown event joiners + try { + await Promises.settled(joiners); + } catch (error) { + this.logService.error(error); + } + + // Then, always make sure at the end + // the state service is flushed. + try { + await this.stateMainService.close(); + } catch (error) { + this.logService.error(error); + } + })(); return this.pendingWillShutdownPromise; } @@ -454,8 +471,8 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private resolvePendingQuitPromise(veto: boolean): void { if (this.pendingQuitPromiseResolve) { this.pendingQuitPromiseResolve(veto); - this.pendingQuitPromiseResolve = null; - this.pendingQuitPromise = null; + this.pendingQuitPromiseResolve = undefined; + this.pendingQuitPromise = undefined; } } @@ -502,16 +519,16 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe }); } - quit(fromUpdate?: boolean): Promise { + quit(willRestart?: boolean): Promise { if (this.pendingQuitPromise) { return this.pendingQuitPromise; } - this.logService.trace(`Lifecycle#quit() - from update: ${fromUpdate}`); + this.logService.trace(`Lifecycle#quit() - will restart: ${willRestart}`); - // Remember the reason for quit was to restart - if (fromUpdate) { - this.stateService.setItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER, true); + // Remember if we are about to restart + if (willRestart) { + this.stateMainService.setItem(LifecycleMainService.QUIT_AND_RESTART_KEY, true); } this.pendingQuitPromise = new Promise(resolve => { @@ -528,7 +545,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe return this.pendingQuitPromise; } - relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void { + async relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise { this.logService.trace('Lifecycle#relaunch()'); const args = process.argv.slice(1); @@ -545,37 +562,34 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } } - let quitVetoed = false; - app.once('quit', () => { - if (!quitVetoed) { - - // Remember the reason for quit was to restart - this.stateService.setItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER, true); - - // Windows: we are about to restart and as such we need to restore the original - // current working directory we had on startup to get the exact same startup - // behaviour. As such, we briefly change back to that directory and then when - // Code starts it will set it back to the installation directory again. - try { - if (isWindows) { - const currentWorkingDir = cwd(); - if (currentWorkingDir !== process.cwd()) { - process.chdir(currentWorkingDir); - } + const quitListener = () => { + // Windows: we are about to restart and as such we need to restore the original + // current working directory we had on startup to get the exact same startup + // behaviour. As such, we briefly change back to that directory and then when + // Code starts it will set it back to the installation directory again. + try { + if (isWindows) { + const currentWorkingDir = cwd(); + if (currentWorkingDir !== process.cwd()) { + process.chdir(currentWorkingDir); } - } catch (err) { - this.logService.error(err); } - - // relaunch after we are sure there is no veto - this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()'); - app.relaunch({ args }); + } catch (err) { + this.logService.error(err); } - }); + + // relaunch after we are sure there is no veto + this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()'); + app.relaunch({ args }); + }; + app.once('quit', quitListener); // app.relaunch() does not quit automatically, so we quit first, // check for vetoes and then relaunch from the app.on('quit') event - this.quit().then(veto => quitVetoed = veto); + const veto = await this.quit(true /* will restart */); + if (veto) { + app.removeListener('quit', quitListener); + } } async kill(code?: number): Promise { diff --git a/lib/vscode/src/vs/platform/list/browser/listService.ts b/lib/vscode/src/vs/platform/list/browser/listService.ts index 6b2f6b35019e..648e319d7d63 100644 --- a/lib/vscode/src/vs/platform/list/browser/listService.ts +++ b/lib/vscode/src/vs/platform/list/browser/listService.ts @@ -183,7 +183,7 @@ function toWorkbenchListOptions(options: IListOptions, configurationServic } }; - result.smoothScrolling = configurationService.getValue(listSmoothScrolling); + result.smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); return [result, disposables]; } @@ -221,7 +221,7 @@ export class WorkbenchList extends List { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); super(user, container, delegate, renderers, @@ -282,11 +282,11 @@ export class WorkbenchList extends List { let options: IListOptionsUpdate = {}; if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); options = { ...options, horizontalScrolling }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); options = { ...options, smoothScrolling }; } if (Object.keys(options).length > 0) { @@ -348,7 +348,7 @@ export class WorkbenchPagedList extends PagedList { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); super(user, container, delegate, renderers, { @@ -394,11 +394,11 @@ export class WorkbenchPagedList extends PagedList { let options: IListOptionsUpdate = {}; if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); options = { ...options, horizontalScrolling }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); options = { ...options, smoothScrolling }; } if (Object.keys(options).length > 0) { @@ -469,7 +469,7 @@ export class WorkbenchTable extends Table { @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); super(user, container, delegate, columns, renderers, @@ -531,11 +531,11 @@ export class WorkbenchTable extends Table { let options: IListOptionsUpdate = {}; if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); options = { ...options, horizontalScrolling }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); options = { ...options, smoothScrolling }; } if (Object.keys(options).length > 0) { @@ -999,10 +999,10 @@ function workbenchTreeDataPreamble { // give priority to the context key value to disable this completely - let automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + let automaticKeyboardNavigation = Boolean(contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey)); if (automaticKeyboardNavigation) { - automaticKeyboardNavigation = configurationService.getValue(automaticKeyboardNavigationSettingKey); + automaticKeyboardNavigation = Boolean(configurationService.getValue(automaticKeyboardNavigationSettingKey)); } return automaticKeyboardNavigation; @@ -1010,7 +1010,7 @@ function workbenchTreeDataPreamble(keyboardNavigationSettingKey); - const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); const additionalScrollHeight = options.additionalScrollHeight; @@ -1023,7 +1023,7 @@ function workbenchTreeDataPreamble(treeIndentKey), renderIndentGuides: configurationService.getValue(treeRenderIndentGuidesKey), - smoothScrolling: configurationService.getValue(listSmoothScrolling), + smoothScrolling: Boolean(configurationService.getValue(listSmoothScrolling)), automaticKeyboardNavigation: getAutomaticKeyboardNavigation(), simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter', @@ -1120,7 +1120,7 @@ class WorkbenchTreeInternals { newOptions = { ...newOptions, renderIndentGuides }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = configurationService.getValue(listSmoothScrolling); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); newOptions = { ...newOptions, smoothScrolling }; } if (e.affectsConfiguration(keyboardNavigationSettingKey)) { @@ -1130,7 +1130,7 @@ class WorkbenchTreeInternals { newOptions = { ...newOptions, automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }; } if (e.affectsConfiguration(horizontalScrollingKey) && options.horizontalScrolling === undefined) { - const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); newOptions = { ...newOptions, horizontalScrolling }; } if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) { @@ -1171,20 +1171,20 @@ class WorkbenchTreeInternals { const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ - 'id': 'workbench', - 'order': 7, - 'title': localize('workbenchConfigurationTitle', "Workbench"), - 'type': 'object', - 'properties': { + id: 'workbench', + order: 7, + title: localize('workbenchConfigurationTitle', "Workbench"), + type: 'object', + properties: { [multiSelectModifierSettingKey]: { - 'type': 'string', - 'enum': ['ctrlCmd', 'alt'], - 'enumDescriptions': [ + type: 'string', + enum: ['ctrlCmd', 'alt'], + enumDescriptions: [ localize('multiSelectModifier.ctrlCmd', "Maps to `Control` on Windows and Linux and to `Command` on macOS."), localize('multiSelectModifier.alt', "Maps to `Alt` on Windows and Linux and to `Option` on macOS.") ], - 'default': 'ctrlCmd', - 'description': localize({ + default: 'ctrlCmd', + description: localize({ key: 'multiSelectModifier', comment: [ '- `ctrlCmd` refers to a value the setting can take and should not be localized.', @@ -1193,25 +1193,25 @@ configurationRegistry.registerConfiguration({ }, "The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (for example in the explorer, open editors and scm view). The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.") }, [openModeSettingKey]: { - 'type': 'string', - 'enum': ['singleClick', 'doubleClick'], - 'default': 'singleClick', - 'description': localize({ + type: 'string', + enum: ['singleClick', 'doubleClick'], + default: 'singleClick', + description: localize({ key: 'openModeModifier', comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.'] }, "Controls how to open items in trees and lists using the mouse (if supported). Note that some trees and lists might choose to ignore this setting if it is not applicable.") }, [horizontalScrollingKey]: { - 'type': 'boolean', - 'default': false, - 'description': localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.") + type: 'boolean', + default: false, + description: localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.") }, [treeIndentKey]: { - 'type': 'number', - 'default': 8, + type: 'number', + default: 8, minimum: 0, maximum: 40, - 'description': localize('tree indent setting', "Controls tree indentation in pixels.") + description: localize('tree indent setting', "Controls tree indentation in pixels.") }, [treeRenderIndentGuidesKey]: { type: 'string', @@ -1225,19 +1225,19 @@ configurationRegistry.registerConfiguration({ description: localize('list smoothScrolling setting', "Controls whether lists and trees have smooth scrolling."), }, [keyboardNavigationSettingKey]: { - 'type': 'string', - 'enum': ['simple', 'highlight', 'filter'], - 'enumDescriptions': [ + type: 'string', + enum: ['simple', 'highlight', 'filter'], + enumDescriptions: [ localize('keyboardNavigationSettingKey.simple', "Simple keyboard navigation focuses elements which match the keyboard input. Matching is done only on prefixes."), localize('keyboardNavigationSettingKey.highlight', "Highlight keyboard navigation highlights elements which match the keyboard input. Further up and down navigation will traverse only the highlighted elements."), localize('keyboardNavigationSettingKey.filter', "Filter keyboard navigation will filter out and hide all the elements which do not match the keyboard input.") ], - 'default': 'highlight', - 'description': localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.") + default: 'highlight', + description: localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.") }, [automaticKeyboardNavigationSettingKey]: { - 'type': 'boolean', - 'default': true, + type: 'boolean', + default: true, markdownDescription: localize('automatic keyboard navigation setting', "Controls whether keyboard navigation in lists and trees is automatically triggered simply by typing. If set to `false`, keyboard navigation is only triggered when executing the `list.toggleKeyboardNavigation` command, for which you can assign a keyboard shortcut.") }, [treeExpandMode]: { diff --git a/lib/vscode/src/vs/platform/localizations/node/localizations.ts b/lib/vscode/src/vs/platform/localizations/node/localizations.ts index 02f3df5a9111..e1ef23c7b367 100644 --- a/lib/vscode/src/vs/platform/localizations/node/localizations.ts +++ b/lib/vscode/src/vs/platform/localizations/node/localizations.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { writeFile } from 'vs/base/node/pfs'; -import { promises } from 'fs'; +import { Promises, writeFile } from 'vs/base/node/pfs'; import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -158,7 +157,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return promises.readFile(this.languagePacksFilePath, 'utf8') + return Promises.readFile(this.languagePacksFilePath, 'utf8') .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) diff --git a/lib/vscode/src/vs/platform/log/node/spdlogLog.ts b/lib/vscode/src/vs/platform/log/node/spdlogLog.ts index b97064e29f74..a57db48bc31d 100644 --- a/lib/vscode/src/vs/platform/log/node/spdlogLog.ts +++ b/lib/vscode/src/vs/platform/log/node/spdlogLog.ts @@ -7,20 +7,21 @@ import { LogLevel, ILogger, AbstractMessageLogger } from 'vs/platform/log/common import * as spdlog from 'spdlog'; import { ByteSize } from 'vs/platform/files/common/files'; -async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise { +async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise { // Do not crash if spdlog cannot be loaded try { const _spdlog = await import('spdlog'); - _spdlog.setAsyncMode(8192, 500); - return _spdlog.createRotatingLoggerAsync(name, logfilePath, filesize, filecount); + _spdlog.setFlushOn(LogLevel.Trace); + return _spdlog.createAsyncRotatingLogger(name, logfilePath, filesize, filecount); } catch (e) { console.error(e); } return null; } -export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): spdlog.RotatingLogger { +export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): Promise { const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); + _spdlog.setFlushOn(LogLevel.Trace); return _spdlog.createRotatingLogger(name, filename, filesize, filecount); } @@ -29,7 +30,7 @@ interface ILog { message: string; } -function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string): void { +function log(logger: spdlog.Logger, level: LogLevel, message: string): void { switch (level) { case LogLevel.Trace: logger.trace(message); break; case LogLevel.Debug: logger.debug(message); break; @@ -45,7 +46,7 @@ export class SpdLogLogger extends AbstractMessageLogger implements ILogger { private buffer: ILog[] = []; private readonly _loggerCreationPromise: Promise; - private _logger: spdlog.RotatingLogger | undefined; + private _logger: spdlog.Logger | undefined; constructor( private readonly name: string, diff --git a/lib/vscode/src/vs/platform/menubar/electron-main/menubar.ts b/lib/vscode/src/vs/platform/menubar/electron-main/menubar.ts index f9ab5a1e3e56..7f8f5b76d601 100644 --- a/lib/vscode/src/vs/platform/menubar/electron-main/menubar.ts +++ b/lib/vscode/src/vs/platform/menubar/electron-main/menubar.ts @@ -19,7 +19,7 @@ import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/ import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemRecentAction, IMenubarMenuRecentItemAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; @@ -70,7 +70,7 @@ export class Menubar { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @@ -100,7 +100,7 @@ export class Menubar { } private restoreCachedMenubarData() { - const menubarData = this.stateService.getItem(Menubar.lastKnownMenubarStorageKey); + const menubarData = this.stateMainService.getItem(Menubar.lastKnownMenubarStorageKey); if (menubarData) { if (menubarData.menus) { this.menubarMenus = menubarData.menus; @@ -200,7 +200,7 @@ export class Menubar { this.keybindings = menubarData.keybindings; // Save off new menu and keybindings - this.stateService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData); + this.stateMainService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData); this.scheduleUpdateMenu(); } @@ -285,53 +285,60 @@ export class Menubar { } // File - const fileMenu = new Menu(); - const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu }); - - this.setMenuById(fileMenu, 'File'); - menubar.append(fileMenuItem); + if (this.shouldDrawMenu('File')) { + const fileMenu = new Menu(); + const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu }); + this.setMenuById(fileMenu, 'File'); + menubar.append(fileMenuItem); + } // Edit - const editMenu = new Menu(); - const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu }); - - this.setMenuById(editMenu, 'Edit'); - menubar.append(editMenuItem); + if (this.shouldDrawMenu('Edit')) { + const editMenu = new Menu(); + const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu }); + this.setMenuById(editMenu, 'Edit'); + menubar.append(editMenuItem); + } // Selection - const selectionMenu = new Menu(); - const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu }); - - this.setMenuById(selectionMenu, 'Selection'); - menubar.append(selectionMenuItem); + if (this.shouldDrawMenu('Selection')) { + const selectionMenu = new Menu(); + const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu }); + this.setMenuById(selectionMenu, 'Selection'); + menubar.append(selectionMenuItem); + } // View - const viewMenu = new Menu(); - const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu }); - - this.setMenuById(viewMenu, 'View'); - menubar.append(viewMenuItem); + if (this.shouldDrawMenu('View')) { + const viewMenu = new Menu(); + const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu }); + this.setMenuById(viewMenu, 'View'); + menubar.append(viewMenuItem); + } // Go - const gotoMenu = new Menu(); - const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu }); - - this.setMenuById(gotoMenu, 'Go'); - menubar.append(gotoMenuItem); + if (this.shouldDrawMenu('Go')) { + const gotoMenu = new Menu(); + const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu }); + this.setMenuById(gotoMenu, 'Go'); + menubar.append(gotoMenuItem); + } // Debug - const debugMenu = new Menu(); - const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu }); - - this.setMenuById(debugMenu, 'Run'); - menubar.append(debugMenuItem); + if (this.shouldDrawMenu('Run')) { + const debugMenu = new Menu(); + const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu }); + this.setMenuById(debugMenu, 'Run'); + menubar.append(debugMenuItem); + } // Terminal - const terminalMenu = new Menu(); - const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); - - this.setMenuById(terminalMenu, 'Terminal'); - menubar.append(terminalMenuItem); + if (this.shouldDrawMenu('Terminal')) { + const terminalMenu = new Menu(); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); + this.setMenuById(terminalMenu, 'Terminal'); + menubar.append(terminalMenuItem); + } // Mac: Window let macWindowMenuItem: MenuItem | undefined; @@ -346,11 +353,12 @@ export class Menubar { } // Help - const helpMenu = new Menu(); - const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' }); - - this.setMenuById(helpMenu, 'Help'); - menubar.append(helpMenuItem); + if (this.shouldDrawMenu('Help')) { + const helpMenu = new Menu(); + const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' }); + this.setMenuById(helpMenu, 'Help'); + menubar.append(helpMenuItem); + } if (menubar.items && menubar.items.length > 0) { Menu.setApplicationMenu(menubar); diff --git a/lib/vscode/src/vs/platform/native/common/native.ts b/lib/vscode/src/vs/platform/native/common/native.ts index dfe4994a11aa..a4133673c0ee 100644 --- a/lib/vscode/src/vs/platform/native/common/native.ts +++ b/lib/vscode/src/vs/platform/native/common/native.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; -import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; +import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; @@ -72,6 +72,8 @@ export interface ICommonNativeHostService { setMinimumSize(width: number | undefined, height: number | undefined): Promise; + saveWindowSplash(splash: IPartsSplash): Promise; + /** * Make the window focused. * @@ -97,7 +99,7 @@ export interface ICommonNativeHostService { setRepresentedFilename(path: string): Promise; setDocumentEdited(edited: boolean): Promise; openExternal(url: string): Promise; - moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise; + moveItemToTrash(fullPath: string): Promise; isAdmin(): Promise; writeElevated(source: URI, target: URI, options?: { unlock?: boolean }): Promise; @@ -127,6 +129,10 @@ export interface ICommonNativeHostService { toggleWindowTabsBar(): Promise; updateTouchBar(items: ISerializableCommandAction[][]): Promise; + // macOS Shell command + installShellCommand(): Promise; + uninstallShellCommand(): Promise; + // Lifecycle notifyReady(): Promise relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise; diff --git a/lib/vscode/src/vs/platform/native/electron-main/nativeHostMainService.ts b/lib/vscode/src/vs/platform/native/electron-main/nativeHostMainService.ts index a0b1a3bd86fd..692db1dbaa90 100644 --- a/lib/vscode/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/lib/vscode/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -3,11 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { localize } from 'vs/nls'; +import { realpath } from 'vs/base/node/extpath'; import { Emitter, Event } from 'vs/base/common/event'; import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme, screen, Display } from 'electron'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; +import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; @@ -15,7 +19,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; -import { SymlinkSupport } from 'vs/base/node/pfs'; +import { exists, Promises, SymlinkSupport } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -23,11 +27,12 @@ import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { arch, totalmem, release, platform, type, loadavg, freemem, cpus } from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { ILogService } from 'vs/platform/log/common/log'; -import { dirname, join } from 'vs/base/common/path'; +import { dirname, join, resolve } from 'vs/base/common/path'; import { IProductService } from 'vs/platform/product/common/productService'; import { memoize } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -50,7 +55,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IThemeMainService private readonly themeMainService: IThemeMainService ) { super(); @@ -247,9 +253,106 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): Promise { + this.themeMainService.saveWindowSplash(windowId, splash); + } + //#endregion + //#region macOS Shell Command + + async installShellCommand(windowId: number | undefined): Promise { + const { source, target } = await this.getShellCommandLink(); + + // Only install unless already existing + try { + const { symbolicLink } = await SymlinkSupport.stat(source); + if (symbolicLink && !symbolicLink.dangling) { + const linkTargetRealPath = await realpath(source); + if (target === linkTargetRealPath) { + return; + } + } + + // Different source, delete it first + await Promises.unlink(source); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; // throw on any error but file not found + } + } + + try { + await Promises.symlink(target, source); + } catch (error) { + if (error.code !== 'EACCES' && error.code !== 'ENOENT') { + throw error; + } + + const { response } = await this.showMessageBox(windowId, { + type: 'info', + message: localize('warnEscalation', "{0} will now prompt with 'osascript' for Administrator privileges to install the shell command.", this.productService.nameShort), + buttons: [localize('ok', "OK"), localize('cancel', "Cancel")], + cancelId: 1 + }); + + if (response === 0 /* OK */) { + try { + const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'${target}\' \'${source}\'\\" with administrator privileges"`; + await promisify(exec)(command); + } catch (error) { + throw new Error(localize('cantCreateBinFolder', "Unable to install the shell command '{0}'.", source)); + } + } + } + } + + async uninstallShellCommand(windowId: number | undefined): Promise { + const { source } = await this.getShellCommandLink(); + + try { + await Promises.unlink(source); + } catch (error) { + switch (error.code) { + case 'EACCES': + const { response } = await this.showMessageBox(windowId, { + type: 'info', + message: localize('warnEscalationUninstall', "{0} will now prompt with 'osascript' for Administrator privileges to uninstall the shell command.", this.productService.nameShort), + buttons: [localize('ok', "OK"), localize('cancel', "Cancel")], + cancelId: 1 + }); + + if (response === 0 /* OK */) { + try { + const command = `osascript -e "do shell script \\"rm \'${source}\'\\" with administrator privileges"`; + await promisify(exec)(command); + } catch (error) { + throw new Error(localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", source)); + } + } + break; + case 'ENOENT': + break; // ignore file not found + default: + throw error; + } + } + } + + private async getShellCommandLink(): Promise<{ readonly source: string, readonly target: string }> { + const target = resolve(this.environmentMainService.appRoot, 'bin', 'code'); + const source = `/usr/local/bin/${this.productService.applicationName}`; + + // Ensure source exists + const sourceExists = await exists(target); + if (!sourceExists) { + throw new Error(localize('sourceMissing', "Unable to find shell script in '{0}'", target)); + } + + return { source, target }; + } + //#region Dialog async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise { @@ -376,8 +479,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir; } - async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { - return shell.moveItemToTrash(fullPath); + moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { + return shell.trashItem(fullPath); } async isAdmin(): Promise { @@ -599,9 +702,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain // Otherwise: normal quit else { - setTimeout(() => { - this.lifecycleMainService.quit(); - }, 10 /* delay to unwind callback stack (IPC) */); + this.lifecycleMainService.quit(); } } @@ -711,6 +812,27 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async setPassword(windowId: number | undefined, service: string, account: string, password: string): Promise { const keytar = await this.withKeytar(); + const MAX_SET_ATTEMPTS = 3; + + // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. + const setPasswordWithRetry = async (service: string, account: string, password: string) => { + let attempts = 0; + let error: any; + while (attempts < MAX_SET_ATTEMPTS) { + try { + await keytar.setPassword(service, account, password); + return; + } catch (e) { + error = e; + this.logService.warn('Error attempting to set a password: ', e); + attempts++; + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + + // throw last error + throw error; + }; if (isWindows && password.length > NativeHostMainService.MAX_PASSWORD_LENGTH) { let index = 0; @@ -726,12 +848,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain hasNextChunk: hasNextChunk }; - await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); + await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); chunk++; } } else { - await keytar.setPassword(service, account, password); + await setPasswordWithRetry(service, account, password); } this._onDidChangePassword.fire({ service, account }); diff --git a/lib/vscode/src/vs/platform/native/electron-sandbox/native.ts b/lib/vscode/src/vs/platform/native/electron-sandbox/native.ts index bb3f6bc715ce..01f1f94aaef3 100644 --- a/lib/vscode/src/vs/platform/native/electron-sandbox/native.ts +++ b/lib/vscode/src/vs/platform/native/electron-sandbox/native.ts @@ -12,7 +12,7 @@ export const INativeHostService = createDecorator('nativeHos * A set of methods specific to a native host, i.e. unsupported in web * environments. * - * @see `IHostService` for methods that can be used in native and web + * @see {@link IHostService} for methods that can be used in native and web * hosts. */ export interface INativeHostService extends ICommonNativeHostService { } diff --git a/lib/vscode/src/vs/platform/opener/browser/link.ts b/lib/vscode/src/vs/platform/opener/browser/link.ts index c3d3d7d02987..86a95a1cec47 100644 --- a/lib/vscode/src/vs/platform/opener/browser/link.ts +++ b/lib/vscode/src/vs/platform/opener/browser/link.ts @@ -6,11 +6,12 @@ import { Event } from 'vs/base/common/event'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { $, EventHelper, EventLike } from 'vs/base/browser/dom'; -import { domEvent } from 'vs/base/browser/event'; +import { DomEmitter, domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Color } from 'vs/base/common/color'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; export interface ILinkDescriptor { readonly label: string; @@ -18,75 +19,86 @@ export interface ILinkDescriptor { readonly title?: string; } -export interface ILinkStyles { - readonly textLinkForeground?: Color; - readonly disabled?: boolean; +export interface ILinkOptions { + readonly opener?: (href: string) => void; + readonly textLinkForeground?: string; } export class Link extends Disposable { readonly el: HTMLAnchorElement; - private disabled: boolean; - private styles: ILinkStyles = { - textLinkForeground: Color.fromHex('#006AB1') - }; + private _enabled: boolean = true; + + get enabled(): boolean { + return this._enabled; + } + + set enabled(enabled: boolean) { + if (enabled) { + this.el.setAttribute('aria-disabled', 'false'); + this.el.tabIndex = 0; + this.el.style.pointerEvents = 'auto'; + this.el.style.opacity = '1'; + this.el.style.cursor = 'pointer'; + this._enabled = false; + } else { + this.el.setAttribute('aria-disabled', 'true'); + this.el.tabIndex = -1; + this.el.style.pointerEvents = 'none'; + this.el.style.opacity = '0.4'; + this.el.style.cursor = 'default'; + this._enabled = true; + } + + this._enabled = enabled; + } constructor( link: ILinkDescriptor, + options: ILinkOptions | undefined = undefined, @IOpenerService openerService: IOpenerService ) { super(); - this.el = $('a', { + this.el = $('a.monaco-link', { tabIndex: 0, href: link.href, title: link.title }, link.label); - const onClick = domEvent(this.el, 'click'); + const onClickEmitter = this._register(new DomEmitter(this.el, 'click')); const onEnterPress = Event.chain(domEvent(this.el, 'keypress')) .map(e => new StandardKeyboardEvent(e)) .filter(e => e.keyCode === KeyCode.Enter) .event; - const onOpen = Event.any(onClick, onEnterPress); + const onOpen = Event.any(onClickEmitter.event, onEnterPress); this._register(onOpen(e => { + if (!this.enabled) { + return; + } + EventHelper.stop(e, true); - if (!this.disabled) { + + if (options?.opener) { + options.opener(link.href); + } else { openerService.open(link.href, { allowCommands: true }); } })); - this.disabled = false; - this.applyStyles(); + this.enabled = true; } +} - style(styles: ILinkStyles): void { - this.styles = styles; - this.applyStyles(); +registerThemingParticipant((theme, collector) => { + const textLinkForegroundColor = theme.getColor(textLinkForeground); + if (textLinkForegroundColor) { + collector.addRule(`.monaco-link { color: ${textLinkForegroundColor}; }`); } - private applyStyles(): void { - const color = this.styles.textLinkForeground?.toString(); - if (color) { - this.el.style.color = color; - } - if (typeof this.styles.disabled === 'boolean' && this.styles.disabled !== this.disabled) { - if (this.styles.disabled) { - this.el.setAttribute('aria-disabled', 'true'); - this.el.tabIndex = -1; - this.el.style.pointerEvents = 'none'; - this.el.style.opacity = '0.4'; - this.el.style.cursor = 'default'; - this.disabled = true; - } else { - this.el.setAttribute('aria-disabled', 'false'); - this.el.tabIndex = 0; - this.el.style.pointerEvents = 'auto'; - this.el.style.opacity = '1'; - this.el.style.cursor = 'pointer'; - this.disabled = false; - } - } + const textLinkActiveForegroundColor = theme.getColor(textLinkActiveForeground); + if (textLinkActiveForegroundColor) { + collector.addRule(`.monaco-link:hover { color: ${textLinkActiveForegroundColor}; }`); } -} +}); diff --git a/lib/vscode/src/vs/platform/opener/common/opener.ts b/lib/vscode/src/vs/platform/opener/common/opener.ts index 33c465736b78..fad7d23c761e 100644 --- a/lib/vscode/src/vs/platform/opener/common/opener.ts +++ b/lib/vscode/src/vs/platform/opener/common/opener.ts @@ -109,6 +109,7 @@ export interface IOpenerService { /** * Resolve a resource to its external form. + * @throws whenever resolvers couldn't resolve this resource externally. */ resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise; } diff --git a/lib/vscode/src/vs/platform/product/common/product.ts b/lib/vscode/src/vs/platform/product/common/product.ts index a0e1e4c15ace..0735d29d9f00 100644 --- a/lib/vscode/src/vs/platform/product/common/product.ts +++ b/lib/vscode/src/vs/platform/product/common/product.ts @@ -13,7 +13,7 @@ import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes let product: IProductConfiguration; // Native sandbox environment -if (typeof globals.vscode !== 'undefined') { +if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.context !== 'undefined') { const configuration: ISandboxConfiguration | undefined = globals.vscode.context.configuration(); if (configuration) { product = configuration.product; @@ -54,7 +54,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.56.0-dev', + version: '1.57.0-dev', nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev', nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev', applicationName: 'code-oss', @@ -66,13 +66,14 @@ else { extensionAllowedProposedApi: [ 'ms-vscode.vscode-js-profile-flame', 'ms-vscode.vscode-js-profile-table', - 'ms-vscode.github-browser', - 'ms-vscode.github-richnav', 'ms-vscode.remotehub', - 'ms-vscode.remotehub-insiders' + 'ms-vscode.remotehub-insiders', + 'GitHub.remotehub', + 'GitHub.remotehub-insiders' ], }); } + // NOTE@coder: Add the ability to inject settings from the server. const el = document.getElementById('vscode-remote-product-configuration'); const rawProductConfiguration = el && el.getAttribute('data-settings'); diff --git a/lib/vscode/src/vs/platform/progress/common/progress.ts b/lib/vscode/src/vs/platform/progress/common/progress.ts index 9c784f36c567..4603828430e8 100644 --- a/lib/vscode/src/vs/platform/progress/common/progress.ts +++ b/lib/vscode/src/vs/platform/progress/common/progress.ts @@ -19,7 +19,7 @@ export interface IProgressService { readonly _serviceBrand: undefined; withProgress( - options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void ): Promise; @@ -66,6 +66,11 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly silent?: boolean; } +export interface IProgressDialogOptions extends IProgressOptions { + readonly delay?: number; + readonly detail?: string; +} + export interface IProgressWindowOptions extends IProgressOptions { readonly location: ProgressLocation.Window; readonly command?: string; @@ -134,7 +139,7 @@ export class UnmanagedProgress extends Disposable { private lastStep?: IProgressStep; constructor( - options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, @IProgressService progressService: IProgressService, ) { super(); @@ -159,7 +164,6 @@ export class UnmanagedProgress extends Disposable { } } - export class LongRunningOperation extends Disposable { private currentOperationId = 0; private readonly currentOperationDisposables = this._register(new DisposableStore()); diff --git a/lib/vscode/src/vs/platform/protocol/electron-main/protocol.ts b/lib/vscode/src/vs/platform/protocol/electron-main/protocol.ts index e224973ab708..85a213cfa075 100644 --- a/lib/vscode/src/vs/platform/protocol/electron-main/protocol.ts +++ b/lib/vscode/src/vs/platform/protocol/electron-main/protocol.ts @@ -34,12 +34,8 @@ export interface IProtocolMainService { /** * Allows to make an object accessible to a renderer * via `ipcRenderer.invoke(resource.toString())`. - * - * @param obj the (optional) object to make accessible to the - * renderer. Can be updated later via the `IObjectUrl#update` - * method too. */ - createIPCObjectUrl(obj?: T): IIPCObjectUrl; + createIPCObjectUrl(): IIPCObjectUrl; /** * Adds a `URI` as root to the list of allowed diff --git a/lib/vscode/src/vs/platform/protocol/electron-main/protocolMainService.ts b/lib/vscode/src/vs/platform/protocol/electron-main/protocolMainService.ts index 9f93a6b7755a..798aa745d4d1 100644 --- a/lib/vscode/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/lib/vscode/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -22,7 +22,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ declare readonly _serviceBrand: undefined; private readonly validRoots = TernarySearchTree.forUris(() => !isLinux); - private readonly validExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 + private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 constructor( @INativeEnvironmentService environmentService: INativeEnvironmentService, @@ -47,10 +47,10 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ const { defaultSession } = session; // Register vscode-file:// handler - defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback)); + defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback)); // Intercept any file:// access - defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback)); + defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback)); // Cleanup this._register(toDisposable(() => { @@ -142,7 +142,8 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ //#region IPC Object URLs - createIPCObjectUrl(obj: T): IIPCObjectUrl { + createIPCObjectUrl(): IIPCObjectUrl { + let obj: T | undefined = undefined; // Create unique URI const resource = URI.from({ @@ -152,7 +153,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ // Install IPC handler const channel = resource.toString(); - const handler = async (): Promise => obj; + const handler = async (): Promise => obj; ipcMain.handle(channel, handler); this.logService.trace(`IPC Object URL: Registered new channel ${channel}.`); diff --git a/lib/vscode/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/lib/vscode/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 96210a56e382..e84a282762a2 100644 --- a/lib/vscode/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/lib/vscode/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -19,7 +19,8 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; import { toErrorMessage } from 'vs/base/common/errorMessage'; export interface ICommandQuickPick extends IPickerQuickAccessItem { @@ -47,14 +48,14 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc @IKeybindingService private readonly keybindingService: IKeybindingService, @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @INotificationService private readonly notificationService: INotificationService + @IDialogService private readonly dialogService: IDialogService ) { super(AbstractCommandsQuickAccessProvider.PREFIX, options); this.options = options; } - protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + protected async _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { // Ask subclass for all command picks const allCommandPicks = await this.getCommandPicks(disposables, token); @@ -162,7 +163,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc await this.commandService.executeCommand(commandPick.commandId); } catch (error) { if (!isPromiseCanceledError(error)) { - this.notificationService.error(localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error))); + this.dialogService.show(Severity.Error, localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error)), [localize('ok', 'OK')]); } } } diff --git a/lib/vscode/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/lib/vscode/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 4d8dafdbf0e5..52db4d078817 100644 --- a/lib/vscode/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/lib/vscode/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -5,7 +5,7 @@ import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickSeparator, IKeyMods, IQuickPickDidAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; @@ -42,7 +42,7 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { * @param keyMods the state of modifier keys when the item was accepted. * @param event the underlying event that caused the accept to trigger. */ - accept?(keyMods: IKeyMods, event: IQuickPickAcceptEvent): void; + accept?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void; /** * A method that will be executed when a button of the pick item was @@ -122,7 +122,7 @@ export abstract class PickerQuickAccessProvider, skipEmpty?: boolean): boolean => { let items: readonly Pick[]; @@ -330,5 +330,5 @@ export abstract class PickerQuickAccessProvider | Promise> | FastAndSlowPicks | null; + protected abstract _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Picks | Promise> | FastAndSlowPicks | null; } diff --git a/lib/vscode/src/vs/platform/quickinput/browser/quickAccess.ts b/lib/vscode/src/vs/platform/quickinput/browser/quickAccess.ts index 1e580e4ff30e..2054edb38381 100644 --- a/lib/vscode/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/lib/vscode/src/vs/platform/quickinput/browser/quickAccess.ts @@ -31,7 +31,17 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon super(); } + pick(value = '', options?: IQuickAccessOptions): Promise { + return this.doShowOrPick(value, true, options); + } + show(value = '', options?: IQuickAccessOptions): void { + this.doShowOrPick(value, false, options); + } + + private doShowOrPick(value: string, pick: true, options?: IQuickAccessOptions): Promise; + private doShowOrPick(value: string, pick: false, options?: IQuickAccessOptions): void; + private doShowOrPick(value: string, pick: boolean, options?: IQuickAccessOptions): Promise | void { // Find provider for the value to show const [provider, descriptor] = this.getOrInstantiateProvider(value); @@ -99,6 +109,18 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon picker.ariaLabel = descriptor?.placeholder; } + // Pick mode: setup a promise that can be resolved + // with the selected items and prevent execution + let pickPromise: Promise | undefined = undefined; + let pickResolve: Function | undefined = undefined; + if (pick) { + pickPromise = new Promise(resolve => pickResolve = resolve); + disposables.add(once(picker.onWillAccept)(e => { + e.veto(); + picker.hide(); + })); + } + // Register listeners disposables.add(this.registerPickerListeners(picker, provider, descriptor, value)); @@ -119,12 +141,20 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Start to dispose once picker hides disposables.dispose(); + + // Resolve pick promise with selected items + pickResolve?.(picker.selectedItems); }); // Finally, show the picker. This is important because a provider // may not call this and then our disposables would leak that rely // on the onDidHide event. picker.show(); + + // Pick mode: return with promise + if (pick) { + return pickPromise; + } } private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { diff --git a/lib/vscode/src/vs/platform/quickinput/browser/quickInput.ts b/lib/vscode/src/vs/platform/quickinput/browser/quickInput.ts index e0884b98b1d5..bb05ceea74ec 100644 --- a/lib/vscode/src/vs/platform/quickinput/browser/quickInput.ts +++ b/lib/vscode/src/vs/platform/quickinput/browser/quickInput.ts @@ -7,7 +7,7 @@ import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuick import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground, keybindingLabelBackground, keybindingLabelForeground, keybindingLabelBorder, keybindingLabelBottomBorder } from 'vs/platform/theme/common/colorRegistry'; +import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground, keybindingLabelBackground, keybindingLabelForeground, keybindingLabelBorder, keybindingLabelBottomBorder, quickInputListFocusForeground } from 'vs/platform/theme/common/colorRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; import { computeStyles } from 'vs/platform/theme/common/styler'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -219,7 +219,7 @@ export class QuickInputService extends Themable implements IQuickInputService { list: computeStyles(this.theme, { listBackground: quickInputBackground, // Look like focused when inactive. - listInactiveFocusForeground: listFocusForeground, + listInactiveFocusForeground: quickInputListFocusForeground, listInactiveFocusBackground: quickInputListFocusBackground, listFocusOutline: activeContrastBorder, listInactiveFocusOutline: activeContrastBorder, diff --git a/lib/vscode/src/vs/platform/quickinput/common/quickAccess.ts b/lib/vscode/src/vs/platform/quickinput/common/quickAccess.ts index 3b682f00add4..d1e7fd05a85f 100644 --- a/lib/vscode/src/vs/platform/quickinput/common/quickAccess.ts +++ b/lib/vscode/src/vs/platform/quickinput/common/quickAccess.ts @@ -36,6 +36,13 @@ export interface IQuickAccessController { * Open the quick access picker with the optional value prefilled. */ show(value?: string, options?: IQuickAccessOptions): void; + + /** + * Same as `show()` but instead of executing the selected pick item, + * it will be returned. May return `undefined` in case no item was + * picked by the user. + */ + pick(value?: string, options?: IQuickAccessOptions): Promise; } export enum DefaultQuickAccessFilterValue { diff --git a/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts b/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts index c65de8ad37e7..60bf97572f8e 100644 --- a/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -217,3 +217,6 @@ export class BrowserSocketFactory implements ISocketFactory { }); } } + + + diff --git a/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 3d5d29b8e007..e7f177ceb17b 100644 --- a/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -40,6 +40,10 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot return this._cache.get(authority)!; } + async getCanonicalURI(uri: URI): Promise { + return uri; + } + getConnectionData(authority: string): IRemoteConnectionData | null { if (!this._cache.has(authority)) { return null; @@ -76,4 +80,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot RemoteAuthorities.setConnectionToken(authority, connectionToken); this._onDidChangeConnectionData.fire(); } + + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { + } } diff --git a/lib/vscode/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/lib/vscode/src/vs/platform/remote/common/remoteAuthorityResolver.ts index 52902edfcbcd..9a3108779985 100644 --- a/lib/vscode/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/lib/vscode/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -5,6 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; export const IRemoteAuthorityResolverService = createDecorator('remoteAuthorityResolverService'); @@ -15,15 +16,9 @@ export interface ResolvedAuthority { readonly connectionToken: string | undefined; } -export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 -} - export interface ResolvedOptions { readonly extensionHostEnv?: { [key: string]: string | null }; - readonly trust?: RemoteTrustOption; + readonly isTrusted?: boolean; } export interface TunnelDescription { @@ -98,9 +93,18 @@ export interface IRemoteAuthorityResolverService { resolveAuthority(authority: string): Promise; getConnectionData(authority: string): IRemoteConnectionData | null; + /** + * Get the canonical URI for a `vscode-remote://` URI. + * + * **NOTE**: This can throw e.g. in cases where there is no resolver installed for the specific remote authority. + * + * @param uri The `vscode-remote://` URI + */ + getCanonicalURI(uri: URI): Promise; _clearResolvedAuthority(authority: string): void; _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void; _setResolvedAuthorityError(authority: string, err: any): void; _setAuthorityConnectionToken(authority: string, connectionToken: string): void; + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void; } diff --git a/lib/vscode/src/vs/platform/remote/common/remoteHosts.ts b/lib/vscode/src/vs/platform/remote/common/remoteHosts.ts index 6894d782f5d1..e125df902cdf 100644 --- a/lib/vscode/src/vs/platform/remote/common/remoteHosts.ts +++ b/lib/vscode/src/vs/platform/remote/common/remoteHosts.ts @@ -26,7 +26,7 @@ export function getRemoteName(authority: string | undefined): string | undefined return authority.substr(0, pos); } -function isVirtualResource(resource: URI) { +export function isVirtualResource(resource: URI) { return resource.scheme !== Schemas.file && resource.scheme !== Schemas.vscodeRemote; } @@ -42,3 +42,7 @@ export function getVirtualWorkspaceLocation(workspace: IWorkspace): { scheme: st export function getVirtualWorkspaceScheme(workspace: IWorkspace): string | undefined { return getVirtualWorkspaceLocation(workspace)?.scheme; } + +export function isVirtualWorkspace(workspace: IWorkspace): boolean { + return getVirtualWorkspaceLocation(workspace) !== undefined; +} diff --git a/lib/vscode/src/vs/platform/remote/common/tunnel.ts b/lib/vscode/src/vs/platform/remote/common/tunnel.ts index 06de888d61fe..bd67538ee5f6 100644 --- a/lib/vscode/src/vs/platform/remote/common/tunnel.ts +++ b/lib/vscode/src/vs/platform/remote/common/tunnel.ts @@ -86,6 +86,7 @@ export interface ITunnelService { readonly onTunnelOpened: Event; readonly onTunnelClosed: Event<{ host: string, port: number; }>; readonly canElevate: boolean; + readonly hasTunnelProvider: boolean; canTunnel(uri: URI): boolean; openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise | undefined; @@ -141,6 +142,10 @@ export abstract class AbstractTunnelService implements ITunnelService { @ILogService protected readonly logService: ILogService ) { } + get hasTunnelProvider(): boolean { + return !!this._tunnelProvider; + } + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable { this._tunnelProvider = provider; if (!provider) { diff --git a/lib/vscode/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/lib/vscode/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index 41529a21aee5..93081c02147d 100644 --- a/lib/vscode/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts +++ b/lib/vscode/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts @@ -8,22 +8,27 @@ import * as errors from 'vs/base/common/errors'; import { RemoteAuthorities } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; - -class PendingResolveAuthorityRequest { - - public value: ResolverResult | null; - - constructor( - private readonly _resolve: (value: ResolverResult) => void, - private readonly _reject: (err: any) => void, - public readonly promise: Promise, - ) { - this.value = null; +import { URI } from 'vs/base/common/uri'; + +class PendingPromise { + public readonly promise: Promise; + public readonly input: I; + public result: R | null; + private _resolve!: (value: R) => void; + private _reject!: (err: any) => void; + + constructor(request: I) { + this.input = request; + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this.result = null; } - resolve(value: ResolverResult): void { - this.value = value; - this._resolve(this.value); + resolve(result: R): void { + this.result = result; + this._resolve(this.result); } reject(err: any): void { @@ -38,40 +43,50 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot private readonly _onDidChangeConnectionData = this._register(new Emitter()); public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event; - private readonly _resolveAuthorityRequests: Map; + private readonly _resolveAuthorityRequests: Map>; private readonly _connectionTokens: Map; + private readonly _canonicalURIRequests: Map>; + private _canonicalURIProvider: ((uri: URI) => Promise) | null; constructor() { super(); - this._resolveAuthorityRequests = new Map(); + this._resolveAuthorityRequests = new Map>(); this._connectionTokens = new Map(); + this._canonicalURIRequests = new Map>(); + this._canonicalURIProvider = null; } resolveAuthority(authority: string): Promise { if (!this._resolveAuthorityRequests.has(authority)) { - let resolve: (value: ResolverResult) => void; - let reject: (err: any) => void; - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - this._resolveAuthorityRequests.set(authority, new PendingResolveAuthorityRequest(resolve!, reject!, promise)); + this._resolveAuthorityRequests.set(authority, new PendingPromise(authority)); } return this._resolveAuthorityRequests.get(authority)!.promise; } + async getCanonicalURI(uri: URI): Promise { + const key = uri.toString(); + if (!this._canonicalURIRequests.has(key)) { + const request = new PendingPromise(uri); + if (this._canonicalURIProvider) { + this._canonicalURIProvider(request.input).then((uri) => request.resolve(uri), (err) => request.reject(err)); + } + this._canonicalURIRequests.set(key, request); + } + return this._canonicalURIRequests.get(key)!.promise; + } + getConnectionData(authority: string): IRemoteConnectionData | null { if (!this._resolveAuthorityRequests.has(authority)) { return null; } const request = this._resolveAuthorityRequests.get(authority)!; - if (!request.value) { + if (!request.result) { return null; } const connectionToken = this._connectionTokens.get(authority); return { - host: request.value.authority.host, - port: request.value.authority.port, + host: request.result.authority.host, + port: request.result.authority.port, connectionToken: connectionToken }; } @@ -107,4 +122,11 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot RemoteAuthorities.setConnectionToken(authority, connectionToken); this._onDidChangeConnectionData.fire(); } + + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { + this._canonicalURIProvider = provider; + this._canonicalURIRequests.forEach((value) => { + this._canonicalURIProvider!(value.input).then((uri) => value.resolve(uri), (err) => value.reject(err)); + }); + } } diff --git a/lib/vscode/src/vs/platform/remote/node/tunnelService.ts b/lib/vscode/src/vs/platform/remote/node/tunnelService.ts index 6483000c8701..711b80e7e670 100644 --- a/lib/vscode/src/vs/platform/remote/node/tunnelService.ts +++ b/lib/vscode/src/vs/platform/remote/node/tunnelService.ts @@ -8,6 +8,7 @@ import { Barrier } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import { findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -15,8 +16,8 @@ import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/t import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; -async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { - const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); +async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { + const tunnel = new NodeRemoteTunnel(options, defaultTunnelHost, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); return tunnel.waitForReady(); } @@ -38,7 +39,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { private readonly _socketsDispose: Map void> = new Map(); - constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) { + constructor(options: IConnectionOptions, private readonly defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) { super(); this._options = options; this._server = net.createServer(); @@ -76,17 +77,19 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { // if that fails, the method above returns 0, which works out fine below... let address: string | net.AddressInfo | null = null; - address = (this._server.listen(localPort).address()); + this._server.listen(localPort, this.defaultTunnelHost); + await this._barrier.wait(); + address = this._server.address(); // It is possible for findFreePortFaster to return a port that there is already a server listening on. This causes the previous listen call to error out. if (!address) { localPort = 0; - address = (this._server.listen(localPort).address()); + this._server.listen(localPort, this.defaultTunnelHost); + await this._barrier.wait(); + address = this._server.address(); } this.tunnelLocalPort = address.port; - - await this._barrier.wait(); this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`; return this; } @@ -135,11 +138,16 @@ export class BaseTunnelService extends AbstractTunnelService { private readonly socketFactory: ISocketFactory, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(logService); } + private get defaultTunnelHost(): string { + return (this.configurationService.getValue('remote.localPortHost') === 'localhost') ? '127.0.0.1' : '0.0.0.0'; + } + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { @@ -160,7 +168,7 @@ export class BaseTunnelService extends AbstractTunnelService { ipcLogger: null }; - const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); + const tunnel = createRemoteTunnel(options, this.defaultTunnelHost, remoteHost, remotePort, localPort); this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.'); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; @@ -172,8 +180,9 @@ export class TunnelService extends BaseTunnelService { public constructor( @ILogService logService: ILogService, @ISignService signService: ISignService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService ) { - super(nodeSocketFactory, logService, signService, productService); + super(nodeSocketFactory, logService, signService, productService, configurationService); } } diff --git a/lib/vscode/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/lib/vscode/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 64168de41a66..f62912e56d14 100644 --- a/lib/vscode/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/lib/vscode/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -18,7 +18,6 @@ import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/i import { assertIsDefined } from 'vs/base/common/types'; import { Emitter, Event } from 'vs/base/common/event'; import { WindowError } from 'vs/platform/windows/electron-main/windows'; -import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; export class SharedProcess extends Disposable implements ISharedProcess { @@ -139,9 +138,6 @@ export class SharedProcess extends Disposable implements ISharedProcess { // Always wait for first window asking for connection await this.firstWindowConnectionBarrier.wait(); - // Resolve shell environment - this.userEnv = { ...this.userEnv, ...(await resolveShellEnv(this.logService, this.environmentMainService.args, process.env)) }; - // Create window for shared process this.createWindow(); diff --git a/lib/vscode/src/vs/platform/state/node/state.ts b/lib/vscode/src/vs/platform/state/electron-main/state.ts similarity index 72% rename from lib/vscode/src/vs/platform/state/node/state.ts rename to lib/vscode/src/vs/platform/state/electron-main/state.ts index 9fd896f914cc..8d498194eb60 100644 --- a/lib/vscode/src/vs/platform/state/node/state.ts +++ b/lib/vscode/src/vs/platform/state/electron-main/state.ts @@ -5,15 +5,19 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -export const IStateService = createDecorator('stateService'); +export const IStateMainService = createDecorator('stateMainService'); + +export interface IStateMainService { -export interface IStateService { readonly _serviceBrand: undefined; getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; setItem(key: string, data?: object | string | number | boolean | undefined | null): void; + setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void; removeItem(key: string): void; + + close(): Promise; } diff --git a/lib/vscode/src/vs/platform/state/electron-main/stateMainService.ts b/lib/vscode/src/vs/platform/state/electron-main/stateMainService.ts new file mode 100644 index 000000000000..08f979251123 --- /dev/null +++ b/lib/vscode/src/vs/platform/state/electron-main/stateMainService.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { join } from 'vs/base/common/path'; +import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; + +type StorageDatabase = { [key: string]: unknown; }; + +export class FileStorage { + + private storage: StorageDatabase = Object.create(null); + private lastSavedStorageContents = ''; + + private readonly flushDelayer = new ThrottledDelayer(100 /* buffer saves over a short time */); + + private initializing: Promise | undefined = undefined; + private closing: Promise | undefined = undefined; + + constructor( + private readonly storagePath: URI, + private readonly logService: ILogService, + private readonly fileService: IFileService + ) { + } + + init(): Promise { + if (!this.initializing) { + this.initializing = this.doInit(); + } + + return this.initializing; + } + + private async doInit(): Promise { + try { + this.lastSavedStorageContents = (await this.fileService.readFile(this.storagePath)).value.toString(); + this.storage = JSON.parse(this.lastSavedStorageContents); + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(error); + } + } + } + + getItem(key: string, defaultValue: T): T; + getItem(key: string, defaultValue?: T): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined { + const res = this.storage[key]; + if (isUndefinedOrNull(res)) { + return defaultValue; + } + + return res as T; + } + + setItem(key: string, data?: object | string | number | boolean | undefined | null): void { + this.setItems([{ key, data }]); + } + + setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void { + let save = false; + + for (const { key, data } of items) { + + // Shortcut for data that did not change + if (this.storage[key] === data) { + continue; + } + + // Remove items when they are undefined or null + if (isUndefinedOrNull(data)) { + if (!isUndefined(this.storage[key])) { + this.storage[key] = undefined; + save = true; + } + } + + // Otherwise add an item + else { + this.storage[key] = data; + save = true; + } + } + + if (save) { + this.save(); + } + } + + removeItem(key: string): void { + + // Only update if the key is actually present (not undefined) + if (!isUndefined(this.storage[key])) { + this.storage[key] = undefined; + this.save(); + } + } + + private async save(delay?: number): Promise { + if (this.closing) { + return; // already about to close + } + + return this.flushDelayer.trigger(() => this.doSave(), delay); + } + + private async doSave(): Promise { + if (!this.initializing) { + return; // if we never initialized, we should not save our state + } + + // Make sure to wait for init to finish first + await this.initializing; + + // Return early if the database has not changed + const serializedDatabase = JSON.stringify(this.storage, null, 4); + if (serializedDatabase === this.lastSavedStorageContents) { + return; + } + + // Write to disk + try { + await this.fileService.writeFile(this.storagePath, VSBuffer.fromString(serializedDatabase)); + this.lastSavedStorageContents = serializedDatabase; + } catch (error) { + this.logService.error(error); + } + } + + async close(): Promise { + if (!this.closing) { + this.closing = this.flushDelayer.trigger(() => this.doSave(), 0 /* as soon as possible */); + } + + return this.closing; + } +} + +export class StateMainService implements IStateMainService { + + declare readonly _serviceBrand: undefined; + + private static readonly STATE_FILE = 'storage.json'; + + private readonly fileStorage: FileStorage; + + constructor( + @IEnvironmentMainService environmentMainService: IEnvironmentMainService, + @ILogService logService: ILogService, + @IFileService fileService: IFileService + ) { + this.fileStorage = new FileStorage(URI.file(join(environmentMainService.userDataPath, StateMainService.STATE_FILE)), logService, fileService); + } + + async init(): Promise { + return this.fileStorage.init(); + } + + getItem(key: string, defaultValue: T): T; + getItem(key: string, defaultValue?: T): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined { + return this.fileStorage.getItem(key, defaultValue); + } + + setItem(key: string, data?: object | string | number | boolean | undefined | null): void { + this.fileStorage.setItem(key, data); + } + + setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void { + this.fileStorage.setItems(items); + } + + removeItem(key: string): void { + this.fileStorage.removeItem(key); + } + + close(): Promise { + return this.fileStorage.close(); + } +} diff --git a/lib/vscode/src/vs/platform/state/node/stateService.ts b/lib/vscode/src/vs/platform/state/node/stateService.ts deleted file mode 100644 index 3799c66d4b04..000000000000 --- a/lib/vscode/src/vs/platform/state/node/stateService.ts +++ /dev/null @@ -1,158 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { writeFileSync } from 'vs/base/node/pfs'; -import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; -import { IStateService } from 'vs/platform/state/node/state'; -import { ILogService } from 'vs/platform/log/common/log'; - -type StorageDatabase = { [key: string]: any; }; - -export class FileStorage { - - private _database: StorageDatabase | null = null; - private lastFlushedSerializedDatabase: string | null = null; - - constructor(private dbPath: string, private onError: (error: Error) => void) { } - - private get database(): StorageDatabase { - if (!this._database) { - this._database = this.loadSync(); - } - - return this._database; - } - - async init(): Promise { - if (this._database) { - return; // return if database was already loaded - } - - const database = await this.loadAsync(); - - if (this._database) { - return; // return if database was already loaded - } - - this._database = database; - } - - private loadSync(): StorageDatabase { - try { - this.lastFlushedSerializedDatabase = fs.readFileSync(this.dbPath).toString(); - - return JSON.parse(this.lastFlushedSerializedDatabase); - } catch (error) { - if (error.code !== 'ENOENT') { - this.onError(error); - } - - return {}; - } - } - - private async loadAsync(): Promise { - try { - this.lastFlushedSerializedDatabase = (await fs.promises.readFile(this.dbPath)).toString(); - - return JSON.parse(this.lastFlushedSerializedDatabase); - } catch (error) { - if (error.code !== 'ENOENT') { - this.onError(error); - } - - return {}; - } - } - - getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue?: T): T | undefined; - getItem(key: string, defaultValue?: T): T | undefined { - const res = this.database[key]; - if (isUndefinedOrNull(res)) { - return defaultValue; - } - - return res; - } - - setItem(key: string, data?: object | string | number | boolean | undefined | null): void { - - // Remove an item when it is undefined or null - if (isUndefinedOrNull(data)) { - return this.removeItem(key); - } - - // Shortcut for primitives that did not change - if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { - if (this.database[key] === data) { - return; - } - } - - this.database[key] = data; - this.saveSync(); - } - - removeItem(key: string): void { - - // Only update if the key is actually present (not undefined) - if (!isUndefined(this.database[key])) { - this.database[key] = undefined; - this.saveSync(); - } - } - - private saveSync(): void { - const serializedDatabase = JSON.stringify(this.database, null, 4); - if (serializedDatabase === this.lastFlushedSerializedDatabase) { - return; // return early if the database has not changed - } - - try { - writeFileSync(this.dbPath, serializedDatabase); // permission issue can happen here - this.lastFlushedSerializedDatabase = serializedDatabase; - } catch (error) { - this.onError(error); - } - } -} - -export class StateService implements IStateService { - - declare readonly _serviceBrand: undefined; - - private static readonly STATE_FILE = 'storage.json'; - - private fileStorage: FileStorage; - - constructor( - @INativeEnvironmentService environmentService: INativeEnvironmentService, - @ILogService logService: ILogService - ) { - this.fileStorage = new FileStorage(path.join(environmentService.userDataPath, StateService.STATE_FILE), error => logService.error(error)); - } - - init(): Promise { - return this.fileStorage.init(); - } - - getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue?: T): T | undefined; - getItem(key: string, defaultValue?: T): T | undefined { - return this.fileStorage.getItem(key, defaultValue); - } - - setItem(key: string, data?: object | string | number | boolean | undefined | null): void { - this.fileStorage.setItem(key, data); - } - - removeItem(key: string): void { - this.fileStorage.removeItem(key); - } -} diff --git a/lib/vscode/src/vs/platform/state/test/electron-main/state.test.ts b/lib/vscode/src/vs/platform/state/test/electron-main/state.test.ts new file mode 100644 index 000000000000..40b751c6c1e6 --- /dev/null +++ b/lib/vscode/src/vs/platform/state/test/electron-main/state.test.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { tmpdir } from 'os'; +import { readFileSync } from 'fs'; +import { join } from 'vs/base/common/path'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { FileStorage } from 'vs/platform/state/electron-main/stateMainService'; +import { Promises, rimraf, writeFileSync } from 'vs/base/node/pfs'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; + +flakySuite('StateMainService', () => { + + let testDir: string; + let fileService: IFileService; + let logService: ILogService; + let diskFileSystemProvider: DiskFileSystemProvider; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'statemainservice'); + + logService = new NullLogService(); + + fileService = new FileService(logService); + diskFileSystemProvider = new DiskFileSystemProvider(logService); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + return Promises.mkdir(testDir, { recursive: true }); + }); + + teardown(() => { + fileService.dispose(); + diskFileSystemProvider.dispose(); + + return rimraf(testDir); + }); + + test('Basics', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + let service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + service.setItem('some.key', 'some.value'); + assert.strictEqual(service.getItem('some.key'), 'some.value'); + + service.removeItem('some.key'); + assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); + + assert.ok(!service.getItem('some.unknonw.key')); + + service.setItem('some.other.key', 'some.other.value'); + + await service.close(); + + service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); + + service.setItem('some.other.key', 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); + + service.setItem('some.undefined.key', undefined); + assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); + + service.setItem('some.null.key', null); + assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); + + service.setItems([ + { key: 'some.setItems.key1', data: 'some.value' }, + { key: 'some.setItems.key2', data: 0 }, + { key: 'some.setItems.key3', data: true }, + { key: 'some.setItems.key4', data: null }, + { key: 'some.setItems.key5', data: undefined } + ]); + + assert.strictEqual(service.getItem('some.setItems.key1'), 'some.value'); + assert.strictEqual(service.getItem('some.setItems.key2'), 0); + assert.strictEqual(service.getItem('some.setItems.key3'), true); + assert.strictEqual(service.getItem('some.setItems.key4'), undefined); + assert.strictEqual(service.getItem('some.setItems.key5'), undefined); + + service.setItems([ + { key: 'some.setItems.key1', data: undefined }, + { key: 'some.setItems.key2', data: undefined }, + { key: 'some.setItems.key3', data: undefined }, + { key: 'some.setItems.key4', data: null }, + { key: 'some.setItems.key5', data: undefined } + ]); + + assert.strictEqual(service.getItem('some.setItems.key1'), undefined); + assert.strictEqual(service.getItem('some.setItems.key2'), undefined); + assert.strictEqual(service.getItem('some.setItems.key3'), undefined); + assert.strictEqual(service.getItem('some.setItems.key4'), undefined); + assert.strictEqual(service.getItem('some.setItems.key5'), undefined); + }); + + test('Multiple ops are buffered and applied', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + let service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + service.removeItem('some.key4'); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + + await service.close(); + + service = new FileStorage(URI.file(storageFile), logService, fileService); + await service.init(); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + }); + + test('Used before init', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + let service = new FileStorage(URI.file(storageFile), logService, fileService); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + service.removeItem('some.key4'); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + + await service.init(); + + assert.strictEqual(service.getItem('some.key1'), 'some.value1'); + assert.strictEqual(service.getItem('some.key2'), 'some.value2'); + assert.strictEqual(service.getItem('some.key3'), 'some.value3'); + assert.strictEqual(service.getItem('some.key4'), undefined); + }); + + test('Used after close', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + const service = new FileStorage(URI.file(storageFile), logService, fileService); + + await service.init(); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + + await service.close(); + + service.setItem('some.key5', 'some.marker'); + + const contents = readFileSync(storageFile).toString(); + assert.ok(contents.includes('some.value1')); + assert.ok(!contents.includes('some.marker')); + + await service.close(); + }); + + test('Closed before init', async function () { + const storageFile = join(testDir, 'storage.json'); + writeFileSync(storageFile, ''); + + const service = new FileStorage(URI.file(storageFile), logService, fileService); + + service.setItem('some.key1', 'some.value1'); + service.setItem('some.key2', 'some.value2'); + service.setItem('some.key3', 'some.value3'); + service.setItem('some.key4', 'some.value4'); + + await service.close(); + + const contents = readFileSync(storageFile).toString(); + assert.strictEqual(contents.length, 0); + }); +}); diff --git a/lib/vscode/src/vs/platform/state/test/node/state.test.ts b/lib/vscode/src/vs/platform/state/test/node/state.test.ts deleted file mode 100644 index 01d2b4e39425..000000000000 --- a/lib/vscode/src/vs/platform/state/test/node/state.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { join } from 'vs/base/common/path'; -import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { FileStorage } from 'vs/platform/state/node/stateService'; -import { rimraf, writeFileSync } from 'vs/base/node/pfs'; - -flakySuite('StateService', () => { - - let testDir: string; - - setup(() => { - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice'); - - return promises.mkdir(testDir, { recursive: true }); - }); - - teardown(() => { - return rimraf(testDir); - }); - - test('Basics', async function () { - const storageFile = join(testDir, 'storage.json'); - writeFileSync(storageFile, ''); - - let service = new FileStorage(storageFile, () => null); - - service.setItem('some.key', 'some.value'); - assert.strictEqual(service.getItem('some.key'), 'some.value'); - - service.removeItem('some.key'); - assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); - - assert.ok(!service.getItem('some.unknonw.key')); - - service.setItem('some.other.key', 'some.other.value'); - - service = new FileStorage(storageFile, () => null); - - assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); - - service.setItem('some.other.key', 'some.other.value'); - assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); - - service.setItem('some.undefined.key', undefined); - assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); - - service.setItem('some.null.key', null); - assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); - }); -}); diff --git a/lib/vscode/src/vs/platform/storage/common/storage.ts b/lib/vscode/src/vs/platform/storage/common/storage.ts index 48f3829c9cf9..4dc61721d815 100644 --- a/lib/vscode/src/vs/platform/storage/common/storage.ts +++ b/lib/vscode/src/vs/platform/storage/common/storage.ts @@ -105,6 +105,8 @@ export interface IStorageService { * * @param target allows to define the target of the storage operation * to either the current machine or user. + * + * NOTE@coder: Add a promise so extensions can await storage writes. */ store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): Promise | void; @@ -333,46 +335,49 @@ export abstract class AbstractStorageService extends Disposable implements IStor return this.getStorage(scope)?.getNumber(key, fallbackValue); } - store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void { + // NOTE@coder: Make a promise so extensions can await storage writes. + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): Promise | void { // We remove the key for undefined/null values if (isUndefinedOrNull(value)) { this.remove(key, scope); - return; + return Promise.resolve(); } // Update our datastructures but send events only after - this.withPausedEmitters(() => { + return this.withPausedEmitters(() => { // Update key-target map this.updateKeyTarget(key, scope, target); // Store actual value - this.getStorage(scope)?.set(key, value); + return this.getStorage(scope)?.set(key, value); }); } - remove(key: string, scope: StorageScope): void { + // NOTE@coder: Make a promise so extensions can await the storage write. + remove(key: string, scope: StorageScope): Promise | void { // Update our datastructures but send events only after - this.withPausedEmitters(() => { + return this.withPausedEmitters(() => { // Update key-target map this.updateKeyTarget(key, scope, undefined); // Remove actual key - this.getStorage(scope)?.delete(key); + return this.getStorage(scope)?.delete(key); }); } - private withPausedEmitters(fn: Function): void { + // NOTE@coder: Return the function's return so extensions can await the storage write. + private withPausedEmitters(fn: () => T): T { // Pause emitters this._onDidChangeValue.pause(); this._onDidChangeTarget.pause(); try { - fn(); + return fn(); } finally { // Resume emitters diff --git a/lib/vscode/src/vs/platform/storage/electron-main/storageMain.ts b/lib/vscode/src/vs/platform/storage/electron-main/storageMain.ts index 3cd5051587fc..16ca034a8737 100644 --- a/lib/vscode/src/vs/platform/storage/electron-main/storageMain.ts +++ b/lib/vscode/src/vs/platform/storage/electron-main/storageMain.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; -import { exists, writeFile } from 'vs/base/node/pfs'; +import { exists, Promises, writeFile } from 'vs/base/node/pfs'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -276,7 +275,7 @@ export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMai } // Ensure storage folder exists - await promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + await Promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder (but do not await) this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); diff --git a/lib/vscode/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/lib/vscode/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index 0f55ca5c3b20..091de0aa8bf4 100644 --- a/lib/vscode/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/lib/vscode/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -66,8 +66,8 @@ suite('StorageMainService', function () { registerWindow(window: ICodeWindow): void { } async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise { } async unload(window: ICodeWindow, reason: UnloadReason): Promise { return true; } - relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): void { } - async quit(fromUpdate?: boolean): Promise { return true; } + async relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): Promise { } + async quit(willRestart?: boolean): Promise { return true; } async kill(code?: number): Promise { } async when(phase: LifecycleMainPhase): Promise { } } diff --git a/lib/vscode/src/vs/platform/telemetry/common/commonProperties.ts b/lib/vscode/src/vs/platform/telemetry/common/commonProperties.ts index 454b873fde13..9bcceeef0654 100644 --- a/lib/vscode/src/vs/platform/telemetry/common/commonProperties.ts +++ b/lib/vscode/src/vs/platform/telemetry/common/commonProperties.ts @@ -101,7 +101,7 @@ export async function resolveCommonProperties( return result; } -function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { +export function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { const userDnsDomain = env['USERDNSDOMAIN']; if (!userDnsDomain) { return false; diff --git a/lib/vscode/src/vs/platform/terminal/common/terminal.ts b/lib/vscode/src/vs/platform/terminal/common/terminal.ts index 28a4c9b649f2..6a0a7aa0dd13 100644 --- a/lib/vscode/src/vs/platform/terminal/common/terminal.ts +++ b/lib/vscode/src/vs/platform/terminal/common/terminal.ts @@ -8,6 +8,85 @@ import { Event } from 'vs/base/common/event'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; + +export const enum TerminalSettingPrefix { + Shell = 'terminal.integrated.shell.', + ShellArgs = 'terminal.integrated.shellArgs.', + DefaultProfile = 'terminal.integrated.defaultProfile.', + Profiles = 'terminal.integrated.profiles.' +} + +export const enum TerminalSettingId { + ShellLinux = 'terminal.integrated.shell.linux', + ShellMacOs = 'terminal.integrated.shell.osx', + ShellWindows = 'terminal.integrated.shell.windows', + SendKeybindingsToShell = 'terminal.integrated.sendKeybindingsToShell', + AutomationShellLinux = 'terminal.integrated.automationShell.linux', + AutomationShellMacOs = 'terminal.integrated.automationShell.osx', + AutomationShellWindows = 'terminal.integrated.automationShell.windows', + ShellArgsLinux = 'terminal.integrated.shellArgs.linux', + ShellArgsMacOs = 'terminal.integrated.shellArgs.osx', + ShellArgsWindows = 'terminal.integrated.shellArgs.windows', + ProfilesWindows = 'terminal.integrated.profiles.windows', + ProfilesMacOs = 'terminal.integrated.profiles.osx', + ProfilesLinux = 'terminal.integrated.profiles.linux', + DefaultProfileLinux = 'terminal.integrated.defaultProfile.linux', + DefaultProfileMacOs = 'terminal.integrated.defaultProfile.osx', + DefaultProfileWindows = 'terminal.integrated.defaultProfile.windows', + UseWslProfiles = 'terminal.integrated.useWslProfiles', + TabsEnabled = 'terminal.integrated.tabs.enabled', + TabsHideCondition = 'terminal.integrated.tabs.hideCondition', + TabsShowActiveTerminal = 'terminal.integrated.tabs.showActiveTerminal', + TabsLocation = 'terminal.integrated.tabs.location', + TabsFocusMode = 'terminal.integrated.tabs.focusMode', + MacOptionIsMeta = 'terminal.integrated.macOptionIsMeta', + MacOptionClickForcesSelection = 'terminal.integrated.macOptionClickForcesSelection', + AltClickMovesCursor = 'terminal.integrated.altClickMovesCursor', + CopyOnSelection = 'terminal.integrated.copyOnSelection', + DrawBoldTextInBrightColors = 'terminal.integrated.drawBoldTextInBrightColors', + FontFamily = 'terminal.integrated.fontFamily', + FontSize = 'terminal.integrated.fontSize', + LetterSpacing = 'terminal.integrated.letterSpacing', + LineHeight = 'terminal.integrated.lineHeight', + MinimumContrastRatio = 'terminal.integrated.minimumContrastRatio', + FastScrollSensitivity = 'terminal.integrated.fastScrollSensitivity', + MouseWheelScrollSensitivity = 'terminal.integrated.mouseWheelScrollSensitivity', + BellDuration = 'terminal.integrated.bellDuration', + FontWeight = 'terminal.integrated.fontWeight', + FontWeightBold = 'terminal.integrated.fontWeightBold', + CursorBlinking = 'terminal.integrated.cursorBlinking', + CursorStyle = 'terminal.integrated.cursorStyle', + CursorWidth = 'terminal.integrated.cursorWidth', + Scrollback = 'terminal.integrated.scrollback', + DetectLocale = 'terminal.integrated.detectLocale', + GpuAcceleration = 'terminal.integrated.gpuAcceleration', + RightClickBehavior = 'terminal.integrated.rightClickBehavior', + Cwd = 'terminal.integrated.cwd', + ConfirmOnExit = 'terminal.integrated.confirmOnExit', + EnableBell = 'terminal.integrated.enableBell', + CommandsToSkipShell = 'terminal.integrated.commandsToSkipShell', + AllowChords = 'terminal.integrated.allowChords', + AllowMnemonics = 'terminal.integrated.allowMnemonics', + EnvMacOs = 'terminal.integrated.env.osx', + EnvLinux = 'terminal.integrated.env.linux', + EnvWindows = 'terminal.integrated.env.windows', + EnvironmentChangesIndicator = 'terminal.integrated.environmentChangesIndicator', + EnvironmentChangesRelaunch = 'terminal.integrated.environmentChangesRelaunch', + ShowExitAlert = 'terminal.integrated.showExitAlert', + SplitCwd = 'terminal.integrated.splitCwd', + WindowsEnableConpty = 'terminal.integrated.windowsEnableConpty', + WordSeparators = 'terminal.integrated.wordSeparators', + TitleMode = 'terminal.integrated.titleMode', + EnableFileLinks = 'terminal.integrated.enableFileLinks', + UnicodeVersion = 'terminal.integrated.unicodeVersion', + ExperimentalLinkProvider = 'terminal.integrated.experimentalLinkProvider', + LocalEchoLatencyThreshold = 'terminal.integrated.localEchoLatencyThreshold', + LocalEchoExcludePrograms = 'terminal.integrated.localEchoExcludePrograms', + LocalEchoStyle = 'terminal.integrated.localEchoStyle', + EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions', + InheritEnv = 'terminal.integrated.inheritEnv' +} export enum WindowsShellType { CommandPrompt = 'cmd', @@ -30,7 +109,6 @@ export interface IRawTerminalTabLayoutInfo { } export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo; -export type ITerminalTabLayoutInfo = IRawTerminalTabLayoutInfo; export interface IRawTerminalsLayoutInfo { tabs: IRawTerminalTabLayoutInfo[]; @@ -40,11 +118,21 @@ export interface IPtyHostAttachTarget { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: TerminalIcon | undefined; +} + +export enum TitleEventSource { + /** From the API or the rename command that overrides any other type */ + Api, + /** From the process name property*/ + Process, + /** From the VT sequence */ + Sequence } export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; @@ -96,8 +184,13 @@ export interface IOffProcessTerminalService { attachToProcess(id: number): Promise; listProcesses(): Promise; getDefaultSystemShell(osOverride?: OperatingSystem): Promise; - getShellEnvironment(): Promise; + getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise; + getWslPath(original: string): Promise; + getEnvironment(): Promise; + getShellEnvironment(): Promise; setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise; + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise; + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getTerminalLayoutInfo(): Promise; reduceConnectionGraceTime(): Promise; } @@ -115,6 +208,8 @@ export interface IPtyService { readonly onPtyHostStart?: Event; readonly onPtyHostUnresponsive?: Event; readonly onPtyHostResponsive?: Event; + readonly onPtyHostRequestResolveVariables?: Event; + readonly onProcessData: Event<{ id: number, event: IProcessDataEvent | string }>; readonly onProcessExit: Event<{ id: number, event: number | undefined }>; readonly onProcessReady: Event<{ id: number, event: { pid: number, cwd: string } }>; @@ -127,6 +222,7 @@ export interface IPtyService { restartPtyHost?(): Promise; shutdownAll?(): Promise; + acceptPtyHostResolvedVariables?(id: number, resolved: string[]): Promise; createProcess( shellLaunchConfig: IShellLaunchConfig, @@ -159,14 +255,22 @@ export interface IPtyService { processBinary(id: number, data: string): Promise; /** Confirm the process is _not_ an orphan. */ orphanQuestionReply(id: number): Promise; - + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise; + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getDefaultSystemShell(osOverride?: OperatingSystem): Promise; - getShellEnvironment(): Promise; + getProfiles?(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise; + getEnvironment(): Promise; + getWslPath(original: string): Promise; setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise; getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise; reduceConnectionGraceTime(): Promise; } +export interface IRequestResolveVariablesEvent { + id: number; + originalText: string[]; +} + export enum HeartbeatConstants { /** * The duration between heartbeats @@ -260,7 +364,7 @@ export interface IShellLaunchConfig { /** * This is a terminal that attaches to an already running terminal. */ - attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; icon?: string; }; + attachPersistentProcess?: { id: number; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string }; /** * Whether the terminal process environment should be exactly as provided in @@ -271,6 +375,14 @@ export interface IShellLaunchConfig { */ strictEnv?: boolean; + /** + * Whether the terminal process environment will inherit VS Code's "shell environment" that may + * get sourced from running a login shell depnding on how the application was launched. + * Consumers that rely on development tools being present in the $PATH should set this to true. + * This will overwrite the value of the inheritEnv setting. + */ + useShellEnvironment?: boolean; + /** * When enabled the terminal will run the process as normal but not be surfaced to the user * until `Terminal.show` is called. The typical usage for this is when you need to run @@ -292,18 +404,25 @@ export interface IShellLaunchConfig { isExtensionOwnedTerminal?: boolean; /** - * The codicon ID to use for this terminal. If not specified it will use the default fallback - * icon. + * The icon for the terminal, used primarily in the terminal tab. + */ + icon?: TerminalIcon; + + /** + * The color ID to use for this terminal. If not specified it will use the default fallback */ - icon?: string; + color?: string; } +export type TerminalIcon = ThemeIcon | URI | { light: URI; dark: URI }; + export interface IShellLaunchConfigDto { name?: string; executable?: string; args?: string[] | string; cwd?: string | UriComponents; env?: ITerminalEnvironment; + useShellEnvironment?: boolean; hideFromUser?: boolean; } @@ -316,6 +435,12 @@ export interface ITerminalLaunchError { code?: number; } +export interface IProcessReadyEvent { + pid: number, + cwd: string, + requiresWindowsMode?: boolean +} + /** * An interface representing a raw terminal child process, this contains a subset of the * child_process.ChildProcess node.js interface. @@ -335,7 +460,7 @@ export interface ITerminalChildProcess { onProcessData: Event; onProcessExit: Event; - onProcessReady: Event<{ pid: number, cwd: string }>; + onProcessReady: Event; onProcessTitleChanged: Event; onProcessOverrideDimensions?: Event; onProcessResolvedShellLaunchConfig?: Event; @@ -378,15 +503,20 @@ export interface ITerminalChildProcess { getLatency(): Promise; } +export interface IReconnectConstants { + GraceTime: number, + ShortGraceTime: number +} + export const enum LocalReconnectConstants { /** * If there is no reconnection within this time-frame, consider the connection permanently closed... */ - ReconnectionGraceTime = 60000, // 60 seconds + GraceTime = 60000, // 60 seconds /** * Maximal grace time between the first and the last reconnection... */ - ReconnectionShortGraceTime = 6000, // 6 seconds + ShortGraceTime = 6000, // 6 seconds } export const enum FlowControlConstants { @@ -433,6 +563,18 @@ export interface ITerminalDimensions { rows: number; } +export interface ITerminalProfile { + profileName: string; + path: string; + isDefault: boolean; + isAutoDetected?: boolean; + args?: string | string[] | undefined; + env?: ITerminalEnvironment; + overrideName?: boolean; + color?: string; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; +} + export interface ITerminalDimensionsOverride extends Readonly { /** * indicate that xterm must receive these exact dimensions, even if they overflow the ui! @@ -440,4 +582,26 @@ export interface ITerminalDimensionsOverride extends Readonly(key: string) => T | undefined; +export const enum ProfileSource { + GitBash = 'Git Bash', + Pwsh = 'PowerShell' +} + +export interface IBaseUnresolvedTerminalProfile { + args?: string | string[] | undefined; + isAutoDetected?: boolean; + overrideName?: boolean; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; + color?: string; + env?: ITerminalEnvironment; +} + +export interface ITerminalExecutable extends IBaseUnresolvedTerminalProfile { + path: string | string[]; +} + +export interface ITerminalProfileSource extends IBaseUnresolvedTerminalProfile { + source: ProfileSource; +} + +export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | null; diff --git a/lib/vscode/src/vs/platform/terminal/common/terminalEnvironment.ts b/lib/vscode/src/vs/platform/terminal/common/terminalEnvironment.ts new file mode 100644 index 000000000000..66b5ad31a3f3 --- /dev/null +++ b/lib/vscode/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function escapeNonWindowsPath(path: string): string { + let newPath = path; + if (newPath.indexOf('\\') !== 0) { + newPath = newPath.replace(/\\/g, '\\\\'); + } + const bannedChars = /[\`\$\|\&\>\~\#\!\^\*\;\<\"\']/g; + newPath = newPath.replace(bannedChars, ''); + return `'${newPath}'`; +} diff --git a/lib/vscode/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/lib/vscode/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts new file mode 100644 index 000000000000..a5fd0a631f9b --- /dev/null +++ b/lib/vscode/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -0,0 +1,388 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { localize } from 'vs/nls'; +import { ITerminalProfile, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Codicon, iconRegistry } from 'vs/base/common/codicons'; +import { OperatingSystem } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; + +const terminalProfileBaseProperties: IJSONSchemaMap = { + args: { + description: localize('terminalProfile.args', 'An optional set of arguments to run the shell executable with.'), + type: 'array', + items: { + type: 'string' + } + }, + overrideName: { + description: localize('terminalProfile.overrideName', 'Controls whether or not the profile name overrides the auto detected one.'), + type: 'boolean' + }, + icon: { + description: localize('terminalProfile.icon', 'A codicon ID to associate with this terminal.'), + type: 'string', + enum: Array.from(iconRegistry.all, icon => icon.id), + markdownEnumDescriptions: Array.from(iconRegistry.all, icon => `$(${icon.id})`), + }, + color: { + description: localize('terminalProfile.color', 'A theme color ID to associate with this terminal.'), + type: ['string', 'null'], + enum: [ + 'terminal.ansiBlack', + 'terminal.ansiRed', + 'terminal.ansiGreen', + 'terminal.ansiYellow', + 'terminal.ansiBlue', + 'terminal.ansiMagenta', + 'terminal.ansiCyan', + 'terminal.ansiWhite' + ], + default: null + }, + env: { + markdownDescription: localize('terminalProfile.env', "An object with environment variables that will be added to the terminal profile process. Set to `null` to delete environment variables from the base environment."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] + }, + default: {} + } +}; + +const terminalProfileSchema: IJSONSchema = { + type: 'object', + required: ['path'], + properties: { + path: { + description: localize('terminalProfile.path', 'A single path to a shell executable or an array of paths that will be used as fallbacks when one fails.'), + type: ['string', 'array'], + items: { + type: 'string' + } + }, + ...terminalProfileBaseProperties + } +}; + +const shellDeprecationMessageLinux = localize('terminal.integrated.shell.linux.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.linux#`', '`#terminal.integrated.defaultProfile.linux#`'); +const shellDeprecationMessageOsx = localize('terminal.integrated.shell.osx.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.osx#`', '`#terminal.integrated.defaultProfile.osx#`'); +const shellDeprecationMessageWindows = localize('terminal.integrated.shell.windows.deprecation', "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in {0} and setting its profile name as the default in {1}. This will currently take priority over the new profiles settings but that will change in the future.", '`#terminal.integrated.profiles.windows#`', '`#terminal.integrated.defaultProfile.windows#`'); + +const terminalPlatformConfiguration: IConfigurationNode = { + id: 'terminal', + order: 100, + title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), + type: 'object', + properties: { + [TerminalSettingId.AutomationShellLinux]: { + restricted: true, + markdownDescription: localize({ + key: 'terminal.integrated.automationShell.linux', + comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] + }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`'), + type: ['string', 'null'], + default: null + }, + [TerminalSettingId.AutomationShellMacOs]: { + restricted: true, + markdownDescription: localize({ + key: 'terminal.integrated.automationShell.osx', + comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] + }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`'), + type: ['string', 'null'], + default: null + }, + [TerminalSettingId.AutomationShellWindows]: { + restricted: true, + markdownDescription: localize({ + key: 'terminal.integrated.automationShell.windows', + comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] + }, "A path that when set will override {0} and ignore {1} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`'), + type: ['string', 'null'], + default: null + }, + [TerminalSettingId.ShellLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: ['string', 'null'], + default: null, + markdownDeprecationMessage: shellDeprecationMessageLinux + }, + [TerminalSettingId.ShellMacOs]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: ['string', 'null'], + default: null, + markdownDeprecationMessage: shellDeprecationMessageOsx + }, + [TerminalSettingId.ShellWindows]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: ['string', 'null'], + default: null, + markdownDeprecationMessage: shellDeprecationMessageWindows + }, + [TerminalSettingId.ShellArgsLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'array', + items: { + type: 'string' + }, + default: [], + markdownDeprecationMessage: shellDeprecationMessageLinux + }, + [TerminalSettingId.ShellArgsMacOs]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'array', + items: { + type: 'string' + }, + // Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This + // is the reason terminals on macOS typically run login shells by default which set up + // the environment. See http://unix.stackexchange.com/a/119675/115410 + default: ['-l'], + markdownDeprecationMessage: shellDeprecationMessageOsx + }, + [TerminalSettingId.ShellArgsWindows]: { + restricted: true, + markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + 'anyOf': [ + { + type: 'array', + items: { + type: 'string', + markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + }, + }, + { + type: 'string', + markdownDescription: localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + } + ], + default: [], + markdownDeprecationMessage: shellDeprecationMessageWindows + }, + [TerminalSettingId.ProfilesWindows]: { + restricted: true, + markdownDescription: localize( + { + key: 'terminal.integrated.profiles.windows', + comment: ['{0}, {1}, and {2} are the `source`, `path` and optional `args` settings keys'] + }, + "The Windows profiles to present when creating a new terminal via the terminal dropdown. Set to null to exclude them, use the {0} property to use the default detected configuration. Or, set the {1} and optional {2}", '`source`', '`path`', '`args`.' + ), + type: 'object', + default: { + 'PowerShell': { + source: 'PowerShell', + icon: 'terminal-powershell' + }, + 'Command Prompt': { + path: [ + '${env:windir}\\Sysnative\\cmd.exe', + '${env:windir}\\System32\\cmd.exe' + ], + args: [], + icon: 'terminal-cmd' + }, + 'Git Bash': { + source: 'Git Bash' + } + }, + additionalProperties: { + 'anyOf': [ + { + type: 'object', + required: ['source'], + properties: { + source: { + description: localize('terminalProfile.windowsSource', 'A profile source that will auto detect the paths to the shell.'), + enum: ['PowerShell', 'Git Bash'] + }, + ...terminalProfileBaseProperties + } + }, + { type: 'null' }, + terminalProfileSchema + ] + } + }, + [TerminalSettingId.ProfilesMacOs]: { + restricted: true, + markdownDescription: localize( + { + key: 'terminal.integrated.profile.osx', + comment: ['{0} and {1} are the `path` and optional `args` settings keys'] + }, + "The macOS profiles to present when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0} and optional {1}", '`path`', '`args`.' + ), + type: 'object', + default: { + 'bash': { + path: 'bash', + args: ['-l'], + icon: 'terminal-bash' + }, + 'zsh': { + path: 'zsh', + args: ['-l'] + }, + 'fish': { + path: 'fish', + args: ['-l'] + }, + 'tmux': { + path: 'tmux', + icon: 'terminal-tmux' + }, + 'pwsh': { + path: 'pwsh', + icon: 'terminal-powershell' + } + }, + additionalProperties: { + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + } + }, + [TerminalSettingId.ProfilesLinux]: { + restricted: true, + markdownDescription: localize( + { + key: 'terminal.integrated.profile.linux', + comment: ['{0} and {1} are the `path` and optional `args` settings keys'] + }, + "The Linux profiles to present when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0} and optional {1}", '`path`', '`args`.' + ), + type: 'object', + default: { + 'bash': { + path: 'bash' + }, + 'zsh': { + path: 'zsh' + }, + 'fish': { + path: 'fish' + }, + 'tmux': { + path: 'tmux', + icon: 'terminal-tmux' + }, + 'pwsh': { + path: 'pwsh', + icon: 'terminal-powershell' + } + }, + additionalProperties: { + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + } + }, + [TerminalSettingId.UseWslProfiles]: { + description: localize('terminal.integrated.useWslProfiles', 'Controls whether or not WSL distros are shown in the terminal dropdown'), + type: 'boolean', + default: true + }, + [TerminalSettingId.InheritEnv]: { + scope: ConfigurationScope.APPLICATION, + description: localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from VS Code which may source a login shell to ensure $PATH and other development variables are initialized. This has no effect on Windows."), + type: 'boolean', + default: true + }, + } +}; + +/** + * Registers terminal configurations required by shared process and remote server. + */ +export function registerTerminalPlatformConfiguration() { + Registry.as(Extensions.Configuration).registerConfiguration(terminalPlatformConfiguration); + registerTerminalDefaultProfileConfiguration(); +} + +let lastDefaultProfilesConfiguration: IConfigurationNode | undefined; +export function registerTerminalDefaultProfileConfiguration(detectedProfiles?: { os: OperatingSystem, profiles: ITerminalProfile[] }) { + const registry = Registry.as(Extensions.Configuration); + if (lastDefaultProfilesConfiguration) { + registry.deregisterConfigurations([lastDefaultProfilesConfiguration]); + } + let enumValues: string[] | undefined = undefined; + let enumDescriptions: string[] | undefined = undefined; + if (detectedProfiles) { + const result = detectedProfiles.profiles.map(e => { + return { + name: e.profileName, + description: createProfileDescription(e) + }; + }); + enumValues = result.map(e => e.name); + enumDescriptions = result.map(e => e.description); + } + lastDefaultProfilesConfiguration = { + id: 'terminal', + order: 100, + title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), + type: 'object', + properties: { + [TerminalSettingId.DefaultProfileLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.defaultProfile.linux', "The default profile used on Linux. This setting will currently be ignored if either {0} or {1} are set.", '`#terminal.integrated.shell.linux#`', '`#terminal.integrated.shellArgs.linux#`'), + type: ['string', 'null'], + default: null, + enum: detectedProfiles?.os === OperatingSystem.Linux ? enumValues : undefined, + markdownEnumDescriptions: detectedProfiles?.os === OperatingSystem.Linux ? enumDescriptions : undefined + }, + [TerminalSettingId.DefaultProfileMacOs]: { + restricted: true, + markdownDescription: localize('terminal.integrated.defaultProfile.osx', "The default profile used on macOS. This setting will currently be ignored if either {0} or {1} are set.", '`#terminal.integrated.shell.osx#`', '`#terminal.integrated.shellArgs.osx#`'), + type: ['string', 'null'], + default: null, + enum: detectedProfiles?.os === OperatingSystem.Macintosh ? enumValues : undefined, + markdownEnumDescriptions: detectedProfiles?.os === OperatingSystem.Macintosh ? enumDescriptions : undefined + }, + [TerminalSettingId.DefaultProfileWindows]: { + restricted: true, + markdownDescription: localize('terminal.integrated.defaultProfile.windows', "The default profile used on Windows. This setting will currently be ignored if either {0} or {1} are set.", '`#terminal.integrated.shell.windows#`', '`#terminal.integrated.shellArgs.windows#`'), + type: ['string', 'null'], + default: null, + enum: detectedProfiles?.os === OperatingSystem.Windows ? enumValues : undefined, + markdownEnumDescriptions: detectedProfiles?.os === OperatingSystem.Windows ? enumDescriptions : undefined + }, + } + }; + registry.registerConfiguration(lastDefaultProfilesConfiguration); +} + +function createProfileDescription(profile: ITerminalProfile): string { + let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.profileName}\n- path: ${profile.path}`; + if (profile.args) { + if (typeof profile.args === 'string') { + description += `\n- args: "${profile.args}"`; + } else { + description += `\n- args: [${profile.args.length === 0 ? '' : profile.args.join(`','`)}]`; + } + } + if (profile.overrideName !== undefined) { + description += `\n- overrideName: ${profile.overrideName}`; + } + if (profile.color) { + description += `\n- color: ${profile.color}`; + } + if (profile.env) { + description += `\n- env: ${JSON.stringify(profile.env)}`; + } + return description; +} diff --git a/lib/vscode/src/vs/platform/terminal/common/terminalProcess.ts b/lib/vscode/src/vs/platform/terminal/common/terminalProcess.ts index 6cdf3b88e175..bc51bd66e7d9 100644 --- a/lib/vscode/src/vs/platform/terminal/common/terminalProcess.ts +++ b/lib/vscode/src/vs/platform/terminal/common/terminalProcess.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { UriComponents } from 'vs/base/common/uri'; -import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; export interface ISingleTerminalConfiguration { @@ -26,7 +26,6 @@ export interface ICompleteTerminalConfiguration { 'terminal.integrated.env.windows': ISingleTerminalConfiguration; 'terminal.integrated.env.osx': ISingleTerminalConfiguration; 'terminal.integrated.env.linux': ISingleTerminalConfiguration; - 'terminal.integrated.inheritEnv': boolean; 'terminal.integrated.cwd': string; 'terminal.integrated.detectLocale': 'auto' | 'off' | 'on'; } @@ -52,11 +51,13 @@ export interface IProcessDetails { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: TerminalIcon | undefined; + color: string | undefined; } export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; diff --git a/lib/vscode/src/vs/platform/terminal/common/terminalRecorder.ts b/lib/vscode/src/vs/platform/terminal/common/terminalRecorder.ts index e8a6e17c3667..68bd0f16fbcf 100644 --- a/lib/vscode/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/lib/vscode/src/vs/platform/terminal/common/terminalRecorder.ts @@ -26,7 +26,7 @@ export class TerminalRecorder { this._entries = [{ cols, rows, data: [] }]; } - public recordResize(cols: number, rows: number): void { + recordResize(cols: number, rows: number): void { if (this._entries.length > 0) { const lastEntry = this._entries[this._entries.length - 1]; if (lastEntry.data.length === 0) { @@ -52,7 +52,7 @@ export class TerminalRecorder { this._entries.push({ cols, rows, data: [] }); } - public recordData(data: string): void { + recordData(data: string): void { const lastEntry = this._entries[this._entries.length - 1]; lastEntry.data.push(data); @@ -76,7 +76,7 @@ export class TerminalRecorder { } } - public generateReplayEvent(): IPtyHostProcessReplayEvent { + generateReplayEvent(): IPtyHostProcessReplayEvent { // normalize entries to one element per data array this._entries.forEach((entry) => { if (entry.data.length > 0) { diff --git a/lib/vscode/src/vs/platform/terminal/node/ptyHostMain.ts b/lib/vscode/src/vs/platform/terminal/node/ptyHostMain.ts index d3b08384ec59..2e98f2259c5e 100644 --- a/lib/vscode/src/vs/platform/terminal/node/ptyHostMain.ts +++ b/lib/vscode/src/vs/platform/terminal/node/ptyHostMain.ts @@ -23,7 +23,11 @@ server.registerChannel(TerminalIpcChannels.Log, logChannel); const heartbeatService = new HeartbeatService(); server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(heartbeatService)); -const ptyService = new PtyService(lastPtyId, logService); +const reconnectConstants = { GraceTime: parseInt(process.env.VSCODE_RECONNECT_GRACE_TIME || '0'), ShortGraceTime: parseInt(process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME || '0') }; +delete process.env.VSCODE_RECONNECT_GRACE_TIME; +delete process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME; + +const ptyService = new PtyService(lastPtyId, logService, reconnectConstants); server.registerChannel(TerminalIpcChannels.PtyHost, ProxyChannel.fromService(ptyService)); process.once('exit', () => { diff --git a/lib/vscode/src/vs/platform/terminal/node/ptyHostService.ts b/lib/vscode/src/vs/platform/terminal/node/ptyHostService.ts index fba6308c4efb..9e00f75b5208 100644 --- a/lib/vscode/src/vs/platform/terminal/node/ptyHostService.ts +++ b/lib/vscode/src/vs/platform/terminal/node/ptyHostService.ts @@ -5,7 +5,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType, ITerminalProfile, IRequestResolveVariablesEvent, TitleEventSource, TerminalIcon, IReconnectConstants } from 'vs/platform/terminal/common/terminal'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { FileAccess } from 'vs/base/common/network'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -14,6 +14,9 @@ import { Emitter } from 'vs/base/common/event'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { detectAvailableProfiles } from 'vs/platform/terminal/node/terminalProfiles'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { registerTerminalPlatformConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; enum Constants { MaxRestarts = 5 @@ -25,6 +28,8 @@ enum Constants { */ let lastPtyId = 0; +let lastResolveVariablesRequestId = 0; + /** * This service implements IPtyService by launching a pty host process, forwarding messages to and * from the pty host process and manages the connection. @@ -51,6 +56,9 @@ export class PtyHostService extends Disposable implements IPtyService { readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event; private readonly _onPtyHostResponsive = this._register(new Emitter()); readonly onPtyHostResponsive = this._onPtyHostResponsive.event; + private readonly _onPtyHostRequestResolveVariables = this._register(new Emitter()); + readonly onPtyHostRequestResolveVariables = this._onPtyHostRequestResolveVariables.event; + private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>()); readonly onProcessData = this._onProcessData.event; private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); @@ -71,11 +79,17 @@ export class PtyHostService extends Disposable implements IPtyService { readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; constructor( + private readonly _reconnectConstants: IReconnectConstants, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private readonly _logService: ILogService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); + // Platform configuration is required on the process running the pty host (shared process or + // remote server). + registerTerminalPlatformConfiguration(); + this._register(toDisposable(() => this._disposePtyHost())); [this._client, this._proxy] = this._startPtyHost(); @@ -91,7 +105,9 @@ export class PtyHostService extends Disposable implements IPtyService { VSCODE_LAST_PTY_ID: lastPtyId, VSCODE_AMD_ENTRYPOINT: 'vs/platform/terminal/node/ptyHostMain', VSCODE_PIPE_LOGGING: 'true', - VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_VERBOSE_LOGGING: 'true', // transmit console logs from server to client, + VSCODE_RECONNECT_GRACE_TIME: this._reconnectConstants.GraceTime, + VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.ShortGraceTime } } ); @@ -122,6 +138,7 @@ export class PtyHostService extends Disposable implements IPtyService { // Setup logging const logChannel = client.getChannel(TerminalIpcChannels.Log); + LogLevelChannelClient.setLevel(logChannel, this._logService.getLevel()); this._register(this._logService.onDidChangeLogLevel(() => { LogLevelChannelClient.setLevel(logChannel, this._logService.getLevel()); })); @@ -153,6 +170,12 @@ export class PtyHostService extends Disposable implements IPtyService { lastPtyId = Math.max(lastPtyId, id); return id; } + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + return this._proxy.updateTitle(id, title, titleSource); + } + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise { + return this._proxy.updateIcon(id, icon, color); + } attachToProcess(id: number): Promise { return this._proxy.attachToProcess(id); } @@ -199,8 +222,14 @@ export class PtyHostService extends Disposable implements IPtyService { getDefaultSystemShell(osOverride?: OperatingSystem): Promise { return this._proxy.getDefaultSystemShell(osOverride); } - getShellEnvironment(): Promise { - return this._proxy.getShellEnvironment(); + async getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles: boolean = false): Promise { + return detectAvailableProfiles(profiles, defaultProfile, includeDetectedProfiles, this._configurationService, undefined, this._logService, this._resolveVariables.bind(this)); + } + getEnvironment(): Promise { + return this._proxy.getEnvironment(); + } + getWslPath(original: string): Promise { + return this._proxy.getWslPath(original); } setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { @@ -279,4 +308,22 @@ export class PtyHostService extends Disposable implements IPtyService { this._heartbeatSecondTimeout = undefined; } } + + private _pendingResolveVariablesRequests: Map void> = new Map(); + private _resolveVariables(text: string[]): Promise { + return new Promise(resolve => { + const id = ++lastResolveVariablesRequestId; + this._pendingResolveVariablesRequests.set(id, resolve); + this._onPtyHostRequestResolveVariables.fire({ id, originalText: text }); + }); + } + async acceptPtyHostResolvedVariables(id: number, resolved: string[]) { + const request = this._pendingResolveVariablesRequests.get(id); + if (request) { + request(resolved); + this._pendingResolveVariablesRequests.delete(id); + } else { + this._logService.warn(`Resolved variables received without matching request ${id}`); + } + } } diff --git a/lib/vscode/src/vs/platform/terminal/node/ptyService.ts b/lib/vscode/src/vs/platform/terminal/node/ptyService.ts index d9ea875c4082..d18817bcf0bf 100644 --- a/lib/vscode/src/vs/platform/terminal/node/ptyService.ts +++ b/lib/vscode/src/vs/platform/terminal/node/ptyService.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessEnvironment, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType, IProcessReadyEvent, TitleEventSource, TerminalIcon, IReconnectConstants } from 'vs/platform/terminal/common/terminal'; import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; @@ -14,6 +14,10 @@ import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IProcessDetails, import { ILogService } from 'vs/platform/log/common/log'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { getSystemShell } from 'vs/base/node/shell'; +import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; +import { execFile } from 'child_process'; +import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; +import { URI } from 'vs/base/common/uri'; type WorkspaceId = string; @@ -47,7 +51,8 @@ export class PtyService extends Disposable implements IPtyService { constructor( private _lastPtyId: number, - private readonly _logService: ILogService + private readonly _logService: ILogService, + private readonly _reconnectConstants: IReconnectConstants ) { super(); @@ -88,7 +93,7 @@ export class PtyService extends Disposable implements IPtyService { if (process.onProcessResolvedShellLaunchConfig) { process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event })); } - const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService, shellLaunchConfig.icon); + const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._reconnectConstants, this._logService, shellLaunchConfig.icon); process.onProcessExit(() => { persistentProcess.dispose(); this._ptys.delete(id); @@ -111,6 +116,14 @@ export class PtyService extends Disposable implements IPtyService { } } + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + this._throwIfNoPty(id).setTitle(title, titleSource); + } + + async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { + this._throwIfNoPty(id).setIcon(icon, color); + } + async detachFromProcess(id: number): Promise { this._throwIfNoPty(id).detach(); } @@ -166,10 +179,25 @@ export class PtyService extends Disposable implements IPtyService { return getSystemShell(osOverride, process.env); } - async getShellEnvironment(): Promise { + async getEnvironment(): Promise { return { ...process.env }; } + async getWslPath(original: string): Promise { + if (!isWindows) { + return original; + } + if (getWindowsBuildNumber() < 17063) { + return original.replace(/\\/g, '/'); + } + return new Promise(c => { + const proc = execFile('bash.exe', ['-c', `wslpath ${escapeNonWindowsPath(original)}`], {}, (error, stdout, stderr) => { + c(escapeNonWindowsPath(stdout.trim())); + }); + proc.stdin!.end(); + }); + } + async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { this._workspaceLayoutInfos.set(args.workspaceId, args); } @@ -178,10 +206,8 @@ export class PtyService extends Disposable implements IPtyService { const layout = this._workspaceLayoutInfos.get(args.workspaceId); if (layout) { const expandedTabs = await Promise.all(layout.tabs.map(async tab => this._expandTerminalTab(tab))); - const filtered = expandedTabs.filter(t => t.terminals.length > 0); - return { - tabs: filtered - }; + const tabs = expandedTabs.filter(t => t.terminals.length > 0); + return { tabs }; } return undefined; } @@ -219,12 +245,14 @@ export class PtyService extends Disposable implements IPtyService { return { id, title: persistentProcess.title, + titleSource: persistentProcess.titleSource, pid: persistentProcess.pid, workspaceId: persistentProcess.workspaceId, workspaceName: persistentProcess.workspaceName, cwd, isOrphan, - icon: persistentProcess.icon + icon: persistentProcess.icon, + color: persistentProcess.color }; } @@ -254,7 +282,7 @@ export class PersistentTerminalProcess extends Disposable { private readonly _onProcessReplay = this._register(new Emitter()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); + private readonly _onProcessReady = this._register(new Emitter()); readonly onProcessReady = this._onProcessReady.event; private readonly _onProcessTitleChanged = this._register(new Emitter()); readonly onProcessTitleChanged = this._onProcessTitleChanged.event; @@ -271,33 +299,49 @@ export class PersistentTerminalProcess extends Disposable { private _pid = -1; private _cwd = ''; + private _title: string | undefined; + private _titleSource: TitleEventSource = TitleEventSource.Process; get pid(): number { return this._pid; } - get title(): string { return this._terminalProcess.currentTitle; } - get icon(): string | undefined { return this._icon; } + get title(): string { return this._title || this._terminalProcess.currentTitle; } + get titleSource(): TitleEventSource { return this._titleSource; } + get icon(): TerminalIcon | undefined { return this._icon; } + get color(): string | undefined { return this._color; } + + setTitle(title: string, titleSource: TitleEventSource): void { + this._title = title; + this._titleSource = titleSource; + } + + setIcon(icon: TerminalIcon, color?: string): void { + this._icon = icon; + this._color = color; + } constructor( private _persistentProcessId: number, private readonly _terminalProcess: TerminalProcess, - public readonly workspaceId: string, - public readonly workspaceName: string, - public readonly shouldPersistTerminal: boolean, + readonly workspaceId: string, + readonly workspaceName: string, + readonly shouldPersistTerminal: boolean, cols: number, rows: number, + reconnectConstants: IReconnectConstants, private readonly _logService: ILogService, - private readonly _icon?: string + private _icon?: TerminalIcon, + private _color?: string ) { super(); this._recorder = new TerminalRecorder(cols, rows); this._orphanQuestionBarrier = null; this._orphanQuestionReplyTime = 0; this._disconnectRunner1 = this._register(new RunOnceScheduler(() => { - this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, shutting down pid "${this._pid}"`); + this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(reconnectConstants.GraceTime)} has expired, shutting down pid "${this._pid}"`); this.shutdown(true); - }, LocalReconnectConstants.ReconnectionGraceTime)); + }, reconnectConstants.GraceTime)); this._disconnectRunner2 = this._register(new RunOnceScheduler(() => { - this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, shutting down pid ${this._pid}`); + this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(reconnectConstants.ShortGraceTime)} has expired, shutting down pid ${this._pid}`); this.shutdown(true); - }, LocalReconnectConstants.ReconnectionShortGraceTime)); + }, reconnectConstants.ShortGraceTime)); this._register(this._terminalProcess.onProcessReady(e => { this._pid = e.pid; @@ -337,7 +381,7 @@ export class PersistentTerminalProcess extends Disposable { } this._isStarted = true; } else { - this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd }); + this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle); this._onProcessShellTypeChanged.fire(this._terminalProcess.shellType); this.triggerReplay(); diff --git a/lib/vscode/src/vs/platform/terminal/node/terminalProcess.ts b/lib/vscode/src/vs/platform/terminal/node/terminalProcess.ts index 3626928f23c3..b37b6ab2c15d 100644 --- a/lib/vscode/src/vs/platform/terminal/node/terminalProcess.ts +++ b/lib/vscode/src/vs/platform/terminal/node/terminalProcess.ts @@ -9,7 +9,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride, TerminalShellType, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { exec } from 'child_process'; import { ILogService } from 'vs/platform/log/common/log'; import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; @@ -18,6 +18,7 @@ import { localize } from 'vs/nls'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { timeout } from 'vs/base/common/async'; +import { Promises } from 'vs/base/node/pfs'; // Writing large amounts of data can be corrupted for some reason, after looking into this is // appears to be a race condition around writing to the FD which may be based on how powerful the @@ -90,21 +91,21 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _isPtyPaused: boolean = false; private _unacknowledgedCharCount: number = 0; - public get exitMessage(): string | undefined { return this._exitMessage; } + get exitMessage(): string | undefined { return this._exitMessage; } - public get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } - public get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; } + get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } + get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; } private readonly _onProcessData = this._register(new Emitter()); - public get onProcessData(): Event { return this._onProcessData.event; } + get onProcessData(): Event { return this._onProcessData.event; } private readonly _onProcessExit = this._register(new Emitter()); - public get onProcessExit(): Event { return this._onProcessExit.event; } - private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); - public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } + get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessReady = this._register(new Emitter()); + get onProcessReady(): Event { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = this._register(new Emitter()); - public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } + get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; + readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; constructor( private readonly _shellLaunchConfig: IShellLaunchConfig, @@ -164,7 +165,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess onProcessOverrideDimensions?: Event | undefined; onProcessResolvedShellLaunchConfig?: Event | undefined; - public async start(): Promise { + async start(): Promise { const results = await Promise.all([this._validateCwd(), this._validateExecutable()]); const firstError = results.find(r => r !== undefined); if (firstError) { @@ -182,7 +183,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private async _validateCwd(): Promise { try { - const result = await fs.promises.stat(this._initialCwd); + const result = await Promises.stat(this._initialCwd); if (!result.isDirectory()) { return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) }; } @@ -200,7 +201,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess throw new Error('IShellLaunchConfig.executable not set'); } try { - const result = await fs.promises.stat(slc.executable); + const result = await Promises.stat(slc.executable); if (!result.isFile() && !result.isSymbolicLink()) { return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file of a symlink", slc.executable) }; } @@ -239,7 +240,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess ptyProcess.pause(); } - // Refire the data event this._onProcessData.fire(data); if (this._closeTimeout) { @@ -251,11 +251,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._exitCode = e.exitCode; this._queueProcessExit(); }); - this._setupTitlePolling(ptyProcess); this._sendProcessId(ptyProcess.pid); + this._setupTitlePolling(ptyProcess); } - public override dispose(): void { + override dispose(): void { this._isDisposed = true; if (this._titleInterval) { clearInterval(this._titleInterval); @@ -266,7 +266,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _setupTitlePolling(ptyProcess: pty.IPty) { // Send initial timeout async to give event listeners a chance to init - setTimeout(() => this._sendProcessTitle(ptyProcess), 0); + setTimeout(() => this._sendProcessTitle(ptyProcess)); // Setup polling for non-Windows, for Windows `process` doesn't change if (!isWindows) { this._titleInterval = setInterval(() => { @@ -325,7 +325,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } private _sendProcessId(pid: number) { - this._onProcessReady.fire({ pid, cwd: this._initialCwd }); + this._onProcessReady.fire({ pid, cwd: this._initialCwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); } private _sendProcessTitle(ptyProcess: pty.IPty): void { @@ -336,7 +336,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._onProcessTitleChanged.fire(this._currentTitle); } - public shutdown(immediate: boolean): void { + shutdown(immediate: boolean): void { // don't force immediate disposal of the terminal processes on Windows as an additional // mitigation for https://github.com/microsoft/vscode/issues/71966 which causes the pty host // to become unresponsive, disconnecting all terminals across all windows. @@ -356,7 +356,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public input(data: string, isBinary?: boolean): void { + input(data: string, isBinary?: boolean): void { if (this._isDisposed || !this._ptyProcess) { return; } @@ -370,7 +370,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._startWrite(); } - public async processBinary(data: string): Promise { + async processBinary(data: string): Promise { this.input(data, true); } @@ -404,7 +404,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public resize(cols: number, rows: number): void { + resize(cols: number, rows: number): void { if (this._isDisposed) { return; } @@ -437,7 +437,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public acknowledgeDataEvent(charCount: number): void { + acknowledgeDataEvent(charCount: number): void { // Prevent lower than 0 to heal from errors this._unacknowledgedCharCount = Math.max(this._unacknowledgedCharCount - charCount, 0); this._logService.trace(`Flow control: Ack ${charCount} chars (unacknowledged: ${this._unacknowledgedCharCount})`); @@ -448,7 +448,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public clearUnacknowledgedChars(): void { + clearUnacknowledgedChars(): void { this._unacknowledgedCharCount = 0; this._logService.trace(`Flow control: Cleared all unacknowledged chars, forcing resume`); if (this._isPtyPaused) { @@ -457,11 +457,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - public getInitialCwd(): Promise { + getInitialCwd(): Promise { return Promise.resolve(this._initialCwd); } - public getCwd(): Promise { + getCwd(): Promise { if (isMacintosh) { // Disable cwd lookup on macOS Big Sur due to spawn blocking thread (darwin v20 is macOS // Big Sur) https://github.com/Microsoft/vscode/issues/105446 @@ -506,7 +506,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess }); } - public getLatency(): Promise { + getLatency(): Promise { return Promise.resolve(0); } } @@ -515,12 +515,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess * Tracks the latest resize event to be trigger at a later point. */ class DelayedResizer extends Disposable { - public rows: number | undefined; - public cols: number | undefined; + rows: number | undefined; + cols: number | undefined; private _timeout: NodeJS.Timeout; private readonly _onTrigger = this._register(new Emitter<{ rows?: number, cols?: number }>()); - public get onTrigger(): Event<{ rows?: number, cols?: number }> { return this._onTrigger.event; } + get onTrigger(): Event<{ rows?: number, cols?: number }> { return this._onTrigger.event; } constructor() { super(); diff --git a/lib/vscode/src/vs/platform/terminal/node/terminalProfiles.ts b/lib/vscode/src/vs/platform/terminal/node/terminalProfiles.ts new file mode 100644 index 000000000000..3096641c5ded --- /dev/null +++ b/lib/vscode/src/vs/platform/terminal/node/terminalProfiles.ts @@ -0,0 +1,362 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { normalize, basename, delimiter } from 'vs/base/common/path'; +import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; +import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; +import * as cp from 'child_process'; +import { ILogService } from 'vs/platform/log/common/log'; +import * as pfs from 'vs/base/node/pfs'; +import { ITerminalEnvironment, ITerminalProfile, ITerminalProfileObject, ProfileSource, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { Codicon } from 'vs/base/common/codicons'; +import { isLinux, isWindows } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +let profileSources: Map | undefined; + +export function detectAvailableProfiles( + profiles: unknown, + defaultProfile: unknown, + includeDetectedProfiles: boolean, + configurationService: IConfigurationService, + fsProvider?: IFsProvider, + logService?: ILogService, + variableResolver?: (text: string[]) => Promise, + testPaths?: string[] +): Promise { + fsProvider = fsProvider || { + existsFile: pfs.SymlinkSupport.existsFile, + readFile: pfs.Promises.readFile + }; + if (isWindows) { + return detectAvailableWindowsProfiles( + includeDetectedProfiles, + fsProvider, + logService, + configurationService.getValue(TerminalSettingId.UseWslProfiles) !== false, + profiles && typeof profiles === 'object' ? { ...profiles } : configurationService.getValue<{ [key: string]: ITerminalProfileObject }>(TerminalSettingId.ProfilesWindows), + typeof defaultProfile === 'string' ? defaultProfile : configurationService.getValue(TerminalSettingId.DefaultProfileWindows), + testPaths, + variableResolver + ); + } + return detectAvailableUnixProfiles( + fsProvider, + logService, + includeDetectedProfiles, + profiles && typeof profiles === 'object' ? { ...profiles } : configurationService.getValue<{ [key: string]: ITerminalProfileObject }>(isLinux ? TerminalSettingId.ProfilesLinux : TerminalSettingId.ProfilesMacOs), + typeof defaultProfile === 'string' ? defaultProfile : configurationService.getValue(isLinux ? TerminalSettingId.DefaultProfileLinux : TerminalSettingId.DefaultProfileMacOs), + testPaths, + variableResolver + ); +} + +async function detectAvailableWindowsProfiles( + includeDetectedProfiles: boolean, + fsProvider: IFsProvider, + logService?: ILogService, + useWslProfiles?: boolean, + configProfiles?: { [key: string]: ITerminalProfileObject }, + defaultProfileName?: string, + testPaths?: string[], + variableResolver?: (text: string[]) => Promise +): Promise { + // Determine the correct System32 path. We want to point to Sysnative + // when the 32-bit version of VS Code is running on a 64-bit machine. + // The reason for this is because PowerShell's important PSReadline + // module doesn't work if this is not the case. See #27915. + const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const system32Path = `${process.env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}`; + + let useWSLexe = false; + + if (getWindowsBuildNumber() >= 16299) { + useWSLexe = true; + } + + await initializeWindowsProfiles(testPaths); + + const detectedProfiles: Map = new Map(); + + // Add auto detected profiles + if (includeDetectedProfiles) { + detectedProfiles.set('PowerShell', { + source: ProfileSource.Pwsh, + icon: Codicon.terminalPowershell, + isAutoDetected: true + }); + detectedProfiles.set('Windows PowerShell', { + path: `${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`, + icon: Codicon.terminalPowershell, + isAutoDetected: true + }); + detectedProfiles.set('Git Bash', { + source: ProfileSource.GitBash, + isAutoDetected: true + }); + detectedProfiles.set('Cygwin', { + path: [ + `${process.env['HOMEDRIVE']}\\cygwin64\\bin\\bash.exe`, + `${process.env['HOMEDRIVE']}\\cygwin\\bin\\bash.exe` + ], + args: ['--login'], + isAutoDetected: true + }); + detectedProfiles.set('Command Prompt', { + path: `${system32Path}\\cmd.exe`, + icon: Codicon.terminalCmd, + isAutoDetected: true + }); + } + + applyConfigProfilesToMap(configProfiles, detectedProfiles); + + const resultProfiles: ITerminalProfile[] = await transformToTerminalProfiles(detectedProfiles.entries(), defaultProfileName, fsProvider, logService, variableResolver); + + if (includeDetectedProfiles || (!includeDetectedProfiles && useWslProfiles)) { + try { + const result = await getWslProfiles(`${system32Path}\\${useWSLexe ? 'wsl' : 'bash'}.exe`, defaultProfileName); + for (const wslProfile of result) { + if (!configProfiles || !(wslProfile.profileName in configProfiles)) { + resultProfiles.push(wslProfile); + } + } + } catch (e) { + logService?.info('WSL is not installed, so could not detect WSL profiles'); + } + } + + return resultProfiles; +} + +async function transformToTerminalProfiles( + entries: IterableIterator<[string, ITerminalProfileObject]>, + defaultProfileName: string | undefined, + fsProvider: IFsProvider, + logService?: ILogService, + variableResolver?: (text: string[]) => Promise +): Promise { + const resultProfiles: ITerminalProfile[] = []; + for (const [profileName, profile] of entries) { + if (profile === null) { continue; } + let originalPaths: string[]; + let args: string[] | string | undefined; + let icon: ThemeIcon | URI | { light: URI, dark: URI } | undefined = undefined; + if ('source' in profile) { + const source = profileSources?.get(profile.source); + if (!source) { + continue; + } + originalPaths = source.paths; + + // if there are configured args, override the default ones + args = profile.args || source.args; + if (profile.icon) { + icon = profile.icon; + } else if (source.icon) { + icon = source.icon; + } + } else { + originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path]; + args = isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined; + icon = profile.icon || undefined; + } + + const paths = (await variableResolver?.(originalPaths)) || originalPaths.slice(); + const validatedProfile = await validateProfilePaths(profileName, defaultProfileName, paths, fsProvider, args, profile.env, profile.overrideName, profile.isAutoDetected, logService); + if (validatedProfile) { + validatedProfile.isAutoDetected = profile.isAutoDetected; + validatedProfile.icon = icon; + validatedProfile.color = profile.color; + resultProfiles.push(validatedProfile); + } else { + logService?.trace('profile not validated', profileName, originalPaths); + } + } + return resultProfiles; +} + +async function initializeWindowsProfiles(testPaths?: string[]): Promise { + if (profileSources) { + return; + } + + profileSources = new Map(); + profileSources.set( + 'Git Bash', { + profileName: 'Git Bash', + paths: [ + `${process.env['ProgramW6432']}\\Git\\bin\\bash.exe`, + `${process.env['ProgramW6432']}\\Git\\usr\\bin\\bash.exe`, + `${process.env['ProgramFiles']}\\Git\\bin\\bash.exe`, + `${process.env['ProgramFiles']}\\Git\\usr\\bin\\bash.exe`, + `${process.env['LocalAppData']}\\Programs\\Git\\bin\\bash.exe`, + `${process.env['UserProfile']}\\scoop\\apps\\git-with-openssh\\current\\bin\\bash.exe`, + `${process.env['AllUsersProfile']}\\scoop\\apps\\git-with-openssh\\current\\bin\\bash.exe` + ], + args: ['--login'] + }); + profileSources.set('PowerShell', { + profileName: 'PowerShell', + paths: testPaths || await getPowershellPaths(), + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) + }); +} + +async function getPowershellPaths(): Promise { + const paths: string[] = []; + // Add all of the different kinds of PowerShells + for await (const pwshExe of enumeratePowerShellInstallations()) { + paths.push(pwshExe.exePath); + } + return paths; +} + +async function getWslProfiles(wslPath: string, defaultProfileName: string | undefined): Promise { + const profiles: ITerminalProfile[] = []; + const distroOutput = await new Promise((resolve, reject) => { + // wsl.exe output is encoded in utf16le (ie. A -> 0x4100) + cp.exec('wsl.exe -l -q', { encoding: 'utf16le' }, (err, stdout) => { + if (err) { + return reject('Problem occurred when getting wsl distros'); + } + resolve(stdout); + }); + }); + if (!distroOutput) { + return []; + } + const regex = new RegExp(/[\r?\n]/); + const distroNames = distroOutput.split(regex).filter(t => t.trim().length > 0 && t !== ''); + for (const distroName of distroNames) { + // Skip empty lines + if (distroName === '') { + continue; + } + + // docker-desktop and docker-desktop-data are treated as implementation details of + // Docker Desktop for Windows and therefore not exposed + if (distroName.startsWith('docker-desktop')) { + continue; + } + + // Create the profile, adding the icon depending on the distro + const profileName = `${distroName} (WSL)`; + const profile: ITerminalProfile = { + profileName, + path: wslPath, + args: [`-d`, `${distroName}`], + isDefault: profileName === defaultProfileName, + icon: getWslIcon(distroName) + }; + // Add the profile + profiles.push(profile); + } + return profiles; +} + +function getWslIcon(distroName: string): ThemeIcon { + if (distroName.includes('Ubuntu')) { + return ThemeIcon.asThemeIcon(Codicon.terminalUbuntu); + } else if (distroName.includes('Debian')) { + return ThemeIcon.asThemeIcon(Codicon.terminalDebian); + } else { + return ThemeIcon.asThemeIcon(Codicon.terminalLinux); + } +} + +async function detectAvailableUnixProfiles( + fsProvider: IFsProvider, + logService?: ILogService, + includeDetectedProfiles?: boolean, + configProfiles?: { [key: string]: ITerminalProfileObject }, + defaultProfileName?: string, + testPaths?: string[], + variableResolver?: (text: string[]) => Promise +): Promise { + const detectedProfiles: Map = new Map(); + + // Add non-quick launch profiles + if (includeDetectedProfiles) { + const contents = (await fsProvider.readFile('/etc/shells')).toString(); + const profiles = testPaths || contents.split('\n').filter(e => e.trim().indexOf('#') !== 0 && e.trim().length > 0); + const counts: Map = new Map(); + for (const profile of profiles) { + let profileName = basename(profile); + let count = counts.get(profileName) || 0; + count++; + if (count > 1) { + profileName = `${profileName} (${count})`; + } + counts.set(profileName, count); + detectedProfiles.set(profileName, { path: profile, isAutoDetected: true }); + } + } + + applyConfigProfilesToMap(configProfiles, detectedProfiles); + + return await transformToTerminalProfiles(detectedProfiles.entries(), defaultProfileName, fsProvider, logService, variableResolver); +} + +function applyConfigProfilesToMap(configProfiles: { [key: string]: ITerminalProfileObject } | undefined, profilesMap: Map) { + if (!configProfiles) { + return; + } + for (const [profileName, value] of Object.entries(configProfiles)) { + if (value === null || (!('path' in value) && !('source' in value))) { + profilesMap.delete(profileName); + } else { + profilesMap.set(profileName, value); + } + } +} + +async function validateProfilePaths(profileName: string, defaultProfileName: string | undefined, potentialPaths: string[], fsProvider: IFsProvider, args?: string[] | string, env?: ITerminalEnvironment, overrideName?: boolean, isAutoDetected?: boolean, logService?: ILogService): Promise { + if (potentialPaths.length === 0) { + return Promise.resolve(undefined); + } + const path = potentialPaths.shift()!; + if (path === '') { + return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, args, env, overrideName, isAutoDetected); + } + + const profile: ITerminalProfile = { profileName, path, args, env, overrideName, isAutoDetected, isDefault: profileName === defaultProfileName }; + + // For non-absolute paths, check if it's available on $PATH + if (basename(path) === path) { + // The executable isn't an absolute path, try find it on the PATH + const envPaths: string[] | undefined = process.env.PATH ? process.env.PATH.split(delimiter) : undefined; + const executable = await findExecutable(path, undefined, envPaths, undefined, fsProvider.existsFile); + if (!executable) { + return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, args); + } + return profile; + } + + const result = await fsProvider.existsFile(normalize(path)); + if (result) { + return profile; + } + + return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, args, env, overrideName, isAutoDetected); +} + +export interface IFsProvider { + existsFile(path: string): Promise, + readFile(path: string): Promise; +} + +export interface IProfileVariableResolver { + resolve(text: string[]): Promise; +} + +interface IPotentialTerminalProfile { + profileName: string; + paths: string[]; + args?: string[]; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; +} diff --git a/lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts b/lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts index 64c3d205db2a..efa33b6e5f6f 100644 --- a/lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -38,15 +38,15 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe private _isDisposed: boolean; private _currentRequest: Promise | undefined; private _shellType: TerminalShellType | undefined; - public get shellType(): TerminalShellType | undefined { return this._shellType; } + get shellType(): TerminalShellType | undefined { return this._shellType; } private _shellTitle: string = ''; - public get shellTitle(): string { return this._shellTitle; } + get shellTitle(): string { return this._shellTitle; } private readonly _onShellNameChanged = new Emitter(); - public get onShellNameChanged(): Event { return this._onShellNameChanged.event; } + get onShellNameChanged(): Event { return this._onShellNameChanged.event; } private readonly _onShellTypeChanged = new Emitter(); - public get onShellTypeChanged(): Event { return this._onShellTypeChanged.event; } + get onShellTypeChanged(): Event { return this._onShellTypeChanged.event; } - public constructor( + constructor( private _rootProcessId: number ) { super(); @@ -112,7 +112,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return this.traverseTree(tree.children[favouriteChild]); } - public override dispose(): void { + override dispose(): void { this._isDisposed = true; super.dispose(); } @@ -120,7 +120,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe /** * Returns the innermost shell executable running in the terminal */ - public getShellName(): Promise { + getShellName(): Promise { if (this._isDisposed) { return Promise.resolve(''); } @@ -141,7 +141,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return this._currentRequest; } - public getShellType(executable: string): TerminalShellType { + getShellType(executable: string): TerminalShellType { switch (executable.toLowerCase()) { case 'cmd.exe': return WindowsShellType.CommandPrompt; diff --git a/lib/vscode/src/vs/platform/theme/common/colorRegistry.ts b/lib/vscode/src/vs/platform/theme/common/colorRegistry.ts index 0d0f8683aef0..1cea9bb26d81 100644 --- a/lib/vscode/src/vs/platform/theme/common/colorRegistry.ts +++ b/lib/vscode/src/vs/platform/theme/common/colorRegistry.ts @@ -11,6 +11,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as nls from 'vs/nls'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { assertNever } from 'vs/base/common/types'; // ------ API types @@ -24,11 +25,21 @@ export interface ColorContribution { readonly deprecationMessage: string | undefined; } - -export interface ColorFunction { - (theme: IColorTheme): Color | undefined; +export const enum ColorTransformType { + Darken, + Lighten, + Transparent, + OneOf, + LessProminent, } +export type ColorTransform = + | { op: ColorTransformType.Darken; value: ColorValue; factor: number } + | { op: ColorTransformType.Lighten; value: ColorValue; factor: number } + | { op: ColorTransformType.Transparent; value: ColorValue; factor: number } + | { op: ColorTransformType.OneOf; values: readonly ColorValue[] } + | { op: ColorTransformType.LessProminent; value: ColorValue; background: ColorValue; factor: number; transparency: number }; + export interface ColorDefaults { light: ColorValue | null; dark: ColorValue | null; @@ -38,7 +49,7 @@ export interface ColorDefaults { /** * A Color Value is either a color literal, a reference to an other color or a derived color */ -export type ColorValue = Color | string | ColorIdentifier | ColorFunction; +export type ColorValue = Color | string | ColorIdentifier | ColorTransform; // color registry export const Extensions = { @@ -344,8 +355,8 @@ export const editorActiveLinkForeground = registerColor('editorLink.activeForegr /** * Inline hints */ -export const editorInlineHintForeground = registerColor('editorInlineHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlineHintForeground', 'Foreground color of inline hints')); -export const editorInlineHintBackground = registerColor('editorInlineHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlineHintBackground', 'Background color of inline hints')); +export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); +export const editorInlayHintBackground = registerColor('editorInlayHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlayHintBackground', 'Background color of inline hints')); /** * Editor lighbulb icon colors @@ -383,7 +394,8 @@ export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); -export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#0097fb', light: '#0066BF', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); +export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#18A3FF', light: '#0066BF', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); +export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hc: '#B89500' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hc: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); export const listWarningForeground = registerColor('list.warningForeground', { dark: '#CCA700', light: '#855F00', hc: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); @@ -400,7 +412,8 @@ export const listDeemphasizedForeground = registerColor('list.deemphasizedForegr * Quick pick widget (dependent on List and tree colors) */ export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', { dark: null, light: null, hc: null }, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); -export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listFocusBackground, '#062F4A'), light: oneOf(_deprecatedQuickInputListFocusBackground, listFocusBackground, '#D6EBFF'), hc: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); +export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hc: listActiveSelectionForeground }, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); +export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground, '#062F4A'), light: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground, '#D6EBFF'), hc: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); /** * Menu colors @@ -493,63 +506,63 @@ export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', li // ----- color functions -export function darken(colorValue: ColorValue, factor: number): ColorFunction { - return (theme) => { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color.darken(factor); - } - return undefined; - }; +export function executeTransform(transform: ColorTransform, theme: IColorTheme) { + switch (transform.op) { + case ColorTransformType.Darken: + return resolveColorValue(transform.value, theme)?.darken(transform.factor); + + case ColorTransformType.Lighten: + return resolveColorValue(transform.value, theme)?.lighten(transform.factor); + + case ColorTransformType.Transparent: + return resolveColorValue(transform.value, theme)?.transparent(transform.factor); + + case ColorTransformType.OneOf: + for (const candidate of transform.values) { + const color = resolveColorValue(candidate, theme); + if (color) { + return color; + } + } + return undefined; + + case ColorTransformType.LessProminent: + const from = resolveColorValue(transform.value, theme); + if (!from) { + return undefined; + } + + const backgroundColor = resolveColorValue(transform.background, theme); + if (!backgroundColor) { + return from.transparent(transform.factor * transform.transparency); + } + + return from.isDarkerThan(backgroundColor) + ? Color.getLighterColor(from, backgroundColor, transform.factor).transparent(transform.transparency) + : Color.getDarkerColor(from, backgroundColor, transform.factor).transparent(transform.transparency); + default: + throw assertNever(transform); + } } -export function lighten(colorValue: ColorValue, factor: number): ColorFunction { - return (theme) => { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color.lighten(factor); - } - return undefined; - }; +export function darken(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Darken, value: colorValue, factor }; } -export function transparent(colorValue: ColorValue, factor: number): ColorFunction { - return (theme) => { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color.transparent(factor); - } - return undefined; - }; +export function lighten(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Lighten, value: colorValue, factor }; } -export function oneOf(...colorValues: ColorValue[]): ColorFunction { - return (theme) => { - for (let colorValue of colorValues) { - let color = resolveColorValue(colorValue, theme); - if (color) { - return color; - } - } - return undefined; - }; +export function transparent(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Transparent, value: colorValue, factor }; } -function lessProminent(colorValue: ColorValue, backgroundColorValue: ColorValue, factor: number, transparency: number): ColorFunction { - return (theme) => { - let from = resolveColorValue(colorValue, theme); - if (from) { - let backgroundColor = resolveColorValue(backgroundColorValue, theme); - if (backgroundColor) { - if (from.isDarkerThan(backgroundColor)) { - return Color.getLighterColor(from, backgroundColor, factor).transparent(transparency); - } - return Color.getDarkerColor(from, backgroundColor, factor).transparent(transparency); - } - return from.transparent(factor * transparency); - } - return undefined; - }; +export function oneOf(...colorValues: ColorValue[]): ColorTransform { + return { op: ColorTransformType.OneOf, values: colorValues }; +} + +function lessProminent(colorValue: ColorValue, backgroundColorValue: ColorValue, factor: number, transparency: number): ColorTransform { + return { op: ColorTransformType.LessProminent, value: colorValue, background: backgroundColorValue, factor, transparency }; } // ----- implementation @@ -567,8 +580,8 @@ export function resolveColorValue(colorValue: ColorValue | null, theme: IColorTh return theme.getColor(colorValue); } else if (colorValue instanceof Color) { return colorValue; - } else if (typeof colorValue === 'function') { - return colorValue(theme); + } else if (typeof colorValue === 'object') { + return executeTransform(colorValue, theme); } return undefined; } diff --git a/lib/vscode/src/vs/platform/theme/common/styler.ts b/lib/vscode/src/vs/platform/theme/common/styler.ts index 9827da48c836..6962116082c9 100644 --- a/lib/vscode/src/vs/platform/theme/common/styler.ts +++ b/lib/vscode/src/vs/platform/theme/common/styler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline, tableColumnsBorder, quickInputListFocusBackground, buttonBorder, keybindingLabelForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline, tableColumnsBorder, quickInputListFocusBackground, buttonBorder, keybindingLabelForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, quickInputListFocusForeground, ColorTransform } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { IThemable, styleFn } from 'vs/base/common/styler'; @@ -35,7 +35,7 @@ export function computeStyles(theme: IColorTheme, styleMap: IColorMapping): ICom } export function attachStyler(themeService: IThemeService, styleMap: T, widgetOrCallback: IThemable | styleFn): IDisposable { - function applyStyles(theme: IColorTheme): void { + function applyStyles(): void { const styles = computeStyles(themeService.getColorTheme(), styleMap); if (typeof widgetOrCallback === 'function') { @@ -45,7 +45,7 @@ export function attachStyler(themeService: IThemeServic } } - applyStyles(themeService.getColorTheme()); + applyStyles(); return themeService.onDidColorThemeChange(applyStyles); } @@ -130,7 +130,7 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer selectBorder: style?.selectBorder || selectBorder, focusBorder: style?.focusBorder || focusBorder, listFocusBackground: style?.listFocusBackground || quickInputListFocusBackground, - listFocusForeground: style?.listFocusForeground || listFocusForeground, + listFocusForeground: style?.listFocusForeground || quickInputListFocusForeground, listFocusOutline: style?.listFocusOutline || ((theme: IColorTheme) => theme.type === ColorScheme.HIGH_CONTRAST ? activeContrastBorder : Color.transparent), listHoverBackground: style?.listHoverBackground || listHoverBackground, listHoverForeground: style?.listHoverForeground || listHoverForeground, @@ -254,16 +254,6 @@ export function attachKeybindingLabelStyler(widget: IThemable, themeService: ITh } as IKeybindingLabelStyleOverrides, widget); } -export interface ILinkStyleOverrides extends IStyleOverrides { - textLinkForeground?: ColorIdentifier; -} - -export function attachLinkStyler(widget: IThemable, themeService: IThemeService, style?: ILinkStyleOverrides): IDisposable { - return attachStyler(themeService, { - textLinkForeground: style?.textLinkForeground || textLinkForeground, - } as ILinkStyleOverrides, widget); -} - export interface IProgressBarStyleOverrides extends IStyleOverrides { progressBarBackground?: ColorIdentifier; } @@ -279,7 +269,7 @@ export function attachStylerCallback(themeService: IThemeService, colors: { [nam } export interface IBreadcrumbsWidgetStyleOverrides extends IColorMapping { - breadcrumbsBackground?: ColorIdentifier | ColorFunction; + breadcrumbsBackground?: ColorIdentifier | ColorTransform; breadcrumbsForeground?: ColorIdentifier; breadcrumbsHoverForeground?: ColorIdentifier; breadcrumbsFocusForeground?: ColorIdentifier; @@ -324,7 +314,7 @@ export function attachMenuStyler(widget: IThemable, themeService: IThemeService, return attachStyler(themeService, { ...defaultMenuStyles, ...style }, widget); } -export interface IDialogStyleOverrides extends IButtonStyleOverrides, ILinkStyleOverrides { +export interface IDialogStyleOverrides extends IButtonStyleOverrides { dialogForeground?: ColorIdentifier; dialogBackground?: ColorIdentifier; dialogShadow?: ColorIdentifier; diff --git a/lib/vscode/src/vs/platform/theme/common/themeService.ts b/lib/vscode/src/vs/platform/theme/common/themeService.ts index 4032bab91fc0..9867c464ae36 100644 --- a/lib/vscode/src/vs/platform/theme/common/themeService.ts +++ b/lib/vscode/src/vs/platform/theme/common/themeService.ts @@ -67,6 +67,10 @@ export namespace ThemeIcon { return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id; } + export function asThemeIcon(codicon: Codicon): ThemeIcon { + return { id: codicon.id }; + } + export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray; export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName; export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector; diff --git a/lib/vscode/src/vs/platform/theme/electron-main/themeMainService.ts b/lib/vscode/src/vs/platform/theme/electron-main/themeMainService.ts index 1d07cf7bfc6b..189811d6fdf6 100644 --- a/lib/vscode/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/lib/vscode/src/vs/platform/theme/electron-main/themeMainService.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BrowserWindow, nativeTheme } from 'electron'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { ipcMain, nativeTheme } from 'electron'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IPartsSplash } from 'vs/platform/windows/common/windows'; const DEFAULT_BG_LIGHT = '#FFFFFF'; const DEFAULT_BG_DARK = '#1E1E1E'; @@ -14,45 +15,38 @@ const DEFAULT_BG_HC_BLACK = '#000000'; const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; +const THEME_WINDOW_SPLASH = 'windowSplash'; export const IThemeMainService = createDecorator('themeMainService'); export interface IThemeMainService { + readonly _serviceBrand: undefined; getBackgroundColor(): string; + + saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void; + getWindowSplash(): IPartsSplash | undefined; } export class ThemeMainService implements IThemeMainService { declare readonly _serviceBrand: undefined; - constructor(@IStateService private stateService: IStateService) { - ipcMain.on('vscode:changeColorTheme', (e: Event, windowId: number, broadcast: string) => { - // Theme changes - if (typeof broadcast === 'string') { - this.storeBackgroundColor(JSON.parse(broadcast)); - } - }); - } - - private storeBackgroundColor(data: { baseTheme: string, background: string }): void { - this.stateService.setItem(THEME_STORAGE_KEY, data.baseTheme); - this.stateService.setItem(THEME_BG_STORAGE_KEY, data.background); - } + constructor(@IStateMainService private stateMainService: IStateMainService) { } getBackgroundColor(): string { if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { return DEFAULT_BG_HC_BLACK; } - let background = this.stateService.getItem(THEME_BG_STORAGE_KEY, null); + let background = this.stateMainService.getItem(THEME_BG_STORAGE_KEY, null); if (!background) { let baseTheme: string; if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { baseTheme = 'hc-black'; } else { - baseTheme = this.stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; + baseTheme = this.stateMainService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; } background = (baseTheme === 'hc-black') ? DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? DEFAULT_BG_LIGHT : DEFAULT_BG_DARK); @@ -64,4 +58,32 @@ export class ThemeMainService implements IThemeMainService { return background; } + + saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void { + + // Update in storage + this.stateMainService.setItems([ + { key: THEME_STORAGE_KEY, data: splash.baseTheme }, + { key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background }, + { key: THEME_WINDOW_SPLASH, data: splash } + ]); + + // Update in opened windows + if (typeof windowId === 'number') { + this.updateBackgroundColor(windowId, splash); + } + } + + private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { + for (const window of BrowserWindow.getAllWindows()) { + if (window.id === windowId) { + window.setBackgroundColor(splash.colorInfo.background); + break; + } + } + } + + getWindowSplash(): IPartsSplash | undefined { + return this.stateMainService.getItem(THEME_WINDOW_SPLASH); + } } diff --git a/lib/vscode/src/vs/platform/undoRedo/common/undoRedoService.ts b/lib/vscode/src/vs/platform/undoRedo/common/undoRedoService.ts index 7f3d251a52af..5833ce80cc43 100644 --- a/lib/vscode/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/lib/vscode/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -709,7 +709,7 @@ export class UndoRedoService implements IUndoRedoService { private _onError(err: Error, element: StackElement): void { onUnexpectedError(err); - // An error occured while undoing or redoing => drop the undo/redo stack for all affected resources + // An error occurred while undoing or redoing => drop the undo/redo stack for all affected resources for (const strResource of element.strResources) { this.removeElements(strResource); } diff --git a/lib/vscode/src/vs/platform/update/electron-main/abstractUpdateService.ts b/lib/vscode/src/vs/platform/update/electron-main/abstractUpdateService.ts index 289b57cef162..20611427502e 100644 --- a/lib/vscode/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/lib/vscode/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -164,7 +164,7 @@ export abstract class AbstractUpdateService implements IUpdateService { this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - this.lifecycleMainService.quit(true /* from update */).then(vetod => { + this.lifecycleMainService.quit(true /* will restart */).then(vetod => { this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); if (vetod) { return; diff --git a/lib/vscode/src/vs/platform/update/electron-main/updateService.snap.ts b/lib/vscode/src/vs/platform/update/electron-main/updateService.snap.ts index d14018a83d71..ac0e652fd862 100644 --- a/lib/vscode/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/lib/vscode/src/vs/platform/update/electron-main/updateService.snap.ts @@ -106,7 +106,7 @@ abstract class AbstractUpdateService implements IUpdateService { this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - this.lifecycleMainService.quit(true /* from update */).then(vetod => { + this.lifecycleMainService.quit(true /* will restart */).then(vetod => { this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); if (vetod) { return; diff --git a/lib/vscode/src/vs/platform/update/electron-main/updateService.win32.ts b/lib/vscode/src/vs/platform/update/electron-main/updateService.win32.ts index eab8c3f18072..93424cad9659 100644 --- a/lib/vscode/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/lib/vscode/src/vs/platform/update/electron-main/updateService.win32.ts @@ -54,7 +54,7 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-update-${this.productService.target}-${process.arch}`); - return fs.promises.mkdir(result, { recursive: true }).then(() => result); + return pfs.Promises.mkdir(result, { recursive: true }).then(() => result); } constructor( @@ -145,7 +145,7 @@ export class Win32UpdateService extends AbstractUpdateService { return this.requestService.request({ url }, CancellationToken.None) .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) .then(hash ? () => checksum(downloadPath, update.hash) : () => undefined) - .then(() => fs.promises.rename(downloadPath, updatePackagePath)) + .then(() => pfs.Promises.rename(downloadPath, updatePackagePath)) .then(() => updatePackagePath); }); }).then(packagePath => { @@ -195,7 +195,7 @@ export class Win32UpdateService extends AbstractUpdateService { const promises = versions.filter(filter).map(async one => { try { - await fs.promises.unlink(path.join(cachePath, one)); + await pfs.Promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } diff --git a/lib/vscode/src/vs/platform/windows/common/windows.ts b/lib/vscode/src/vs/platform/windows/common/windows.ts index 9fb6e838d522..cc5ac005a9aa 100644 --- a/lib/vscode/src/vs/platform/windows/common/windows.ts +++ b/lib/vscode/src/vs/platform/windows/common/windows.ts @@ -233,6 +233,31 @@ export interface IOSConfiguration { readonly hostname: string; } +export interface IPartsSplash { + baseTheme: string; + colorInfo: { + background: string; + foreground: string | undefined; + editorBackground: string | undefined; + titleBarBackground: string | undefined; + activityBarBackground: string | undefined; + sideBarBackground: string | undefined; + statusBarBackground: string | undefined; + statusBarNoFolderBackground: string | undefined; + windowBorder: string | undefined; + } + layoutInfo: { + sideBarSide: string; + editorPartMinWidth: number; + titleBarHeight: number; + activityBarWidth: number; + sideBarWidth: number; + statusBarHeight: number; + windowBorder: boolean; + windowBorderRadius: string | undefined; + } | undefined +} + export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs, ISandboxConfiguration { mainPid: number; @@ -245,7 +270,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native tmpDir: string; userDataDir: string; - partsSplashPath: string; + partsSplash?: IPartsSplash; workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; diff --git a/lib/vscode/src/vs/platform/windows/electron-main/window.ts b/lib/vscode/src/vs/platform/windows/electron-main/window.ts index d4011761837d..c597738aa696 100644 --- a/lib/vscode/src/vs/platform/windows/electron-main/window.ts +++ b/lib/vscode/src/vs/platform/windows/electron-main/window.ts @@ -34,7 +34,6 @@ import { IFileService } from 'vs/platform/files/common/files'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; export interface IWindowCreationOptions { @@ -153,7 +152,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { @ITelemetryService private readonly telemetryService: ITelemetryService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @IProductService private readonly productService: IProductService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService ) { @@ -441,7 +439,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { }); // Block all SVG requests from unsupported origins - const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); // TODO: handle webview origin + const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); // TODO@mjbvz: handle webview origin // But allow them if the are made from inside an webview const isSafeFrame = (requestFrame: WebFrameMain | undefined): boolean => { @@ -453,13 +451,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { return false; }; + const isRequestFromSafeContext = (details: Electron.OnBeforeRequestListenerDetails | Electron.OnHeadersReceivedListenerDetails): boolean => { + return details.resourceType === 'xhr' || isSafeFrame(details.frame); + }; + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { const uri = URI.parse(details.url); if (uri.path.endsWith('.svg')) { const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme) || uri.path.includes(Schemas.vscodeRemoteResource); if (!isSafeResourceUrl) { - const isSafeContext = isSafeFrame(details.frame); - return callback({ cancel: !isSafeContext }); + return callback({ cancel: !isRequestFromSafeContext(details) }); } } @@ -485,8 +486,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // remote extension schemes have the following format // http://127.0.0.1:/vscode-remote-resource?path= if (!uri.path.includes(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) { - const isSafeContext = isSafeFrame(details.frame); - return callback({ cancel: !isSafeContext }); + return callback({ cancel: !isRequestFromSafeContext(details) }); } } @@ -510,22 +510,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._lastFocusTime = Date.now(); }); - if (isMacintosh) { - this._register(this.nativeHostMainService.onDidChangeDisplay(() => { - if (!this._win) { - return; // disposed - } - - // Simple fullscreen doesn't resize automatically when the resolution changes so as a workaround - // we need to detect when display metrics change or displays are added/removed and toggle the - // fullscreen manually. - if (!this.useNativeFullScreen() && this.isFullScreen) { - this.setFullScreen(false); - this.setFullScreen(true); - } - })); - } - // Window (Un)Maximize this._win.on('maximize', (e: Event) => { if (this.currentConfig) { @@ -574,7 +558,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { switch (type) { case WindowError.CRASHED: - this.logService.error(`CodeWindow: renderer process crashed (detail: ${typeof details === 'string' ? details : details?.reason})`); + this.logService.error(`CodeWindow: renderer process crashed (detail: ${typeof details === 'string' ? details : details?.reason}, code: ${typeof details === 'string' ? '' : details?.exitCode ?? ''})`); break; case WindowError.UNRESPONSIVE: this.logService.error('CodeWindow: detected unresponsive'); @@ -668,8 +652,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private destroyWindow(): void { - this._onDidDestroy.fire(); // 'close' event will not be fired on destroy(), so signal crash via explicit event - this._win.destroy(); // make sure to destroy the window as it has crashed + this._onDidDestroy.fire(); // 'close' event will not be fired on destroy(), so signal crash via explicit event + this._win.destroy(); // make sure to destroy the window as it has crashed } private onDidDeleteUntitledWorkspace(workspace: IWorkspaceIdentifier): void { @@ -806,6 +790,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Update window related properties configuration.fullscreen = this.isFullScreen; configuration.maximized = this._win.isMaximized(); + configuration.partsSplash = this.themeMainService.getWindowSplash(); // Update with latest perf marks mark('code/willOpenNewWindow'); diff --git a/lib/vscode/src/vs/platform/windows/electron-main/windowsFinder.ts b/lib/vscode/src/vs/platform/windows/electron-main/windowsFinder.ts index 30f7601832d5..aaa765af3787 100644 --- a/lib/vscode/src/vs/platform/windows/electron-main/windowsFinder.ts +++ b/lib/vscode/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -8,7 +8,7 @@ import { IWorkspaceIdentifier, IResolvedWorkspace, isWorkspaceIdentifier, isSing import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): ICodeWindow | undefined { +export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | undefined): ICodeWindow | undefined { // First check for windows with workspaces that have a parent folder of the provided path opened for (const window of windows) { diff --git a/lib/vscode/src/vs/platform/windows/electron-main/windowsMainService.ts b/lib/vscode/src/vs/platform/windows/electron-main/windowsMainService.ts index 4f66b0be81fa..991c9585ec6e 100644 --- a/lib/vscode/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/lib/vscode/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -14,7 +14,7 @@ import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { CodeWindow } from 'vs/platform/windows/electron-main/window'; import { app, BrowserWindow, MessageBoxOptions, nativeTheme, WebContents } from 'electron'; import { ILifecycleMainService, UnloadReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -139,13 +139,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private readonly _onDidChangeWindowsCount = this._register(new Emitter()); readonly onDidChangeWindowsCount = this._onDidChangeWindowsCount.event; - private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateService, this.lifecycleMainService, this.logService, this.configurationService)); + private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateMainService, this.lifecycleMainService, this.logService, this.configurationService)); constructor( private readonly machineId: string, private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @@ -409,7 +409,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let windowToUseForFiles: ICodeWindow | undefined = undefined; if (fileToCheck?.fileUri && !openFilesInNewWindow) { if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) { - windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : null); + windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : undefined); } if (!windowToUseForFiles) { @@ -1169,7 +1169,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic appRoot: this.environmentMainService.appRoot, execPath: process.execPath, nodeCachedDataDir: this.environmentMainService.nodeCachedDataDir, - partsSplashPath: join(this.environmentMainService.userDataPath, 'rapid_render.json'), // If we know the backup folder upfront (for empty windows to restore), we can set it // directly here which helps for restoring UI state associated with that window. // For all other cases we first call into registerEmptyWindowBackupSync() to set it before diff --git a/lib/vscode/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/lib/vscode/src/vs/platform/windows/electron-main/windowsStateHandler.ts index 75c96494b804..1bdadb126107 100644 --- a/lib/vscode/src/vs/platform/windows/electron-main/windowsStateHandler.ts +++ b/lib/vscode/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { INativeWindowConfiguration, IWindowSettings } from 'vs/platform/windows/common/windows'; import { defaultWindowState, ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -53,7 +53,7 @@ export class WindowsStateHandler extends Disposable { private static readonly windowsStateStorageKey = 'windowsState'; get state() { return this._state; } - private readonly _state = restoreWindowsState(this.stateService.getItem(WindowsStateHandler.windowsStateStorageKey)); + private readonly _state = restoreWindowsState(this.stateMainService.getItem(WindowsStateHandler.windowsStateStorageKey)); private lastClosedState: IWindowState | undefined = undefined; @@ -61,7 +61,7 @@ export class WindowsStateHandler extends Disposable { constructor( @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -177,7 +177,7 @@ export class WindowsStateHandler extends Disposable { // Persist const state = getWindowsStateStoreData(currentWindowsState); - this.stateService.setItem(WindowsStateHandler.windowsStateStorageKey, state); + this.stateMainService.setItem(WindowsStateHandler.windowsStateStorageKey, state); if (this.shuttingDown) { this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state); diff --git a/lib/vscode/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/lib/vscode/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 8a1b2999b6ef..f26f2765f750 100644 --- a/lib/vscode/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/lib/vscode/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -28,7 +28,7 @@ suite('WindowsFinder', () => { }; const testWorkspaceFolders = toWorkspaceFolders([{ path: join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); - const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }; + const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : undefined; }; function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?: URI, openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { return new class implements ICodeWindow { diff --git a/lib/vscode/src/vs/platform/workspace/common/workspaceTrust.ts b/lib/vscode/src/vs/platform/workspace/common/workspaceTrust.ts index 1a303f1482dc..275035ffa398 100644 --- a/lib/vscode/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/lib/vscode/src/vs/platform/workspace/common/workspaceTrust.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -29,27 +30,43 @@ export interface WorkspaceTrustRequestButton { export interface WorkspaceTrustRequestOptions { readonly buttons?: WorkspaceTrustRequestButton[]; readonly message?: string; - readonly modal: boolean; } -export type WorkspaceTrustChangeEvent = Event; export const IWorkspaceTrustManagementService = createDecorator('workspaceTrustManagementService'); export interface IWorkspaceTrustManagementService { readonly _serviceBrand: undefined; - onDidChangeTrust: WorkspaceTrustChangeEvent; + onDidChangeTrust: Event; onDidChangeTrustedFolders: Event; + readonly workspaceTrustEnabled: boolean; + readonly workspaceResolved: Promise; + readonly workspaceTrustInitialized: Promise; + acceptsOutOfWorkspaceFiles: boolean; + isWorkpaceTrusted(): boolean; + isWorkspaceTrustForced(): boolean; + canSetParentFolderTrust(): boolean; - setParentFolderTrust(trusted: boolean): void; + setParentFolderTrust(trusted: boolean): Promise; + canSetWorkspaceTrust(): boolean; - setWorkspaceTrust(trusted: boolean): void; - getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo; - setFoldersTrust(folders: URI[], trusted: boolean): void; - getTrustedFolders(): URI[]; - setTrustedFolders(folders: URI[]): void; + setWorkspaceTrust(trusted: boolean): Promise; + + getUriTrustInfo(uri: URI): Promise; + setUrisTrust(uri: URI[], trusted: boolean): Promise; + + getTrustedUris(): URI[]; + setTrustedUris(uris: URI[]): Promise; + + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable; +} + +export const enum WorkspaceTrustUriResponse { + Open = 1, + OpenInNewWindow = 2, + Cancel = 3 } export const IWorkspaceTrustRequestService = createDecorator('workspaceTrustRequestService'); @@ -57,14 +74,18 @@ export const IWorkspaceTrustRequestService = createDecorator; - readonly onDidCompleteWorkspaceTrustRequest: Event; + readonly onDidInitiateWorkspaceTrustRequest: Event; + requestOpenUris(uris: URI[]): Promise; cancelRequest(): void; - completeRequest(trusted?: boolean): void; + completeRequest(trusted?: boolean): Promise; requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; } +export interface IWorkspaceTrustTransitionParticipant { + participate(trusted: boolean): Promise; +} + export interface IWorkspaceTrustUriInfo { uri: URI, trusted: boolean diff --git a/lib/vscode/src/vs/platform/workspaces/common/workspaces.ts b/lib/vscode/src/vs/platform/workspaces/common/workspaces.ts index 811a44f2c4ac..7a11eafa2eb4 100644 --- a/lib/vscode/src/vs/platform/workspaces/common/workspaces.ts +++ b/lib/vscode/src/vs/platform/workspaces/common/workspaces.ts @@ -39,7 +39,7 @@ export interface IWorkspacesService { readonly _serviceBrand: undefined; // Workspaces Management - enterWorkspace(path: URI): Promise; + enterWorkspace(path: URI): Promise; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; getWorkspaceIdentifier(workspacePath: URI): Promise; @@ -320,7 +320,7 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], const relativeTo = extUri.dirname(workspaceConfigFile); for (let configuredFolder of configuredFolders) { - let uri: URI | null = null; + let uri: URI | undefined = undefined; if (isRawFileWorkspaceFolder(configuredFolder)) { if (configuredFolder.path) { uri = extUri.resolvePath(relativeTo, configuredFolder.path); @@ -328,16 +328,16 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], } else if (isRawUriWorkspaceFolder(configuredFolder)) { try { uri = URI.parse(configuredFolder.uri); - // this makes sure all workspace folder are absolute if (uri.path[0] !== '/') { - uri = uri.with({ path: '/' + uri.path }); + uri = uri.with({ path: '/' + uri.path }); // this makes sure all workspace folder are absolute } } catch (e) { - console.warn(e); - // ignore + console.warn(e); // ignore } } + if (uri) { + // remove duplicates let comparisonKey = extUri.getComparisonKey(uri); if (!seen.has(comparisonKey)) { @@ -369,11 +369,9 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); let absolute; if (isFromUntitledWorkspace) { - // if it was an untitled workspace, try to make paths relative - absolute = false; + absolute = false; // if it was an untitled workspace, try to make paths relative } else { - // for existing workspaces, preserve whether a path was absolute or relative - absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); + absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); // for existing workspaces, preserve whether a path was absolute or relative } rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri)); } diff --git a/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index dd246999d7b8..62c029d242bb 100644 --- a/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { coalesce } from 'vs/base/common/arrays'; -import { IStateService } from 'vs/platform/state/node/state'; +import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { app, JumpListCategory, JumpListItem } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { normalizeDriveLetter, splitName } from 'vs/base/common/labels'; @@ -62,7 +62,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( - @IStateService private readonly stateService: IStateService, + @IStateMainService private readonly stateMainService: IStateMainService, @ILogService private readonly logService: ILogService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService @@ -291,7 +291,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } private getRecentlyOpenedFromStorage(): IRecentlyOpened { - const storedRecents = this.stateService.getItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey); + const storedRecents = this.stateMainService.getItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey); return restoreRecentlyOpened(storedRecents, this.logService); } @@ -299,7 +299,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa private saveRecentlyOpened(recent: IRecentlyOpened): void { const serialized = toStoreData(recent); - this.stateService.setItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey, serialized); + this.stateMainService.setItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey, serialized); } updateWindowsJumpList(): void { diff --git a/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 67a5e54db3fb..88d90f9e74eb 100644 --- a/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -25,13 +25,13 @@ export class WorkspacesMainService implements AddFirstParameterToFunctions { + async enterWorkspace(windowId: number, path: URI): Promise { const window = this.windowsMainService.getWindowById(windowId); if (window) { return this.workspacesManagementMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); } - return null; + return undefined; } createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { diff --git a/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 4a510b7ffb80..47a5be15c909 100644 --- a/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/lib/vscode/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -6,8 +6,8 @@ import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { join, dirname } from 'vs/base/common/path'; -import { writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; -import { promises, readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; +import { writeFile, rimrafSync, readdirSync, writeFileSync, Promises } from 'vs/base/node/pfs'; +import { readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; @@ -41,7 +41,7 @@ export interface IWorkspacesManagementMainService { readonly onDidDeleteUntitledWorkspace: Event; readonly onDidEnterWorkspace: Event; - enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; + enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; @@ -52,7 +52,7 @@ export interface IWorkspacesManagementMainService { getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; + resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | undefined; getWorkspaceIdentifier(workspacePath: URI): Promise; } @@ -83,20 +83,20 @@ export class WorkspacesManagementMainService extends Disposable implements IWork super(); } - resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { + resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | undefined { if (!this.isWorkspacePath(uri)) { - return null; // does not look like a valid workspace config file + return undefined; // does not look like a valid workspace config file } if (uri.scheme !== Schemas.file) { - return null; + return undefined; } let contents: string; try { contents = readFileSync(uri.fsPath, 'utf8'); } catch (error) { - return null; // invalid workspace + return undefined; // invalid workspace } return this.doResolveWorkspace(uri, contents); @@ -106,7 +106,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return isUntitledWorkspace(uri, this.environmentMainService) || hasWorkspaceFileExtension(uri); } - private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | undefined { try { const workspace = this.doParseStoredWorkspace(path, contents); const workspaceIdentifier = getWorkspaceIdentifier(path); @@ -120,7 +120,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork this.logService.warn(error.toString()); } - return null; + return undefined; } private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { @@ -142,7 +142,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - await promises.mkdir(dirname(configPath), { recursive: true }); + await Promises.mkdir(dirname(configPath), { recursive: true }); await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); return workspace; @@ -238,19 +238,19 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return untitledWorkspaces; } - async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { + async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { if (!window || !window.win || !window.isReady) { - return null; // return early if the window is not ready or disposed + return undefined; // return early if the window is not ready or disposed } const isValid = await this.isValidTargetWorkspacePath(window, windows, path); if (!isValid) { - return null; // return early if the workspace is not valid + return undefined; // return early if the workspace is not valid } const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); if (!result) { - return null; + return undefined; } // Emit as event @@ -287,9 +287,9 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return true; // OK } - private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { + private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | undefined { if (!window.config) { - return null; + return undefined; } window.focus(); diff --git a/lib/vscode/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/lib/vscode/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 1b524310e9c1..3769d7324bb7 100644 --- a/lib/vscode/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/lib/vscode/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -109,7 +109,7 @@ flakySuite('WorkspacesManagementMainService', () => { service = new WorkspacesManagementMainService(environmentMainService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService(), productService); - return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); + return pfs.Promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { diff --git a/lib/vscode/src/vs/server/node/channel.ts b/lib/vscode/src/vs/server/channel.ts similarity index 93% rename from lib/vscode/src/vs/server/node/channel.ts rename to lib/vscode/src/vs/server/channel.ts index 06e6de6b9a9e..0af9303cec9e 100644 --- a/lib/vscode/src/vs/server/node/channel.ts +++ b/lib/vscode/src/vs/server/channel.ts @@ -20,18 +20,18 @@ import { ILogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; import { IRemoteAgentEnvironment, RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IPtyService, IShellLaunchConfig, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; -import { getTranslations } from 'vs/server/node/nls'; -import { getUriTransformer } from 'vs/server/node/util'; +import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; +import { getTranslations } from 'vs/server/nls'; +import { getUriTransformer } from 'vs/server/util'; import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol'; import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import * as terminal from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints'; +import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; /** * Extend the file provider to allow unwatching. @@ -241,7 +241,7 @@ export class ExtensionEnvironmentChannel implements IServerChannel { ); case 'getDiagnosticInfo': return this.getDiagnosticInfo(); case 'disableTelemetry': return this.disableTelemetry(); - case 'logTelemetry': return this.logTelemetry(args[0], args[1]); + case 'logTelemetry': return this.logTelemetry(args.eventName, args.data); case 'flushTelemetry': return this.flushTelemetry(); } throw new Error(`Invalid call '${command}'`); @@ -272,6 +272,7 @@ export class ExtensionEnvironmentChannel implements IServerChannel { return Promise.all(paths.map((path) => { return ExtensionScanner.scanExtensions(new ExtensionScannerInput( product.version, + product.date, product.commit, language, !!process.env.VSCODE_DEV, @@ -386,7 +387,7 @@ class VariableResolverService extends AbstractVariableResolverService { export class TerminalProviderChannel implements IServerChannel, IDisposable { public constructor ( private readonly logService: ILogService, - private readonly ptyService: IPtyService, + private readonly ptyService: PtyHostService, ) {} public listen(_: RemoteAgentConnectionContext, event: string, args: any): Event { @@ -397,7 +398,7 @@ export class TerminalProviderChannel implements IServerChannel { - if (this.ptyService.restartPtyHost) { - return this.ptyService.restartPtyHost(); - } - } - - // References: - ../../workbench/api/node/extHostTerminalService.ts // - ../../workbench/contrib/terminal/browser/terminalProcessManager.ts private async createProcess(remoteAuthority: string, args: terminal.ICreateTerminalProcessArguments): Promise { @@ -499,8 +498,11 @@ export class TerminalProviderChannel implements IServerChannel => { - const env = await getMainProcessParentEnv(process.env); + // ptyHostService calls getEnvironment in the ptyHost process it creates, + // which uses that process's environment. The process spawned doesn't have + // VSCODE_IPC_HOOK_CLI in its env, so we add it here. + const getEnvironment = async (): Promise => { + const env = await this.ptyService.getEnvironment(); env.VSCODE_IPC_HOOK_CLI = process.env['VSCODE_IPC_HOOK_CLI']!; return env; }; @@ -511,9 +513,7 @@ export class TerminalProviderChannel implements IServerChannel { - throw new Error(`Invalid listen ${event}`); - } - - call(_: unknown, command: string, args?: any): Promise { - switch (command) { - case 'publicLog': return this.service.publicLog(args[0], args[1], args[2]); - case 'publicLog2': return this.service.publicLog2(args[0], args[1], args[2]); - case 'publicLogError': return this.service.publicLogError(args[0], args[1]); - case 'publicLogError2': return this.service.publicLogError2(args[0], args[1]); - case 'setEnabled': return Promise.resolve(this.service.setEnabled(args[0])); - case 'getTelemetryInfo': return this.service.getTelemetryInfo(); - case 'setExperimentProperty': return Promise.resolve(this.service.setExperimentProperty(args[0], args[1])); - } - throw new Error(`Invalid call ${command}`); - } -} - -export class TelemetryChannelClient implements ITelemetryService { - _serviceBrand: any; - - // These don't matter; telemetry is sent to the Node side which decides - // whether to send the telemetry event. - public isOptedIn = true; - public sendErrorTelemetry = true; - - constructor(private readonly channel: IChannel) {} - - public publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { - return this.channel.call('publicLog', [eventName, data, anonymizeFilePaths]); - } - - public publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { - return this.channel.call('publicLog2', [eventName, data, anonymizeFilePaths]); - } - - public publicLogError(errorEventName: string, data?: ITelemetryData): Promise { - return this.channel.call('publicLogError', [errorEventName, data]); - } - - public publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise { - return this.channel.call('publicLogError2', [eventName, data]); - } - - public setEnabled(value: boolean): void { - this.channel.call('setEnable', [value]); - } - - public getTelemetryInfo(): Promise { - return this.channel.call('getTelemetryInfo'); - } - - public setExperimentProperty(name: string, value: string): void { - this.channel.call('setExperimentProperty', [name, value]); - } -} diff --git a/lib/vscode/src/vs/server/node/connection.ts b/lib/vscode/src/vs/server/connection.ts similarity index 91% rename from lib/vscode/src/vs/server/node/connection.ts rename to lib/vscode/src/vs/server/connection.ts index ef5cecdaa88b..2bf5b522a180 100644 --- a/lib/vscode/src/vs/server/node/connection.ts +++ b/lib/vscode/src/vs/server/connection.ts @@ -5,8 +5,8 @@ import { Emitter } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; -import { getNlsConfiguration } from 'vs/server/node/nls'; -import { Protocol } from 'vs/server/node/protocol'; +import { getNlsConfiguration } from 'vs/server/nls'; +import { Protocol } from 'vs/server/protocol'; import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export abstract class Connection { @@ -167,10 +167,17 @@ export class ExtensionHostConnection extends Connection { this.logger.debug('Spawning extension host...'); const proc = cp.fork( FileAccess.asFileUri('bootstrap-fork', require).fsPath, - // While not technically necessary, makes it easier to tell which process - // bootstrap-fork is executing. Can also do pkill -f extensionHost - // Other spawns in the VS Code codebase behave similarly. - [ '--type=extensionHost' ], + [ + // While not technically necessary, adding --type makes it easier to + // tell which process bootstrap-fork is executing. Can also do `pkill -f + // extensionHost`. Other spawns in the VS Code codebase behave + // similarly. + '--type=extensionHost', + // We can't use the symlinked uriTransformer in this same directory + // because it gets compiled into AMD syntax and this path is imported + // using Node's native require. + `--uriTransformerPath=${FileAccess.asFileUri('vs/../../../../out/node/uriTransformer.js', require).fsPath}` + ], { env: { ...process.env, diff --git a/lib/vscode/src/vs/server/entry.ts b/lib/vscode/src/vs/server/entry.ts index 2f458f4035b7..124c997ea40f 100644 --- a/lib/vscode/src/vs/server/entry.ts +++ b/lib/vscode/src/vs/server/entry.ts @@ -1,10 +1,10 @@ import { field } from '@coder/logger'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import * as proxyAgent from 'vs/base/node/proxy_agent'; -import { CodeServerMessage, VscodeMessage } from 'vs/ipc'; -import { logger } from 'vs/server/node/logger'; -import { enableCustomMarketplace } from 'vs/server/node/marketplace'; -import { Vscode } from 'vs/server/node/server'; +import { CodeServerMessage, VscodeMessage } from 'vs/base/common/ipc'; +import { logger } from 'vs/server/logger'; +import { enableCustomMarketplace } from 'vs/server/marketplace'; +import { Vscode } from 'vs/server/server'; setUnexpectedErrorHandler((error) => { logger.warn('Uncaught error', field('error', error instanceof Error ? error.message : error)); diff --git a/lib/vscode/src/vs/server/node/insights.ts b/lib/vscode/src/vs/server/insights.ts similarity index 100% rename from lib/vscode/src/vs/server/node/insights.ts rename to lib/vscode/src/vs/server/insights.ts diff --git a/lib/vscode/src/vs/server/node/ipc.ts b/lib/vscode/src/vs/server/ipc.ts similarity index 100% rename from lib/vscode/src/vs/server/node/ipc.ts rename to lib/vscode/src/vs/server/ipc.ts diff --git a/lib/vscode/src/vs/server/node/logger.ts b/lib/vscode/src/vs/server/logger.ts similarity index 100% rename from lib/vscode/src/vs/server/node/logger.ts rename to lib/vscode/src/vs/server/logger.ts diff --git a/lib/vscode/src/vs/server/logsDataCleaner.ts b/lib/vscode/src/vs/server/logsDataCleaner.ts new file mode 120000 index 000000000000..b36b3f617a85 --- /dev/null +++ b/lib/vscode/src/vs/server/logsDataCleaner.ts @@ -0,0 +1 @@ +../code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts \ No newline at end of file diff --git a/lib/vscode/src/vs/server/node/marketplace.ts b/lib/vscode/src/vs/server/marketplace.ts similarity index 100% rename from lib/vscode/src/vs/server/node/marketplace.ts rename to lib/vscode/src/vs/server/marketplace.ts diff --git a/lib/vscode/src/vs/server/node/nls.ts b/lib/vscode/src/vs/server/nls.ts similarity index 100% rename from lib/vscode/src/vs/server/node/nls.ts rename to lib/vscode/src/vs/server/nls.ts diff --git a/lib/vscode/src/vs/server/node/util.ts b/lib/vscode/src/vs/server/node/util.ts deleted file mode 100644 index d76f655e3664..000000000000 --- a/lib/vscode/src/vs/server/node/util.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { URITransformer } from 'vs/base/common/uriIpc'; - -export const getUriTransformer = (remoteAuthority: string): URITransformer => { - return new URITransformer(remoteAuthority); -}; - -/** - * Encode a path for opening via the folder or workspace query parameter. This - * preserves slashes so it can be edited by hand more easily. - */ -export const encodePath = (path: string): string => { - return path.split('/').map((p) => encodeURIComponent(p)).join('/'); -}; diff --git a/lib/vscode/src/vs/server/node/protocol.ts b/lib/vscode/src/vs/server/protocol.ts similarity index 100% rename from lib/vscode/src/vs/server/node/protocol.ts rename to lib/vscode/src/vs/server/protocol.ts diff --git a/lib/vscode/src/vs/server/node/server.ts b/lib/vscode/src/vs/server/server.ts similarity index 94% rename from lib/vscode/src/vs/server/node/server.ts rename to lib/vscode/src/vs/server/server.ts index ec0e2edd19a6..d96142f0063a 100644 --- a/lib/vscode/src/vs/server/node/server.ts +++ b/lib/vscode/src/vs/server/server.ts @@ -7,9 +7,9 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { getMachineId } from 'vs/base/node/id'; import { ClientConnectionEvent, IPCServer, IServerChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { main } from 'vs/code/node/cliProcessMain'; -import { Query, VscodeOptions, WorkbenchOptions } from 'vs/ipc'; +import { LogsDataCleaner } from 'vs/server/logsDataCleaner'; +import { main } from 'vs/server/cliProcessMain'; +import { Query, VscodeOptions, WorkbenchOptions } from 'vs/base/common/ipc'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; @@ -39,21 +39,20 @@ import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAg import { IRequestService } from 'vs/platform/request/common/request'; import { RequestChannel } from 'vs/platform/request/common/requestIpc'; import { RequestService } from 'vs/platform/request/node/requestService'; -import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; +import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { TelemetryChannel } from 'vs/server/common/telemetry'; -import { ExtensionEnvironmentChannel, FileProviderChannel, TerminalProviderChannel } from 'vs/server/node/channel'; -import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/node/connection'; -import { TelemetryClient } from 'vs/server/node/insights'; -import { logger } from 'vs/server/node/logger'; -import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/node/nls'; -import { Protocol } from 'vs/server/node/protocol'; -import { getUriTransformer } from 'vs/server/node/util'; +import { ExtensionEnvironmentChannel, FileProviderChannel, TerminalProviderChannel } from 'vs/server/channel'; +import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/connection'; +import { TelemetryClient } from 'vs/server/insights'; +import { logger } from 'vs/server/logger'; +import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/nls'; +import { Protocol } from 'vs/server/protocol'; +import { getUriTransformer } from 'vs/server/util'; import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -269,7 +268,8 @@ export class Vscode { instantiationService.createInstance(LogsDataCleaner); let telemetryService: ITelemetryService; - if (!environmentService.disableTelemetry) { + + if (!environmentService.isExtensionDevelopment && !environmentService.disableTelemetry && !!productService.enableTelemetry) { telemetryService = new TelemetryService({ appender: combinedAppender( new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any), @@ -300,11 +300,10 @@ export class Vscode { environmentService, logService, telemetryService, '', )); this.ipc.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); - this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService)); this.ipc.registerChannel('localizations', >ProxyChannel.fromService(accessor.get(ILocalizationsService))); this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService)); - const ptyHostService = new PtyHostService(logService, telemetryService); + const ptyHostService = new PtyHostService({GraceTime: 60000, ShortGraceTime: 6000}, configurationService, logService, telemetryService); this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel(logService, ptyHostService)); resolve(new ErrorTelemetry(telemetryService)); diff --git a/lib/vscode/src/vs/server/uriTransformer.ts b/lib/vscode/src/vs/server/uriTransformer.ts new file mode 120000 index 000000000000..e9e274263292 --- /dev/null +++ b/lib/vscode/src/vs/server/uriTransformer.ts @@ -0,0 +1 @@ +../../../../../src/node/uriTransformer.ts \ No newline at end of file diff --git a/lib/vscode/src/vs/server/util.ts b/lib/vscode/src/vs/server/util.ts new file mode 100644 index 000000000000..5f1fc9ba3ae5 --- /dev/null +++ b/lib/vscode/src/vs/server/util.ts @@ -0,0 +1,6 @@ +import { URITransformer } from 'vs/base/common/uriIpc'; +import rawURITransformerFactory = require('vs/server/uriTransformer'); + +export const getUriTransformer = (remoteAuthority: string): URITransformer => { + return new URITransformer(rawURITransformerFactory(remoteAuthority)); +}; diff --git a/lib/vscode/src/vs/vscode.d.ts b/lib/vscode/src/vs/vscode.d.ts index ad38c32f8bbd..77b7c5465dc4 100644 --- a/lib/vscode/src/vs/vscode.d.ts +++ b/lib/vscode/src/vs/vscode.d.ts @@ -24,7 +24,7 @@ declare module 'vscode' { /** * The identifier of the actual command handler. - * @see [commands.registerCommand](#commands.registerCommand). + * @see {@link commands.registerCommand} */ command: string; @@ -43,7 +43,7 @@ declare module 'vscode' { /** * Represents a line of text, such as a line of source code. * - * TextLine objects are __immutable__. When a [document](#TextDocument) changes, + * TextLine objects are __immutable__. When a {@link TextDocument document} changes, * previously retrieved lines will not represent the latest state. */ export interface TextLine { @@ -76,14 +76,14 @@ declare module 'vscode' { /** * Whether this line is whitespace only, shorthand - * for [TextLine.firstNonWhitespaceCharacterIndex](#TextLine.firstNonWhitespaceCharacterIndex) === [TextLine.text.length](#TextLine.text). + * for {@link TextLine.firstNonWhitespaceCharacterIndex} === {@link TextLine.text TextLine.text.length}. */ readonly isEmptyOrWhitespace: boolean; } /** * Represents a text document, such as a source file. Text documents have - * [lines](#TextLine) and knowledge about an underlying resource like a file. + * {@link TextLine lines} and knowledge about an underlying resource like a file. */ export interface TextDocument { @@ -93,21 +93,21 @@ declare module 'vscode' { * *Note* that most documents use the `file`-scheme, which means they are files on disk. However, **not** all documents are * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. * - * @see [FileSystemProvider](#FileSystemProvider) - * @see [TextDocumentContentProvider](#TextDocumentContentProvider) + * @see {@link FileSystemProvider} + * @see {@link TextDocumentContentProvider} */ readonly uri: Uri; /** * The file system path of the associated resource. Shorthand - * notation for [TextDocument.uri.fsPath](#TextDocument.uri). Independent of the uri scheme. + * notation for {@link TextDocument.uri TextDocument.uri.fsPath}. Independent of the uri scheme. */ readonly fileName: string; /** * Is this document representing an untitled file which has never been saved yet. *Note* that - * this does not mean the document will be saved to disk, use [`uri.scheme`](#Uri.scheme) - * to figure out where a document will be [saved](#FileSystemProvider), e.g. `file`, `ftp` etc. + * this does not mean the document will be saved to disk, use {@link Uri.scheme `uri.scheme`} + * to figure out where a document will be {@link FileSystemProvider saved}, e.g. `file`, `ftp` etc. */ readonly isUntitled: boolean; @@ -143,7 +143,7 @@ declare module 'vscode' { save(): Thenable; /** - * The [end of line](#EndOfLine) sequence that is predominately + * The {@link EndOfLine end of line} sequence that is predominately * used in this document. */ readonly eol: EndOfLine; @@ -159,7 +159,7 @@ declare module 'vscode' { * document are not reflected. * * @param line A line number in [0, lineCount). - * @return A [line](#TextLine). + * @return A {@link TextLine line}. */ lineAt(line: number): TextLine; @@ -168,18 +168,19 @@ declare module 'vscode' { * that the returned object is *not* live and changes to the * document are not reflected. * - * The position will be [adjusted](#TextDocument.validatePosition). + * The position will be {@link TextDocument.validatePosition adjusted}. + * + * @see {@link TextDocument.lineAt} * - * @see [TextDocument.lineAt](#TextDocument.lineAt) * @param position A position. - * @return A [line](#TextLine). + * @return A {@link TextLine line}. */ lineAt(position: Position): TextLine; /** * Converts the position to a zero-based offset. * - * The position will be [adjusted](#TextDocument.validatePosition). + * The position will be {@link TextDocument.validatePosition adjusted}. * * @param position A position. * @return A valid zero-based offset. @@ -190,13 +191,13 @@ declare module 'vscode' { * Converts a zero-based offset to a position. * * @param offset A zero-based offset. - * @return A valid [position](#Position). + * @return A valid {@link Position}. */ positionAt(offset: number): Position; /** * Get the text of this document. A substring can be retrieved by providing - * a range. The range will be [adjusted](#TextDocument.validateRange). + * a range. The range will be {@link TextDocument.validateRange adjusted}. * * @param range Include only the text included by the range. * @return The text inside the provided range or the entire text. @@ -206,16 +207,16 @@ declare module 'vscode' { /** * Get a word-range at the given position. By default words are defined by * common separators, like space, -, _, etc. In addition, per language custom - * [word definitions](#LanguageConfiguration.wordPattern) can be defined. It + * [word definitions} can be defined. It * is also possible to provide a custom regular expression. * * * *Note 1:* A custom regular expression must not match the empty string and * if it does, it will be ignored. * * *Note 2:* A custom regular expression will fail to match multiline strings * and in the name of speed regular expressions should not match words with - * spaces. Use [`TextLine.text`](#TextLine.text) for more complex, non-wordy, scenarios. + * spaces. Use {@link TextLine.text `TextLine.text`} for more complex, non-wordy, scenarios. * - * The position will be [adjusted](#TextDocument.validatePosition). + * The position will be {@link TextDocument.validatePosition adjusted}. * * @param position A position. * @param regex Optional regular expression that describes what a word is. @@ -244,8 +245,8 @@ declare module 'vscode' { * Represents a line and character position, such as * the position of the cursor. * - * Position objects are __immutable__. Use the [with](#Position.with) or - * [translate](#Position.translate) methods to derive new positions + * Position objects are __immutable__. Use the {@link Position.with with} or + * {@link Position.translate translate} methods to derive new positions * from an existing position. */ export class Position { @@ -343,8 +344,8 @@ declare module 'vscode' { /** * Create a new position derived from this position. * - * @param line Value that should be used as line value, default is the [existing value](#Position.line) - * @param character Value that should be used as character value, default is the [existing value](#Position.character) + * @param line Value that should be used as line value, default is the {@link Position.line existing value} + * @param character Value that should be used as character value, default is the {@link Position.character existing value} * @return A position where line and character are replaced by the given values. */ with(line?: number, character?: number): Position; @@ -361,21 +362,21 @@ declare module 'vscode' { /** * A range represents an ordered pair of two positions. - * It is guaranteed that [start](#Range.start).isBeforeOrEqual([end](#Range.end)) + * It is guaranteed that {@link Range.start start}.isBeforeOrEqual({@link Range.end end}) * - * Range objects are __immutable__. Use the [with](#Range.with), - * [intersection](#Range.intersection), or [union](#Range.union) methods + * Range objects are __immutable__. Use the {@link Range.with with}, + * {@link Range.intersection intersection}, or {@link Range.union union} methods * to derive new ranges from an existing range. */ export class Range { /** - * The start position. It is before or equal to [end](#Range.end). + * The start position. It is before or equal to {@link Range.end end}. */ readonly start: Position; /** - * The end position. It is after or equal to [start](#Range.start). + * The end position. It is after or equal to {@link Range.start start}. */ readonly end: Position; @@ -422,7 +423,7 @@ declare module 'vscode' { * Check if `other` equals this range. * * @param other A range. - * @return `true` when start and end are [equal](#Position.isEqual) to + * @return `true` when start and end are {@link Position.isEqual equal} to * start and end of this range. */ isEqual(other: Range): boolean; @@ -448,8 +449,8 @@ declare module 'vscode' { /** * Derived a new range from this range. * - * @param start A position that should be used as start. The default value is the [current start](#Range.start). - * @param end A position that should be used as end. The default value is the [current end](#Range.end). + * @param start A position that should be used as start. The default value is the {@link Range.start current start}. + * @param end A position that should be used as end. The default value is the {@link Range.end current end}. * @return A range derived from this range with the given start and end position. * If start and end are not different `this` range will be returned. */ @@ -472,13 +473,13 @@ declare module 'vscode' { /** * The position at which the selection starts. - * This position might be before or after [active](#Selection.active). + * This position might be before or after {@link Selection.active active}. */ anchor: Position; /** * The position of the cursor. - * This position might be before or after [anchor](#Selection.anchor). + * This position might be before or after {@link Selection.anchor anchor}. */ active: Position; @@ -501,13 +502,13 @@ declare module 'vscode' { constructor(anchorLine: number, anchorCharacter: number, activeLine: number, activeCharacter: number); /** - * A selection is reversed if [active](#Selection.active).isBefore([anchor](#Selection.anchor)). + * A selection is reversed if {@link Selection.active active}.isBefore({@link Selection.anchor anchor}). */ isReversed: boolean; } /** - * Represents sources that can cause [selection change events](#window.onDidChangeTextEditorSelection). + * Represents sources that can cause {@link window.onDidChangeTextEditorSelection selection change events}. */ export enum TextEditorSelectionChangeKind { /** @@ -525,62 +526,62 @@ declare module 'vscode' { } /** - * Represents an event describing the change in a [text editor's selections](#TextEditor.selections). + * Represents an event describing the change in a {@link TextEditor.selections text editor's selections}. */ export interface TextEditorSelectionChangeEvent { /** - * The [text editor](#TextEditor) for which the selections have changed. + * The {@link TextEditor text editor} for which the selections have changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's selections](#TextEditor.selections). + * The new value for the {@link TextEditor.selections text editor's selections}. */ - readonly selections: ReadonlyArray; + readonly selections: readonly Selection[]; /** - * The [change kind](#TextEditorSelectionChangeKind) which has triggered this + * The {@link TextEditorSelectionChangeKind change kind} which has triggered this * event. Can be `undefined`. */ readonly kind?: TextEditorSelectionChangeKind; } /** - * Represents an event describing the change in a [text editor's visible ranges](#TextEditor.visibleRanges). + * Represents an event describing the change in a {@link TextEditor.visibleRanges text editor's visible ranges}. */ export interface TextEditorVisibleRangesChangeEvent { /** - * The [text editor](#TextEditor) for which the visible ranges have changed. + * The {@link TextEditor text editor} for which the visible ranges have changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's visible ranges](#TextEditor.visibleRanges). + * The new value for the {@link TextEditor.visibleRanges text editor's visible ranges}. */ - readonly visibleRanges: ReadonlyArray; + readonly visibleRanges: readonly Range[]; } /** - * Represents an event describing the change in a [text editor's options](#TextEditor.options). + * Represents an event describing the change in a {@link TextEditor.options text editor's options}. */ export interface TextEditorOptionsChangeEvent { /** - * The [text editor](#TextEditor) for which the options have changed. + * The {@link TextEditor text editor} for which the options have changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's options](#TextEditor.options). + * The new value for the {@link TextEditor.options text editor's options}. */ readonly options: TextEditorOptions; } /** - * Represents an event describing the change of a [text editor's view column](#TextEditor.viewColumn). + * Represents an event describing the change of a {@link TextEditor.viewColumn text editor's view column}. */ export interface TextEditorViewColumnChangeEvent { /** - * The [text editor](#TextEditor) for which the view column has changed. + * The {@link TextEditor text editor} for which the view column has changed. */ readonly textEditor: TextEditor; /** - * The new value for the [text editor's view column](#TextEditor.viewColumn). + * The new value for the {@link TextEditor.viewColumn text editor's view column}. */ readonly viewColumn: ViewColumn; } @@ -634,14 +635,14 @@ declare module 'vscode' { } /** - * Represents a [text editor](#TextEditor)'s [options](#TextEditor.options). + * Represents a {@link TextEditor text editor}'s {@link TextEditor.options options}. */ export interface TextEditorOptions { /** * The size in spaces a tab takes. This is used for two purposes: * - the rendering width of a tab character; - * - the number of spaces to insert when [insertSpaces](#TextEditorOptions.insertSpaces) is true. + * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true. * * When getting a text editor's options, this property will always be a number (resolved). * When setting a text editor's options, this property is optional and it can be a number or `"auto"`. @@ -649,7 +650,7 @@ declare module 'vscode' { tabSize?: number | string; /** - * When pressing Tab insert [n](#TextEditorOptions.tabSize) spaces. + * When pressing Tab insert {@link TextEditorOptions.tabSize n} spaces. * When getting a text editor's options, this property will always be a boolean (resolved). * When setting a text editor's options, this property is optional and it can be a boolean or `"auto"`. */ @@ -672,10 +673,10 @@ declare module 'vscode' { /** * Represents a handle to a set of decorations - * sharing the same [styling options](#DecorationRenderOptions) in a [text editor](#TextEditor). + * sharing the same {@link DecorationRenderOptions styling options} in a {@link TextEditor text editor}. * * To get an instance of a `TextEditorDecorationType` use - * [createTextEditorDecorationType](#window.createTextEditorDecorationType). + * {@link window.createTextEditorDecorationType createTextEditorDecorationType}. */ export interface TextEditorDecorationType { @@ -691,7 +692,7 @@ declare module 'vscode' { } /** - * Represents different [reveal](#TextEditor.revealRange) strategies in a text editor. + * Represents different {@link TextEditor.revealRange reveal} strategies in a text editor. */ export enum TextEditorRevealType { /** @@ -714,7 +715,7 @@ declare module 'vscode' { } /** - * Represents different positions for rendering a decoration in an [overview ruler](#DecorationRenderOptions.overviewRulerLane). + * Represents different positions for rendering a decoration in an {@link DecorationRenderOptions.overviewRulerLane overview ruler}. * The overview ruler supports three lanes. */ export enum OverviewRulerLane { @@ -747,31 +748,31 @@ declare module 'vscode' { } /** - * Represents options to configure the behavior of showing a [document](#TextDocument) in an [editor](#TextEditor). + * Represents options to configure the behavior of showing a {@link TextDocument document} in an {@link TextEditor editor}. */ export interface TextDocumentShowOptions { /** - * An optional view column in which the [editor](#TextEditor) should be shown. - * The default is the [active](#ViewColumn.Active), other values are adjusted to - * be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is - * not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the + * An optional view column in which the {@link TextEditor editor} should be shown. + * The default is the {@link ViewColumn.Active active}, other values are adjusted to + * be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is + * not adjusted. Use {@link ViewColumn.Beside `ViewColumn.Beside`} to open the * editor to the side of the currently active one. */ viewColumn?: ViewColumn; /** - * An optional flag that when `true` will stop the [editor](#TextEditor) from taking focus. + * An optional flag that when `true` will stop the {@link TextEditor editor} from taking focus. */ preserveFocus?: boolean; /** - * An optional flag that controls if an [editor](#TextEditor)-tab will be replaced + * An optional flag that controls if an {@link TextEditor editor}-tab will be replaced * with the next editor or if it will be kept. */ preview?: boolean; /** - * An optional selection to apply for the document in the [editor](#TextEditor). + * An optional selection to apply for the document in the {@link TextEditor editor}. */ selection?: Range; } @@ -790,7 +791,7 @@ declare module 'vscode' { } /** - * A reference to a named icon. Currently, [File](#ThemeIcon.File), [Folder](#ThemeIcon.Folder), + * A reference to a named icon. Currently, {@link ThemeIcon.File File}, {@link ThemeIcon.Folder Folder}, * and [ThemeIcon ids](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing) are supported. * Using a theme icon is preferred over a custom icon as it gives product theme authors the possibility to change the icons. * @@ -814,25 +815,25 @@ declare module 'vscode' { readonly id: string; /** - * The optional ThemeColor of the icon. The color is currently only used in [TreeItem](#TreeItem). + * The optional ThemeColor of the icon. The color is currently only used in {@link TreeItem}. */ readonly color?: ThemeColor; /** * Creates a reference to a theme icon. * @param id id of the icon. The available icons are listed in https://code.visualstudio.com/api/references/icons-in-labels#icon-listing. - * @param color optional `ThemeColor` for the icon. The color is currently only used in [TreeItem](#TreeItem). + * @param color optional `ThemeColor` for the icon. The color is currently only used in {@link TreeItem}. */ constructor(id: string, color?: ThemeColor); } /** - * Represents theme specific rendering styles for a [text editor decoration](#TextEditorDecorationType). + * Represents theme specific rendering styles for a {@link TextEditorDecorationType text editor decoration}. */ export interface ThemableDecorationRenderOptions { /** * Background color of the decoration. Use rgba() and define transparent background colors to play well with other decorations. - * Alternatively a color from the color registry can be [referenced](#ThemeColor). + * Alternatively a color from the color registry can be {@link ThemeColor referenced}. */ backgroundColor?: string | ThemeColor; @@ -1010,7 +1011,7 @@ declare module 'vscode' { } /** - * Represents rendering styles for a [text editor decoration](#TextEditorDecorationType). + * Represents rendering styles for a {@link TextEditorDecorationType text editor decoration}. */ export interface DecorationRenderOptions extends ThemableDecorationRenderOptions { /** @@ -1042,7 +1043,7 @@ declare module 'vscode' { } /** - * Represents options for a specific decoration in a [decoration set](#TextEditorDecorationType). + * Represents options for a specific decoration in a {@link TextEditorDecorationType decoration set}. */ export interface DecorationOptions { @@ -1088,7 +1089,7 @@ declare module 'vscode' { } /** - * Represents an editor that is attached to a [document](#TextDocument). + * Represents an editor that is attached to a {@link TextDocument document}. */ export interface TextEditor { @@ -1128,18 +1129,18 @@ declare module 'vscode' { /** * Perform an edit on the document associated with this text editor. * - * The given callback-function is invoked with an [edit-builder](#TextEditorEdit) which must + * The given callback-function is invoked with an {@link TextEditorEdit edit-builder} which must * be used to make edits. Note that the edit-builder is only valid while the * callback executes. * - * @param callback A function which can create edits using an [edit-builder](#TextEditorEdit). + * @param callback A function which can create edits using an {@link TextEditorEdit edit-builder}. * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. * @return A promise that resolves with a value indicating if the edits could be applied. */ edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; /** - * Insert a [snippet](#SnippetString) and put the editor into snippet mode. "Snippet mode" + * Insert a {@link SnippetString snippet} and put the editor into snippet mode. "Snippet mode" * means the editor adds placeholders and additional cursors so that the user can complete * or accept the snippet. * @@ -1149,18 +1150,20 @@ declare module 'vscode' { * @return A promise that resolves with a value indicating if the snippet could be inserted. Note that the promise does not signal * that the snippet is completely filled-in or accepted. */ - insertSnippet(snippet: SnippetString, location?: Position | Range | ReadonlyArray | ReadonlyArray, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + insertSnippet(snippet: SnippetString, location?: Position | Range | readonly Position[] | readonly Range[], options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; /** * Adds a set of decorations to the text editor. If a set of decorations already exists with - * the given [decoration type](#TextEditorDecorationType), they will be replaced. + * the given {@link TextEditorDecorationType decoration type}, they will be replaced. If + * `rangesOrOptions` is empty, the existing decorations with the given {@link TextEditorDecorationType decoration type} + * will be removed. * - * @see [createTextEditorDecorationType](#window.createTextEditorDecorationType). + * @see {@link window.createTextEditorDecorationType createTextEditorDecorationType}. * * @param decorationType A decoration type. - * @param rangesOrOptions Either [ranges](#Range) or more detailed [options](#DecorationOptions). + * @param rangesOrOptions Either {@link Range ranges} or more detailed {@link DecorationOptions options}. */ - setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]): void; + setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: readonly Range[] | readonly DecorationOptions[]): void; /** * Scroll as indicated by `revealType` in order to reveal the given range. @@ -1173,9 +1176,9 @@ declare module 'vscode' { /** * Show the text editor. * - * @deprecated Use [window.showTextDocument](#window.showTextDocument) instead. + * @deprecated Use {@link window.showTextDocument} instead. * - * @param column The [column](#ViewColumn) in which to show this editor. + * @param column The {@link ViewColumn column} in which to show this editor. * This method shows unexpected behavior and will be removed in the next major update. */ show(column?: ViewColumn): void; @@ -1190,7 +1193,7 @@ declare module 'vscode' { } /** - * Represents an end of line character sequence in a [document](#TextDocument). + * Represents an end of line character sequence in a {@link TextDocument document}. */ export enum EndOfLine { /** @@ -1206,12 +1209,12 @@ declare module 'vscode' { /** * A complex edit that will be applied in one transaction on a TextEditor. * This holds a description of the edits and if the edits are valid (i.e. no overlapping regions, document was not changed in the meantime, etc.) - * they can be applied on a [document](#TextDocument) associated with a [text editor](#TextEditor). + * they can be applied on a {@link TextDocument document} associated with a {@link TextEditor text editor}. */ export interface TextEditorEdit { /** * Replace a certain text region with a new value. - * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). + * You can use \r\n or \n in `value` and they will be normalized to the current {@link TextDocument document}. * * @param location The range this operation should remove. * @param value The new text this operation should insert after removing `location`. @@ -1220,8 +1223,8 @@ declare module 'vscode' { /** * Insert text at a location. - * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). - * Although the equivalent text edit can be made with [replace](#TextEditorEdit.replace), `insert` will produce a different resulting selection (it will get moved). + * You can use \r\n or \n in `value` and they will be normalized to the current {@link TextDocument document}. + * Although the equivalent text edit can be made with {@link TextEditorEdit.replace replace}, `insert` will produce a different resulting selection (it will get moved). * * @param location The position where the new text should be inserted. * @param value The new text this operation should insert. @@ -1238,7 +1241,7 @@ declare module 'vscode' { /** * Set the end of line sequence. * - * @param endOfLine The new end of line for the [document](#TextDocument). + * @param endOfLine The new end of line for the {@link TextDocument document}. */ setEndOfLine(endOfLine: EndOfLine): void; } @@ -1257,7 +1260,7 @@ declare module 'vscode' { * as all uris should have a scheme. To avoid breakage of existing code the optional * `strict`-argument has been added. We *strongly* advise to use it, e.g. `Uri.parse('my:uri', true)` * - * @see [Uri.toString](#Uri.toString) + * @see {@link Uri.toString} * @param value The string value of an Uri. * @param strict Throw an error when `value` is empty or when no `scheme` can be parsed. * @return A new Uri instance. @@ -1265,10 +1268,10 @@ declare module 'vscode' { static parse(value: string, strict?: boolean): Uri; /** - * Create an URI from a file system path. The [scheme](#Uri.scheme) + * Create an URI from a file system path. The {@link Uri.scheme scheme} * will be `file`. * - * The *difference* between `Uri#parse` and `Uri#file` is that the latter treats the argument + * The *difference* between {@link Uri.parse} and {@link Uri.file} is that the latter treats the argument * as path, not as stringified-uri. E.g. `Uri.file(path)` is *not* the same as * `Uri.parse('file://' + path)` because the path might contain characters that are * interpreted (# and ?). See the following sample: @@ -1311,6 +1314,15 @@ declare module 'vscode' { */ static joinPath(base: Uri, ...pathSegments: string[]): Uri; + /** + * Create an URI from its component parts + * + * @see {@link Uri.toString} + * @param components The component parts of an Uri. + * @return A new Uri instance. + */ + static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri; + /** * Use the `file` and `parse` factory functions to create new `Uri` objects. */ @@ -1354,7 +1366,7 @@ declare module 'vscode' { * * The resulting string shall *not* be used for display purposes but * for disk operations, like `readFile` et al. * - * The *difference* to the [`path`](#Uri.path)-property is the use of the platform specific + * The *difference* to the {@link Uri.path `path`}-property is the use of the platform specific * path separator and the handling of UNC paths. The sample below outlines the difference: * ```ts const u = URI.parse('file://server/c$/folder/file.txt') @@ -1385,7 +1397,7 @@ declare module 'vscode' { * Returns a string representation of this Uri. The representation and normalization * of a URI depends on the scheme. * - * * The resulting string can be safely used with [Uri.parse](#Uri.parse). + * * The resulting string can be safely used with {@link Uri.parse}. * * The resulting string shall *not* be used for display purposes. * * *Note* that the implementation will encode _aggressive_ which often leads to unexpected, @@ -1414,7 +1426,7 @@ declare module 'vscode' { * for completion items because the user continued to type. * * To get an instance of a `CancellationToken` use a - * [CancellationTokenSource](#CancellationTokenSource). + * {@link CancellationTokenSource}. */ export interface CancellationToken { @@ -1424,13 +1436,13 @@ declare module 'vscode' { isCancellationRequested: boolean; /** - * An [event](#Event) which fires upon cancellation. + * An {@link Event} which fires upon cancellation. */ onCancellationRequested: Event; } /** - * A cancellation source creates and controls a [cancellation token](#CancellationToken). + * A cancellation source creates and controls a {@link CancellationToken cancellation token}. */ export class CancellationTokenSource { @@ -1453,7 +1465,7 @@ declare module 'vscode' { /** * An error type that should be used to signal cancellation of an operation. * - * This type can be used in response to a [cancellation token](#CancellationToken) + * This type can be used in response to a {@link CancellationToken cancellation token} * being cancelled or when an operation is being cancelled by the * executor of that operation. */ @@ -1512,18 +1524,18 @@ declare module 'vscode' { * * @param listener The listener function will be called when the event happens. * @param thisArgs The `this`-argument which will be used when calling the event listener. - * @param disposables An array to which a [disposable](#Disposable) will be added. + * @param disposables An array to which a {@link Disposable} will be added. * @return A disposable which unsubscribes the event listener. */ (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable; } /** - * An event emitter can be used to create and manage an [event](#Event) for others + * An event emitter can be used to create and manage an {@link Event} for others * to subscribe to. One emitter always owns one event. * * Use this class if you want to provide event from within your extension, for instance - * inside a [TextDocumentContentProvider](#TextDocumentContentProvider) or when providing + * inside a {@link TextDocumentContentProvider} or when providing * API to other extensions. */ export class EventEmitter { @@ -1534,7 +1546,7 @@ declare module 'vscode' { event: Event; /** - * Notify all subscribers of the [event](#EventEmitter.event). Failure + * Notify all subscribers of the {@link EventEmitter.event event}. Failure * of one or more listener will not fail this function call. * * @param data The event object. @@ -1549,10 +1561,10 @@ declare module 'vscode' { /** * A file system watcher notifies about changes to files and folders - * on disk or from other [FileSystemProviders](#FileSystemProvider). + * on disk or from other {@link FileSystemProvider FileSystemProviders}. * * To get an instance of a `FileSystemWatcher` use - * [createFileSystemWatcher](#workspace.createFileSystemWatcher). + * {@link workspace.createFileSystemWatcher createFileSystemWatcher}. */ export interface FileSystemWatcher extends Disposable { @@ -1594,9 +1606,9 @@ declare module 'vscode' { * A text document content provider allows to add readonly documents * to the editor, such as source from a dll or generated html from md. * - * Content providers are [registered](#workspace.registerTextDocumentContentProvider) - * for a [uri-scheme](#Uri.scheme). When a uri with that scheme is to - * be [loaded](#workspace.openTextDocument) the content provider is + * Content providers are {@link workspace.registerTextDocumentContentProvider registered} + * for a {@link Uri.scheme uri-scheme}. When a uri with that scheme is to + * be {@link workspace.openTextDocument loaded} the content provider is * asked. */ export interface TextDocumentContentProvider { @@ -1610,13 +1622,13 @@ declare module 'vscode' { * Provide textual content for a given uri. * * The editor will use the returned string-content to create a readonly - * [document](#TextDocument). Resources allocated should be released when - * the corresponding document has been [closed](#workspace.onDidCloseTextDocument). + * {@link TextDocument document}. Resources allocated should be released when + * the corresponding document has been {@link workspace.onDidCloseTextDocument closed}. * - * **Note**: The contents of the created [document](#TextDocument) might not be + * **Note**: The contents of the created {@link TextDocument document} might not be * identical to the provided text due to end-of-line-sequence normalization. * - * @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for. + * @param uri An uri which scheme matches the scheme this provider was {@link workspace.registerTextDocumentContentProvider registered} for. * @param token A cancellation token. * @return A string or a thenable that resolves to such. */ @@ -1630,20 +1642,20 @@ declare module 'vscode' { export interface QuickPickItem { /** - * A human-readable string which is rendered prominent. Supports rendering of [theme icons](#ThemeIcon) via + * A human-readable string which is rendered prominent. Supports rendering of {@link ThemeIcon theme icons} via * the `$()`-syntax. */ label: string; /** * A human-readable string which is rendered less prominent in the same line. Supports rendering of - * [theme icons](#ThemeIcon) via the `$()`-syntax. + * {@link ThemeIcon theme icons} via the `$()`-syntax. */ description?: string; /** * A human-readable string which is rendered less prominent in a separate line. Supports rendering of - * [theme icons](#ThemeIcon) via the `$()`-syntax. + * {@link ThemeIcon theme icons} via the `$()`-syntax. */ detail?: string; @@ -1651,7 +1663,7 @@ declare module 'vscode' { * Optional flag indicating if this item is picked initially. * (Only honored when the picker allows multiple selections.) * - * @see [QuickPickOptions.canPickMany](#QuickPickOptions.canPickMany) + * @see {@link QuickPickOptions.canPickMany} */ picked?: boolean; @@ -1703,7 +1715,7 @@ declare module 'vscode' { } /** - * Options to configure the behaviour of the [workspace folder](#WorkspaceFolder) pick UI. + * Options to configure the behaviour of the {@link WorkspaceFolder workspace folder} pick UI. */ export interface WorkspaceFolderPickOptions { @@ -1812,9 +1824,9 @@ declare module 'vscode' { * Represents an action that is shown with an information, warning, or * error message. * - * @see [showInformationMessage](#window.showInformationMessage) - * @see [showWarningMessage](#window.showWarningMessage) - * @see [showErrorMessage](#window.showErrorMessage) + * @see {@link window.showInformationMessage showInformationMessage} + * @see {@link window.showWarningMessage showWarningMessage} + * @see {@link window.showErrorMessage showErrorMessage} */ export interface MessageItem { @@ -1836,9 +1848,9 @@ declare module 'vscode' { /** * Options to configure the behavior of the message. * - * @see [showInformationMessage](#window.showInformationMessage) - * @see [showWarningMessage](#window.showWarningMessage) - * @see [showErrorMessage](#window.showErrorMessage) + * @see {@link window.showInformationMessage showInformationMessage} + * @see {@link window.showWarningMessage showWarningMessage} + * @see {@link window.showErrorMessage showErrorMessage} */ export interface MessageOptions { @@ -1864,7 +1876,7 @@ declare module 'vscode' { value?: string; /** - * Selection of the prefilled [`value`](#InputBoxOptions.value). Defined as tuple of two number where the + * Selection of the prefilled {@link InputBoxOptions.value `value`}. Defined as tuple of two number where the * first is the inclusive start index and the second the exclusive end index. When `undefined` the whole * word will be selected, when empty (start equals end) only the cursor will be set, * otherwise the defined range will be selected. @@ -1905,7 +1917,7 @@ declare module 'vscode' { /** * A relative pattern is a helper to construct glob patterns that are matched * relatively to a base file path. The base path can either be an absolute file - * path as string or uri or a [workspace folder](#WorkspaceFolder), which is the + * path as string or uri or a {@link WorkspaceFolder workspace folder}, which is the * preferred way of creating the relative pattern. */ export class RelativePattern { @@ -1942,7 +1954,7 @@ declare module 'vscode' { * ``` * * @param base A base to which this pattern will be matched against relatively. It is recommended - * to pass in a [workspace folder](#WorkspaceFolder) if the pattern should match inside the workspace. + * to pass in a {@link WorkspaceFolder workspace folder} if the pattern should match inside the workspace. * Otherwise, a uri or string should only be used if the pattern is for a file path outside the workspace. * @param pattern A file glob pattern like `*.{ts,js}` that will be matched on paths relative to the base. */ @@ -1951,7 +1963,7 @@ declare module 'vscode' { /** * A file glob pattern to match file paths against. This can either be a glob pattern string - * (like `**โ€‹/*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern). + * (like `**โ€‹/*.{ts,js}` or `*.{ts,js}`) or a {@link RelativePattern relative pattern}. * * Glob patterns can have the following syntax: * * `*` to match one or more characters in a path segment @@ -1962,7 +1974,7 @@ declare module 'vscode' { * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) * * Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file - * path to match against, consider to use the [relative pattern](#RelativePattern) support + * path to match against, consider to use the {@link RelativePattern relative pattern} support * that takes care of converting any backslash into slash. Otherwise, make sure to convert * any backslash to slash when creating the glob pattern. */ @@ -1970,8 +1982,8 @@ declare module 'vscode' { /** * A document filter denotes a document by different properties like - * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of - * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). + * the {@link TextDocument.languageId language}, the {@link Uri.scheme scheme} of + * its resource, or a glob-pattern that is applied to the {@link TextDocument.fileName path}. * * @example A language filter that applies to typescript files on disk * { language: 'typescript', scheme: 'file' } @@ -1987,20 +1999,20 @@ declare module 'vscode' { readonly language?: string; /** - * A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + * A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. */ readonly scheme?: string; /** - * A [glob pattern](#GlobPattern) that is matched on the absolute path of the document. Use a [relative pattern](#RelativePattern) - * to filter documents to a [workspace folder](#WorkspaceFolder). + * A {@link GlobPattern glob pattern} that is matched on the absolute path of the document. Use a {@link RelativePattern relative pattern} + * to filter documents to a {@link WorkspaceFolder workspace folder}. */ readonly pattern?: GlobPattern; } /** * A language selector is the combination of one or many language identifiers - * and [language filters](#DocumentFilter). + * and {@link DocumentFilter language filters}. * * *Note* that a document selector that is just a language identifier selects *all* * documents, even those that are not saved on disk. Only use such selectors when @@ -2013,12 +2025,12 @@ declare module 'vscode' { export type DocumentSelector = DocumentFilter | string | ReadonlyArray; /** - * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * A provider result represents the values a provider, like the {@link HoverProvider `HoverProvider`}, * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. * - * The snippets below are all valid implementations of the [`HoverProvider`](#HoverProvider): + * The snippets below are all valid implementations of the {@link HoverProvider `HoverProvider`}: * * ```ts * let a: HoverProvider = { @@ -2049,7 +2061,7 @@ declare module 'vscode' { * * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. * - * Code action kinds are used by VS Code for UI elements such as the refactoring context menu. Users + * Code action kinds are used by the editor for UI elements such as the refactoring context menu. Users * can also trigger code actions with a specific kind with the `editor.action.codeAction` command. */ export class CodeActionKind { @@ -2188,7 +2200,7 @@ declare module 'vscode' { /** * Contains additional diagnostic information about the context in which - * a [code action](#CodeActionProvider.provideCodeActions) is run. + * a {@link CodeActionProvider.provideCodeActions code action} is run. */ export interface CodeActionContext { /** @@ -2199,7 +2211,7 @@ declare module 'vscode' { /** * An array of diagnostics. */ - readonly diagnostics: ReadonlyArray; + readonly diagnostics: readonly Diagnostic[]; /** * Requested kind of actions to return. @@ -2213,7 +2225,7 @@ declare module 'vscode' { * A code action represents a change that can be performed in code, e.g. to fix a problem or * to refactor code. * - * A CodeAction must set either [`edit`](#CodeAction.edit) and/or a [`command`](#CodeAction.command). If both are supplied, the `edit` is applied first, then the command is executed. + * A CodeAction must set either {@link CodeAction.edit `edit`} and/or a {@link CodeAction.command `command`}. If both are supplied, the `edit` is applied first, then the command is executed. */ export class CodeAction { @@ -2223,25 +2235,25 @@ declare module 'vscode' { title: string; /** - * A [workspace edit](#WorkspaceEdit) this code action performs. + * A {@link WorkspaceEdit workspace edit} this code action performs. */ edit?: WorkspaceEdit; /** - * [Diagnostics](#Diagnostic) that this code action resolves. + * {@link Diagnostic Diagnostics} that this code action resolves. */ diagnostics?: Diagnostic[]; /** - * A [command](#Command) this code action executes. + * A {@link Command} this code action executes. * - * If this command throws an exception, VS Code displays the exception message to users in the editor at the + * If this command throws an exception, the editor displays the exception message to users in the editor at the * current cursor position. */ command?: Command; /** - * [Kind](#CodeActionKind) of the code action. + * {@link CodeActionKind Kind} of the code action. * * Used to filter code actions. */ @@ -2266,7 +2278,7 @@ declare module 'vscode' { * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user an + * that auto applies a code action and only a disabled code actions are returned, the editor will show the user an * error message with `reason` in the editor. */ disabled?: { @@ -2281,8 +2293,8 @@ declare module 'vscode' { /** * Creates a new code action. * - * A code action must have at least a [title](#CodeAction.title) and [edits](#CodeAction.edit) - * and/or a [command](#CodeAction.command). + * A code action must have at least a {@link CodeAction.title title} and {@link CodeAction.edit edits} + * and/or a {@link CodeAction.command command}. * * @param title The title of the code action. * @param kind The kind of the code action. @@ -2294,7 +2306,7 @@ declare module 'vscode' { * The code action interface defines the contract between extensions and * the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. * - * A code action can be any command that is [known](#commands.getCommands) to the system. + * A code action can be any command that is {@link commands.getCommands known} to the system. */ export interface CodeActionProvider { /** @@ -2315,7 +2327,7 @@ declare module 'vscode' { provideCodeActions(document: TextDocument, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | T)[]>; /** - * Given a code action fill in its [`edit`](#CodeAction.edit)-property. Changes to + * Given a code action fill in its {@link CodeAction.edit `edit`}-property. Changes to * all other properties, like title, are ignored. A code action that has an edit * will not be resolved. * @@ -2332,28 +2344,28 @@ declare module 'vscode' { } /** - * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) provides. + * Metadata about the type of code actions that a {@link CodeActionProvider} provides. */ export interface CodeActionProviderMetadata { /** - * List of [CodeActionKinds](#CodeActionKind) that a [CodeActionProvider](#CodeActionProvider) may return. + * List of {@link CodeActionKind CodeActionKinds} that a {@link CodeActionProvider} may return. * * This list is used to determine if a given `CodeActionProvider` should be invoked or not. * To avoid unnecessary computation, every `CodeActionProvider` should list use `providedCodeActionKinds`. The * list of kinds may either be generic, such as `[CodeActionKind.Refactor]`, or list out every kind provided, * such as `[CodeActionKind.Refactor.Extract.append('function'), CodeActionKind.Refactor.Extract.append('constant'), ...]`. */ - readonly providedCodeActionKinds?: ReadonlyArray; + readonly providedCodeActionKinds?: readonly CodeActionKind[]; /** * Static documentation for a class of code actions. * * Documentation from the provider is shown in the code actions menu if either: * - * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * - Code actions of `kind` are requested by the editor. In this case, the editor will show the documentation that * most closely matches the requested code action kind. For example, if a provider has documentation for * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, - * VS Code will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. + * the editor will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. * * - Any code actions of `kind` are returned by the provider. * @@ -2372,23 +2384,23 @@ declare module 'vscode' { /** * Command that displays the documentation to the user. * - * This can display the documentation directly in VS Code or open a website using [`env.openExternal`](#env.openExternal); + * This can display the documentation directly in the editor or open a website using {@link env.openExternal `env.openExternal`}; * - * The title of this documentation code action is taken from [`Command.title`](#Command.title) + * The title of this documentation code action is taken from {@link Command.title `Command.title`} */ readonly command: Command; }>; } /** - * A code lens represents a [command](#Command) that should be shown along with + * A code lens represents a {@link Command} that should be shown along with * source text, like the number of references, a way to run tests, etc. * * A code lens is _unresolved_ when no command is associated to it. For performance * reasons the creation of a code lens and resolving should be done to two stages. * - * @see [CodeLensProvider.provideCodeLenses](#CodeLensProvider.provideCodeLenses) - * @see [CodeLensProvider.resolveCodeLens](#CodeLensProvider.resolveCodeLens) + * @see {@link CodeLensProvider.provideCodeLenses} + * @see {@link CodeLensProvider.resolveCodeLens} */ export class CodeLens { @@ -2417,7 +2429,7 @@ declare module 'vscode' { } /** - * A code lens provider adds [commands](#Command) to source text. The commands will be shown + * A code lens provider adds {@link Command commands} to source text. The commands will be shown * as dedicated horizontal lines in between the source text. */ export interface CodeLensProvider { @@ -2428,9 +2440,9 @@ declare module 'vscode' { onDidChangeCodeLenses?: Event; /** - * Compute a list of [lenses](#CodeLens). This call should return as fast as possible and if + * Compute a list of {@link CodeLens lenses}. This call should return as fast as possible and if * computing the commands is expensive implementors should only return code lens objects with the - * range set and implement [resolve](#CodeLensProvider.resolveCodeLens). + * range set and implement {@link CodeLensProvider.resolveCodeLens resolve}. * * @param document The document in which the command was invoked. * @param token A cancellation token. @@ -2441,7 +2453,7 @@ declare module 'vscode' { /** * This function will be called for each visible code lens, usually when scrolling and after - * calls to [compute](#CodeLensProvider.provideCodeLenses)-lenses. + * calls to {@link CodeLensProvider.provideCodeLenses compute}-lenses. * * @param codeLens Code lens that must be resolved. * @param token A cancellation token. @@ -2453,7 +2465,7 @@ declare module 'vscode' { /** * Information about where a symbol is defined. * - * Provides additional metadata over normal {@link Location location} definitions, including the range of + * Provides additional metadata over normal {@link Location} definitions, including the range of * the defining symbol */ export type DefinitionLink = LocationLink; @@ -2521,8 +2533,8 @@ declare module 'vscode' { } /** - * The declaration of a symbol representation as one or many [locations](#Location) - * or [location links](#LocationLink). + * The declaration of a symbol representation as one or many {@link Location locations} + * or {@link LocationLink location links}. */ export type Declaration = Location | Location[] | LocationLink[]; @@ -2548,7 +2560,7 @@ declare module 'vscode' { * The MarkdownString represents human-readable text that supports formatting via the * markdown syntax. Standard markdown is supported, also tables, but no embedded html. * - * When created with `supportThemeIcons` then rendering of [theme icons](#ThemeIcon) via + * When created with `supportThemeIcons` then rendering of {@link ThemeIcon theme icons} via * the `$()`-syntax is supported. */ export class MarkdownString { @@ -2565,7 +2577,7 @@ declare module 'vscode' { isTrusted?: boolean; /** - * Indicates that this markdown string can contain [ThemeIcons](#ThemeIcon), e.g. `$(zap)`. + * Indicates that this markdown string can contain {@link ThemeIcon ThemeIcons}, e.g. `$(zap)`. */ readonly supportThemeIcons?: boolean; @@ -2573,7 +2585,7 @@ declare module 'vscode' { * Creates a new markdown string with the given value. * * @param value Optional, initial value. - * @param supportThemeIcons Optional, Specifies whether [ThemeIcons](#ThemeIcon) are supported within the [`MarkdownString`](#MarkdownString). + * @param supportThemeIcons Optional, Specifies whether {@link ThemeIcon ThemeIcons} are supported within the {@link MarkdownString `MarkdownString`}. */ constructor(value?: string, supportThemeIcons?: boolean); @@ -2584,7 +2596,7 @@ declare module 'vscode' { appendText(value: string): MarkdownString; /** - * Appends the given string 'as is' to this markdown string. When [`supportThemeIcons`](#MarkdownString.supportThemeIcons) is `true`, [ThemeIcons](#ThemeIcon) in the `value` will be iconified. + * Appends the given string 'as is' to this markdown string. When {@link MarkdownString.supportThemeIcons `supportThemeIcons`} is `true`, {@link ThemeIcon ThemeIcons} in the `value` will be iconified. * @param value Markdown string. */ appendMarkdown(value: string): MarkdownString; @@ -2592,7 +2604,7 @@ declare module 'vscode' { /** * Appends the given string as codeblock using the provided language. * @param value A code snippet. - * @param language An optional [language identifier](#languages.getLanguages). + * @param language An optional {@link languages.getLanguages language identifier}. */ appendCodeblock(value: string, language?: string): MarkdownString; } @@ -2602,7 +2614,7 @@ declare module 'vscode' { * or a code-block that provides a language and a code snippet. Note that * markdown strings will be sanitized - that means html will be escaped. * - * @deprecated This type is deprecated, please use [`MarkdownString`](#MarkdownString) instead. + * @deprecated This type is deprecated, please use {@link MarkdownString `MarkdownString`} instead. */ export type MarkedString = MarkdownString | string | { language: string; value: string }; @@ -2684,13 +2696,13 @@ declare module 'vscode' { /** * The evaluatable expression provider interface defines the contract between extensions and * the debug hover. In this contract the provider returns an evaluatable expression for a given position - * in a document and VS Code evaluates this expression in the active debug session and shows the result in a debug hover. + * in a document and the editor evaluates this expression in the active debug session and shows the result in a debug hover. */ export interface EvaluatableExpressionProvider { /** * Provide an evaluatable expression for the given document and position. - * VS Code will evaluate this expression in the active debug session and will show the result in the debug hover. + * The editor will evaluate this expression in the active debug session and will show the result in the debug hover. * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. * * @param document The document for which the debug hover is about to appear. @@ -2811,7 +2823,7 @@ declare module 'vscode' { /** * An optional event to signal that inline values have changed. - * @see [EventEmitter](#EventEmitter) + * @see {@link EventEmitter} */ onDidChangeInlineValues?: Event | undefined; @@ -2864,7 +2876,7 @@ declare module 'vscode' { range: Range; /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ kind?: DocumentHighlightKind; @@ -2872,7 +2884,7 @@ declare module 'vscode' { * Creates a new document highlight object. * * @param range The range the highlight applies to. - * @param kind The highlight kind, default is [text](#DocumentHighlightKind.Text). + * @param kind The highlight kind, default is {@link DocumentHighlightKind.Text text}. */ constructor(range: Range, kind?: DocumentHighlightKind); } @@ -2963,7 +2975,7 @@ declare module 'vscode' { /** * Tags for this symbol. */ - tags?: ReadonlyArray; + tags?: readonly SymbolTag[]; /** * The location of this symbol. @@ -2983,7 +2995,7 @@ declare module 'vscode' { /** * Creates a new symbol information object. * - * @deprecated Please use the constructor taking a [location](#Location) object. + * @deprecated Please use the constructor taking a {@link Location} object. * * @param name The name of the symbol. * @param kind The kind of the symbol. @@ -3019,7 +3031,7 @@ declare module 'vscode' { /** * Tags for this symbol. */ - tags?: ReadonlyArray; + tags?: readonly SymbolTag[]; /** * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. @@ -3028,7 +3040,7 @@ declare module 'vscode' { /** * The range that should be selected and reveal when this symbol is being picked, e.g. the name of a function. - * Must be contained by the [`range`](#DocumentSymbol.range). + * Must be contained by the {@link DocumentSymbol.range `range`}. */ selectionRange: Range; @@ -3091,7 +3103,7 @@ declare module 'vscode' { * strict matching. * * To improve performance implementors can implement `resolveWorkspaceSymbol` and then provide symbols with partial - * [location](#SymbolInformation.location)-objects, without a `range` defined. The editor will then call + * {@link SymbolInformation.location location}-objects, without a `range` defined. The editor will then call * `resolveWorkspaceSymbol` for selected symbols only, e.g. when opening a workspace symbol. * * @param query A query string, can be the empty string in which case all symbols should be returned. @@ -3102,9 +3114,9 @@ declare module 'vscode' { provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult; /** - * Given a symbol fill in its [location](#SymbolInformation.location). This method is called whenever a symbol + * Given a symbol fill in its {@link SymbolInformation.location location}. This method is called whenever a symbol * is selected in the UI. Providers can implement this method and return incomplete symbols from - * [`provideWorkspaceSymbols`](#WorkspaceSymbolProvider.provideWorkspaceSymbols) which often helps to improve + * {@link WorkspaceSymbolProvider.provideWorkspaceSymbols `provideWorkspaceSymbols`} which often helps to improve * performance. * * @param symbol The symbol that is to be resolved. Guaranteed to be an instance of an object returned from an @@ -3237,7 +3249,7 @@ declare module 'vscode' { description?: string; /** - * The icon path or [ThemeIcon](#ThemeIcon) for the edit. + * The icon path or {@link ThemeIcon} for the edit. */ iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; } @@ -3246,7 +3258,7 @@ declare module 'vscode' { * A workspace edit is a collection of textual and files changes for * multiple resources and documents. * - * Use the [applyEdit](#workspace.applyEdit)-function to apply a workspace edit. + * Use the {@link workspace.applyEdit applyEdit}-function to apply a workspace edit. */ export class WorkspaceEdit { @@ -3368,7 +3380,7 @@ declare module 'vscode' { /** * Builder-function that appends the given string to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param string A value to append 'as given'. The string will be escaped. * @return This snippet string. @@ -3377,7 +3389,7 @@ declare module 'vscode' { /** * Builder-function that appends a tabstop (`$1`, `$2` etc) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param number The number of this tabstop, defaults to an auto-increment * value starting at 1. @@ -3387,7 +3399,7 @@ declare module 'vscode' { /** * Builder-function that appends a placeholder (`${1:value}`) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param value The value of this placeholder - either a string or a function * with which a nested snippet can be created. @@ -3399,7 +3411,7 @@ declare module 'vscode' { /** * Builder-function that appends a choice (`${1|a,b,c|}`) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param values The values for choices - the array of strings * @param number The number of this tabstop, defaults to an auto-increment @@ -3410,7 +3422,7 @@ declare module 'vscode' { /** * Builder-function that appends a variable (`${VAR}`) to - * the [`value`](#SnippetString.value) of this snippet string. + * the {@link SnippetString.value `value`} of this snippet string. * * @param name The name of the variable - excluding the `$`. * @param defaultValue The default value which is used when the variable name cannot @@ -3508,8 +3520,8 @@ declare module 'vscode' { /** * Represents semantic tokens, either in a range or in an entire document. - * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format. - * @see [SemanticTokensBuilder](#SemanticTokensBuilder) for a helper to create an instance. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format. + * @see {@link SemanticTokensBuilder} for a helper to create an instance. */ export class SemanticTokens { /** @@ -3520,7 +3532,7 @@ declare module 'vscode' { readonly resultId?: string; /** * The actual tokens data. - * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format. */ readonly data: Uint32Array; @@ -3529,7 +3541,7 @@ declare module 'vscode' { /** * Represents edits to semantic tokens. - * @see [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits provideDocumentSemanticTokensEdits} for an explanation of the format. */ export class SemanticTokensEdits { /** @@ -3549,7 +3561,7 @@ declare module 'vscode' { /** * Represents an edit to semantic tokens. - * @see [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format. + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits provideDocumentSemanticTokensEdits} for an explanation of the format. */ export class SemanticTokensEdit { /** @@ -3633,7 +3645,7 @@ declare module 'vscode' { * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * - * @see [SemanticTokensBuilder](#SemanticTokensBuilder) for a helper to encode tokens as integers. + * @see {@link SemanticTokensBuilder} for a helper to encode tokens as integers. * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. */ @@ -3677,7 +3689,7 @@ declare module 'vscode' { */ export interface DocumentRangeSemanticTokensProvider { /** - * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). + * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens}. */ provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; } @@ -3778,8 +3790,8 @@ declare module 'vscode' { * The label of this signature. * * Either a string or inclusive start and exclusive end offsets within its containing - * [signature label](#SignatureInformation.label). *Note*: A label of type string must be - * a substring of its containing signature information's [label](#SignatureInformation.label). + * {@link SignatureInformation.label signature label}. *Note*: A label of type string must be + * a substring of its containing signature information's {@link SignatureInformation.label label}. */ label: string | [number, number]; @@ -3825,7 +3837,7 @@ declare module 'vscode' { /** * The index of the active parameter. * - * If provided, this is used in place of [`SignatureHelp.activeSignature`](#SignatureHelp.activeSignature). + * If provided, this is used in place of {@link SignatureHelp.activeSignature `SignatureHelp.activeSignature`}. */ activeParameter?: number; @@ -3862,7 +3874,7 @@ declare module 'vscode' { } /** - * How a [`SignatureHelpProvider`](#SignatureHelpProvider) was triggered. + * How a {@link SignatureHelpProvider `SignatureHelpProvider`} was triggered. */ export enum SignatureHelpTriggerKind { /** @@ -3883,7 +3895,7 @@ declare module 'vscode' { /** * Additional information about the context in which a - * [`SignatureHelpProvider`](#SignatureHelpProvider.provideSignatureHelp) was triggered. + * {@link SignatureHelpProvider.provideSignatureHelp `SignatureHelpProvider`} was triggered. */ export interface SignatureHelpContext { /** @@ -3908,7 +3920,7 @@ declare module 'vscode' { readonly isRetrigger: boolean; /** - * The currently active [`SignatureHelp`](#SignatureHelp). + * The currently active {@link SignatureHelp `SignatureHelp`}. * * The `activeSignatureHelp` has its [`SignatureHelp.activeSignature`] field updated based on * the user arrowing through available signatures. @@ -3937,13 +3949,13 @@ declare module 'vscode' { } /** - * Metadata about a registered [`SignatureHelpProvider`](#SignatureHelpProvider). + * Metadata about a registered {@link SignatureHelpProvider `SignatureHelpProvider`}. */ export interface SignatureHelpProviderMetadata { /** * List of characters that trigger signature help. */ - readonly triggerCharacters: ReadonlyArray; + readonly triggerCharacters: readonly string[]; /** * List of characters that re-trigger signature help. @@ -3951,7 +3963,7 @@ declare module 'vscode' { * These trigger characters are only active when signature help is already showing. All trigger characters * are also counted as re-trigger characters. */ - readonly retriggerCharacters: ReadonlyArray; + readonly retriggerCharacters: readonly string[]; } /** @@ -4001,17 +4013,17 @@ declare module 'vscode' { /** * A completion item represents a text snippet that is proposed to complete text that is being typed. * - * It is sufficient to create a completion item from just a [label](#CompletionItem.label). In that - * case the completion item will replace the [word](#TextDocument.getWordRangeAtPosition) - * until the cursor with the given label or [insertText](#CompletionItem.insertText). Otherwise the - * given [edit](#CompletionItem.textEdit) is used. + * It is sufficient to create a completion item from just a {@link CompletionItem.label label}. In that + * case the completion item will replace the {@link TextDocument.getWordRangeAtPosition word} + * until the cursor with the given label or {@link CompletionItem.insertText insertText}. Otherwise the + * given {@link CompletionItem.textEdit edit} is used. * * When selecting a completion item in the editor its defined or synthesized text edit will be applied - * to *all* cursors/selections whereas [additionalTextEdits](#CompletionItem.additionalTextEdits) will be + * to *all* cursors/selections whereas {@link CompletionItem.additionalTextEdits additionalTextEdits} will be * applied as provided. * - * @see [CompletionItemProvider.provideCompletionItems](#CompletionItemProvider.provideCompletionItems) - * @see [CompletionItemProvider.resolveCompletionItem](#CompletionItemProvider.resolveCompletionItem) + * @see {@link CompletionItemProvider.provideCompletionItems} + * @see {@link CompletionItemProvider.resolveCompletionItem} */ export class CompletionItem { @@ -4031,7 +4043,7 @@ declare module 'vscode' { /** * Tags for this completion item. */ - tags?: ReadonlyArray; + tags?: readonly CompletionItemTag[]; /** * A human-readable string with additional information @@ -4046,25 +4058,25 @@ declare module 'vscode' { /** * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. * * Note that `sortText` is only used for the initial ordering of completion * items. When having a leading word (prefix) ordering is based on how * well completions match that prefix and the initial ordering is only used * when completions match equally well. The prefix is defined by the - * [`range`](#CompletionItem.range)-property and can therefore be different + * {@link CompletionItem.range `range`}-property and can therefore be different * for each completion. */ sortText?: string; /** * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. * * Note that the filter text is matched against the leading word (prefix) which is defined - * by the [`range`](#CompletionItem.range)-property. + * by the {@link CompletionItem.range `range`}-property. */ filterText?: string; @@ -4077,7 +4089,7 @@ declare module 'vscode' { /** * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. When `falsy` the {@link CompletionItem.label label} * is used. */ insertText?: string | SnippetString; @@ -4085,12 +4097,12 @@ declare module 'vscode' { /** * A range or a insert and replace range selecting the text that should be replaced by this completion item. * - * When omitted, the range of the [current word](#TextDocument.getWordRangeAtPosition) is used as replace-range - * and as insert-range the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * When omitted, the range of the {@link TextDocument.getWordRangeAtPosition current word} is used as replace-range + * and as insert-range the start of the {@link TextDocument.getWordRangeAtPosition current word} to the * current position is used. * - * *Note 1:* A range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note 1:* A range must be a {@link Range.isSingleLine single line} and it must + * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. * *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position. */ range?: Range | { inserting: Range; replacing: Range; }; @@ -4103,7 +4115,7 @@ declare module 'vscode' { commitCharacters?: string[]; /** - * Keep whitespace of the [insertText](#CompletionItem.insertText) as is. By default, the editor adjusts leading + * Keep whitespace of the {@link CompletionItem.insertText insertText} as is. By default, the editor adjusts leading * whitespace of new lines so that they match the indentation of the line for which the item is accepted - setting * this to `true` will prevent that. */ @@ -4112,43 +4124,43 @@ declare module 'vscode' { /** * @deprecated Use `CompletionItem.insertText` and `CompletionItem.range` instead. * - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this completion. When an edit is provided the value of - * [insertText](#CompletionItem.insertText) is ignored. + * {@link CompletionItem.insertText insertText} is ignored. * - * The [range](#Range) of the edit must be single-line and on the same - * line completions were [requested](#CompletionItemProvider.provideCompletionItems) at. + * The {@link Range} of the edit must be single-line and on the same + * line completions were {@link CompletionItemProvider.provideCompletionItems requested} at. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this completion. Edits must not overlap with the main [edit](#CompletionItem.textEdit) + * An optional array of additional {@link TextEdit text edits} that are applied when + * selecting this completion. Edits must not overlap with the main {@link CompletionItem.textEdit edit} * nor with themselves. */ additionalTextEdits?: TextEdit[]; /** - * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that + * An optional {@link Command} that is executed *after* inserting this completion. *Note* that * additional modifications to the current document should be described with the - * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. + * {@link CompletionItem.additionalTextEdits additionalTextEdits}-property. */ command?: Command; /** * Creates a new completion item. * - * Completion items must have at least a [label](#CompletionItem.label) which then + * Completion items must have at least a {@link CompletionItem.label label} which then * will be used as insert text as well as for sorting and filtering. * * @param label The label of the completion. - * @param kind The [kind](#CompletionItemKind) of the completion. + * @param kind The {@link CompletionItemKind kind} of the completion. */ constructor(label: string, kind?: CompletionItemKind); } /** - * Represents a collection of [completion items](#CompletionItem) to be presented + * Represents a collection of {@link CompletionItem completion items} to be presented * in the editor. */ export class CompletionList { @@ -4174,7 +4186,7 @@ declare module 'vscode' { } /** - * How a [completion provider](#CompletionItemProvider) was triggered + * How a {@link CompletionItemProvider completion provider} was triggered */ export enum CompletionTriggerKind { /** @@ -4193,7 +4205,7 @@ declare module 'vscode' { /** * Contains additional information about the context in which - * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered. */ export interface CompletionContext { /** @@ -4215,9 +4227,9 @@ declare module 'vscode' { * The completion item provider interface defines the contract between extensions and * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). * - * Providers can delay the computation of the [`detail`](#CompletionItem.detail) - * and [`documentation`](#CompletionItem.documentation) properties by implementing the - * [`resolveCompletionItem`](#CompletionItemProvider.resolveCompletionItem)-function. However, properties that + * Providers can delay the computation of the {@link CompletionItem.detail `detail`} + * and {@link CompletionItem.documentation `documentation`} properties by implementing the + * {@link CompletionItemProvider.resolveCompletionItem `resolveCompletionItem`}-function. However, properties that * are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `range`, must * not be changed during resolve. * @@ -4234,22 +4246,22 @@ declare module 'vscode' { * @param token A cancellation token. * @param context How the completion was triggered. * - * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. + * @return An array of completions, a {@link CompletionList completion list}, or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult>; /** - * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) - * or [details](#CompletionItem.detail). + * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment} + * or {@link CompletionItem.detail details}. * * The editor will only resolve a completion item once. * * *Note* that this function is called when completion items are already showing in the UI or when an item has been * selected for insertion. Because of that, no property that changes the presentation (label, sorting, filtering etc) - * or the (primary) insert behaviour ([insertText](#CompletionItem.insertText)) can be changed. + * or the (primary) insert behaviour ({@link CompletionItem.insertText insertText}) can be changed. * - * This function may fill in [additionalTextEdits](#CompletionItem.additionalTextEdits). However, that means an item might be + * This function may fill in {@link CompletionItem.additionalTextEdits additionalTextEdits}. However, that means an item might be * inserted *before* resolving is done and in that case the editor will do a best effort to still apply those additional * text edits. * @@ -4307,15 +4319,15 @@ declare module 'vscode' { * * @param document The document in which the command was invoked. * @param token A cancellation token. - * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result + * @return An array of {@link DocumentLink document links} or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult; /** - * Given a link fill in its [target](#DocumentLink.target). This method is called when an incomplete + * Given a link fill in its {@link DocumentLink.target target}. This method is called when an incomplete * link is selected in the UI. Providers can implement this method and return incomplete links - * (without target) from the [`provideDocumentLinks`](#DocumentLinkProvider.provideDocumentLinks) method which + * (without target) from the {@link DocumentLinkProvider.provideDocumentLinks `provideDocumentLinks`} method which * often helps to improve performance. * * @param link The link that is to be resolved. @@ -4386,7 +4398,7 @@ declare module 'vscode' { } /** - * A color presentation object describes how a [`color`](#Color) should be represented as text and what + * A color presentation object describes how a {@link Color `color`} should be represented as text and what * edits are required to refer to it from source code. * * For some languages one color can have multiple presentations, e.g. css can represent the color red with @@ -4403,15 +4415,15 @@ declare module 'vscode' { label: string; /** - * An [edit](#TextEdit) which is applied to a document when selecting - * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) + * An {@link TextEdit edit} which is applied to a document when selecting + * this presentation for the color. When `falsy` the {@link ColorPresentation.label label} * is used. */ textEdit?: TextEdit; /** - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. + * An optional array of additional {@link TextEdit text edits} that are applied when + * selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. */ additionalTextEdits?: TextEdit[]; @@ -4434,13 +4446,13 @@ declare module 'vscode' { * * @param document The document in which the command was invoked. * @param token A cancellation token. - * @return An array of [color information](#ColorInformation) or a thenable that resolves to such. The lack of a result + * @return An array of {@link ColorInformation color information} or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentColors(document: TextDocument, token: CancellationToken): ProviderResult; /** - * Provide [representations](#ColorPresentation) for a color. + * Provide {@link ColorPresentation representations} for a color. * * @param color The color to show and insert. * @param context A context object with additional information @@ -4470,10 +4482,10 @@ declare module 'vscode' { end: number; /** - * Describes the [Kind](#FoldingRangeKind) of the folding range such as [Comment](#FoldingRangeKind.Comment) or - * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of all kinds. + * {@link FoldingRangeKind} for an enumeration of all kinds. * If not set, the range is originated from a syntax element. */ kind?: FoldingRangeKind; @@ -4489,7 +4501,7 @@ declare module 'vscode' { } /** - * An enumeration of specific folding range kinds. The kind is an optional field of a [FoldingRange](#FoldingRange) + * An enumeration of specific folding range kinds. The kind is an optional field of a {@link FoldingRange} * and is used to distinguish specific folding ranges such as ranges originated from comments. The kind is used by commands like * `Fold all comments` or `Fold all regions`. * If the kind is not set on the range, the range originated from a syntax element other than comments, imports or region markers. @@ -4543,7 +4555,7 @@ declare module 'vscode' { export class SelectionRange { /** - * The [range](#Range) of this selection range. + * The {@link Range} of this selection range. */ range: Range; @@ -4567,7 +4579,7 @@ declare module 'vscode' { * * Selection ranges should be computed individually and independent for each position. The editor will merge * and deduplicate ranges but providers must return hierarchies of selection ranges so that a range - * is [contained](#Range.contains) by its parent. + * is {@link Range.contains contained} by its parent. * * @param document The document in which the command was invoked. * @param positions The positions at which the command was invoked. @@ -4596,7 +4608,7 @@ declare module 'vscode' { /** * Tags for this item. */ - tags?: ReadonlyArray; + tags?: readonly SymbolTag[]; /** * More detail for this item, e.g. the signature of a function. @@ -4615,7 +4627,7 @@ declare module 'vscode' { /** * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. - * Must be contained by the [`range`](#CallHierarchyItem.range). + * Must be contained by the {@link CallHierarchyItem.range `range`}. */ selectionRange: Range; @@ -4637,7 +4649,7 @@ declare module 'vscode' { /** * The range at which at which the calls appears. This is relative to the caller - * denoted by [`this.from`](#CallHierarchyIncomingCall.from). + * denoted by {@link CallHierarchyIncomingCall.from `this.from`}. */ fromRanges: Range[]; @@ -4662,8 +4674,8 @@ declare module 'vscode' { /** * The range at which this item is called. This is the range relative to the caller, e.g the item - * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyProvider.provideCallHierarchyOutgoingCalls) - * and not [`this.to`](#CallHierarchyOutgoingCall.to). + * passed to {@link CallHierarchyProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} + * and not {@link CallHierarchyOutgoingCall.to `this.to`}. */ fromRanges: Range[]; @@ -4970,10 +4982,10 @@ declare module 'vscode' { * - *Default Settings* * - *Global (User) Settings* * - *Workspace settings* - * - *Workspace Folder settings* - From one of the [Workspace Folders](#workspace.workspaceFolders) under which requested resource belongs to. + * - *Workspace Folder settings* - From one of the {@link workspace.workspaceFolders Workspace Folders} under which requested resource belongs to. * - *Language settings* - Settings defined under requested language. * - * The *effective* value (returned by [`get`](#WorkspaceConfiguration.get)) is computed by overriding or merging the values in the following order. + * The *effective* value (returned by {@link WorkspaceConfiguration.get `get`}) is computed by overriding or merging the values in the following order. * * ``` * `defaultValue` (if defined in `package.json` otherwise derived from the value's type) @@ -5059,7 +5071,7 @@ declare module 'vscode' { * Retrieve all information about a configuration setting. A configuration value * often consists of a *default* value, a global or installation-wide value, * a workspace-specific value, folder-specific value - * and language-specific values (if [WorkspaceConfiguration](#WorkspaceConfiguration) is scoped to a language). + * and language-specific values (if {@link WorkspaceConfiguration} is scoped to a language). * * Also provides all language ids under which the given configuration setting is defined. * @@ -5091,20 +5103,20 @@ declare module 'vscode' { * * A value can be changed in * - * - [Global settings](#ConfigurationTarget.Global): Changes the value for all instances of the editor. - * - [Workspace settings](#ConfigurationTarget.Workspace): Changes the value for current workspace, if available. - * - [Workspace folder settings](#ConfigurationTarget.WorkspaceFolder): Changes the value for settings from one of the [Workspace Folders](#workspace.workspaceFolders) under which the requested resource belongs to. + * - {@link ConfigurationTarget.Global Global settings}: Changes the value for all instances of the editor. + * - {@link ConfigurationTarget.Workspace Workspace settings}: Changes the value for current workspace, if available. + * - {@link ConfigurationTarget.WorkspaceFolder Workspace folder settings}: Changes the value for settings from one of the {@link workspace.workspaceFolders Workspace Folders} under which the requested resource belongs to. * - Language settings: Changes the value for the requested languageId. * * *Note:* To remove a configuration value use `undefined`, like so: `config.update('somekey', undefined)` * * @param section Configuration name, supports _dotted_ names. * @param value The new value. - * @param configurationTarget The [configuration target](#ConfigurationTarget) or a boolean value. - * - If `true` updates [Global settings](#ConfigurationTarget.Global). - * - If `false` updates [Workspace settings](#ConfigurationTarget.Workspace). - * - If `undefined` or `null` updates to [Workspace folder settings](#ConfigurationTarget.WorkspaceFolder) if configuration is resource specific, - * otherwise to [Workspace settings](#ConfigurationTarget.Workspace). + * @param configurationTarget The {@link ConfigurationTarget configuration target} or a boolean value. + * - If `true` updates {@link ConfigurationTarget.Global Global settings}. + * - If `false` updates {@link ConfigurationTarget.Workspace Workspace settings}. + * - If `undefined` or `null` updates to {@link ConfigurationTarget.WorkspaceFolder Workspace folder settings} if configuration is resource specific, + * otherwise to {@link ConfigurationTarget.Workspace Workspace settings}. * @param overrideInLanguage Whether to update the value in the scope of requested languageId or not. * - If `true` updates the value under the requested languageId. * - If `undefined` updates the value under the requested languageId only if the configuration is defined for the language. @@ -5113,7 +5125,7 @@ declare module 'vscode' { * - window configuration to workspace folder * - configuration to workspace or workspace folder when no workspace is opened. * - configuration to workspace folder when there is no workspace folder settings. - * - configuration to workspace folder when [WorkspaceConfiguration](#WorkspaceConfiguration) is not scoped to a resource. + * - configuration to workspace folder when {@link WorkspaceConfiguration} is not scoped to a resource. */ update(section: string, value: any, configurationTarget?: ConfigurationTarget | boolean, overrideInLanguage?: boolean): Thenable; @@ -5149,7 +5161,7 @@ declare module 'vscode' { } /** - * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), + * Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, * including an origin range. */ export interface LocationLink { @@ -5185,7 +5197,7 @@ declare module 'vscode' { /** * An array of resources for which diagnostics have changed. */ - readonly uris: ReadonlyArray; + readonly uris: readonly Uri[]; } /** @@ -5282,7 +5294,7 @@ declare module 'vscode' { message: string; /** - * The severity, default is [error](#DiagnosticSeverity.Error). + * The severity, default is {@link DiagnosticSeverity.Error error}. */ severity: DiagnosticSeverity; @@ -5294,12 +5306,12 @@ declare module 'vscode' { /** * A code or identifier for this diagnostic. - * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). + * Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}. */ code?: string | number | { /** * A code or identifier for this diagnostic. - * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). + * Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}. */ value: string | number; @@ -5325,18 +5337,18 @@ declare module 'vscode' { * * @param range The range to which this diagnostic applies. * @param message The human-readable message. - * @param severity The severity, default is [error](#DiagnosticSeverity.Error). + * @param severity The severity, default is {@link DiagnosticSeverity.Error error}. */ constructor(range: Range, message: string, severity?: DiagnosticSeverity); } /** * A diagnostics collection is a container that manages a set of - * [diagnostics](#Diagnostic). Diagnostics are always scopes to a + * {@link Diagnostic diagnostics}. Diagnostics are always scopes to a * diagnostics collection and a resource. * * To get an instance of a `DiagnosticCollection` use - * [createDiagnosticCollection](#languages.createDiagnosticCollection). + * {@link languages.createDiagnosticCollection createDiagnosticCollection}. */ export interface DiagnosticCollection { @@ -5354,7 +5366,7 @@ declare module 'vscode' { * @param uri A resource identifier. * @param diagnostics Array of diagnostics or `undefined` */ - set(uri: Uri, diagnostics: ReadonlyArray | undefined): void; + set(uri: Uri, diagnostics: readonly Diagnostic[] | undefined): void; /** * Replace diagnostics for multiple resources in this collection. @@ -5366,7 +5378,7 @@ declare module 'vscode' { * * @param entries An array of tuples, like `[[file1, [d1, d2]], [file2, [d3, d4, d5]]]`, or `undefined`. */ - set(entries: ReadonlyArray<[Uri, ReadonlyArray | undefined]>): void; + set(entries: ReadonlyArray<[Uri, readonly Diagnostic[] | undefined]>): void; /** * Remove all diagnostics from this collection that belong @@ -5388,16 +5400,16 @@ declare module 'vscode' { * @param callback Function to execute for each entry. * @param thisArg The `this` context used when invoking the handler function. */ - forEach(callback: (uri: Uri, diagnostics: ReadonlyArray, collection: DiagnosticCollection) => any, thisArg?: any): void; + forEach(callback: (uri: Uri, diagnostics: readonly Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void; /** * Get the diagnostics for a given resource. *Note* that you cannot * modify the diagnostics-array returned from this call. * * @param uri A resource identifier. - * @returns An immutable array of [diagnostics](#Diagnostic) or `undefined`. + * @returns An immutable array of {@link Diagnostic diagnostics} or `undefined`. */ - get(uri: Uri): ReadonlyArray | undefined; + get(uri: Uri): readonly Diagnostic[] | undefined; /** * Check if this collection contains diagnostics for a @@ -5410,7 +5422,7 @@ declare module 'vscode' { /** * Dispose and free associated resources. Calls - * [clear](#DiagnosticCollection.clear). + * {@link DiagnosticCollection.clear clear}. */ dispose(): void; } @@ -5423,13 +5435,13 @@ declare module 'vscode' { export enum ViewColumn { /** * A *symbolic* editor column representing the currently active column. This value - * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`. */ Active = -1, /** * A *symbolic* editor column representing the column to the side of the active one. This value - * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`. */ Beside = -2, @@ -5475,7 +5487,7 @@ declare module 'vscode' { * An output channel is a container for readonly textual information. * * To get an instance of an `OutputChannel` use - * [createOutputChannel](#window.createOutputChannel). + * {@link window.createOutputChannel createOutputChannel}. */ export interface OutputChannel { @@ -5572,6 +5584,14 @@ declare module 'vscode' { */ export interface StatusBarItem { + /** + * The identifier of this item. + * + * *Note*: if no identifier was provided by the {@link window.createStatusBarItem `window.createStatusBarItem`} + * method, the identifier will match the {@link Extension.id extension identifier}. + */ + readonly id: string; + /** * The alignment of this item. */ @@ -5583,6 +5603,13 @@ declare module 'vscode' { */ readonly priority?: number; + /** + * The name of the entry, like 'Python Language Indicator', 'Git Status' etc. + * Try to keep the length of the name short, yet descriptive enough that + * users can understand what the status bar item is about. + */ + name: string | undefined; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * @@ -5616,17 +5643,17 @@ declare module 'vscode' { backgroundColor: ThemeColor | undefined; /** - * [`Command`](#Command) or identifier of a command to run on click. + * {@link Command `Command`} or identifier of a command to run on click. * - * The command must be [known](#commands.getCommands). + * The command must be {@link commands.getCommands known}. * - * Note that if this is a [`Command`](#Command) object, only the [`command`](#Command.command) and [`arguments`](#Command.arguments) + * Note that if this is a {@link Command `Command`} object, only the {@link Command.command `command`} and {@link Command.arguments `arguments`} * are used by VS Code. */ command: string | Command | undefined; /** - * Accessibility information used when screen reader interacts with this StatusBar item + * Accessibility information used when a screen reader interacts with this StatusBar item */ accessibilityInformation?: AccessibilityInformation; @@ -5642,7 +5669,7 @@ declare module 'vscode' { /** * Dispose and free associated resources. Call - * [hide](#StatusBarItem.hide). + * {@link StatusBarItem.hide hide}. */ dispose(): void; } @@ -5767,12 +5794,12 @@ declare module 'vscode' { */ export interface TerminalLink { /** - * The start index of the link on [TerminalLinkContext.line](#TerminalLinkContext.line]. + * The start index of the link on {@link TerminalLinkContext.line}. */ startIndex: number; /** - * The length of the link on [TerminalLinkContext.line](#TerminalLinkContext.line] + * The length of the link on {@link TerminalLinkContext.line}. */ length: number; @@ -5833,7 +5860,7 @@ declare module 'vscode' { * * *Note* that this event should be used to propagate information about children. * - * @see [EventEmitter](#EventEmitter) + * @see {@link EventEmitter} */ onDidChangeFileDecorations?: Event; @@ -5842,7 +5869,7 @@ declare module 'vscode' { * * *Note* that this function is only called when a file gets rendered in the UI. * This means a decoration from a descendent that propagates upwards must be signaled - * to the editor via the [onDidChangeFileDecorations](#FileDecorationProvider.onDidChangeFileDecorations)-event. + * to the editor via the {@link FileDecorationProvider.onDidChangeFileDecorations onDidChangeFileDecorations}-event. * * @param uri The uri of the file to provide a decoration for. * @param token A cancellation token. @@ -5872,7 +5899,7 @@ declare module 'vscode' { /** * Represents an extension. * - * To get an instance of an `Extension` use [getExtension](#extensions.getExtension). + * To get an instance of an `Extension` use {@link extensions.getExtension getExtension}. */ export interface Extension { @@ -5888,7 +5915,7 @@ declare module 'vscode' { /** * The absolute file path of the directory containing this extension. Shorthand - * notation for [Extension.extensionUri.fsPath](#Extension.extensionUri) (independent of the uri scheme). + * notation for {@link Extension.extensionUri Extension.extensionUri.fsPath} (independent of the uri scheme). */ readonly extensionPath: string; @@ -5907,7 +5934,7 @@ declare module 'vscode' { * or if an extension runs where the remote extension host runs. The extension kind * is defined in the `package.json`-file of extensions but can also be refined * via the `remote.extensionKind`-setting. When no remote extension host exists, - * the value is [`ExtensionKind.UI`](#ExtensionKind.UI). + * the value is {@link ExtensionKind.UI `ExtensionKind.UI`}. */ extensionKind: ExtensionKind; @@ -5966,13 +5993,13 @@ declare module 'vscode' { /** * A memento object that stores state in the context - * of the currently opened [workspace](#workspace.workspaceFolders). + * of the currently opened {@link workspace.workspaceFolders workspace}. */ readonly workspaceState: Memento; /** * A memento object that stores state independent - * of the current opened [workspace](#workspace.workspaceFolders). + * of the current opened {@link workspace.workspaceFolders workspace}. */ readonly globalState: Memento & { /** @@ -5988,11 +6015,12 @@ declare module 'vscode' { * * @param keys The set of keys whose values are synced. */ - setKeysForSync(keys: string[]): void; + setKeysForSync(keys: readonly string[]): void; }; /** - * A storage utility for secrets. + * A storage utility for secrets. Secrets are persisted across reloads and are independent of the + * current opened {@link workspace.workspaceFolders workspace}. */ readonly secrets: SecretStorage; @@ -6003,7 +6031,7 @@ declare module 'vscode' { /** * The absolute file path of the directory containing the extension. Shorthand - * notation for [ExtensionContext.extensionUri.fsPath](#TextDocument.uri) (independent of the uri scheme). + * notation for {@link TextDocument.uri ExtensionContext.extensionUri.fsPath} (independent of the uri scheme). */ readonly extensionPath: string; @@ -6016,8 +6044,8 @@ declare module 'vscode' { /** * Get the absolute path of a resource contained in the extension. * - * *Note* that an absolute uri can be constructed via [`Uri.joinPath`](#Uri.joinPath) and - * [`extensionUri`](#ExtensionContext.extensionUri), e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` + * *Note* that an absolute uri can be constructed via {@link Uri.joinPath `Uri.joinPath`} and + * {@link ExtensionContext.extensionUri `extensionUri`}, e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` * * @param relativePath A relative path to a resource contained in the extension. * @return The absolute path of the resource. @@ -6030,10 +6058,10 @@ declare module 'vscode' { * up to the extension. However, the parent directory is guaranteed to be existent. * The value is `undefined` when no workspace nor folder has been opened. * - * Use [`workspaceState`](#ExtensionContext.workspaceState) or - * [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.workspaceState `workspaceState`} or + * {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * @see {@link FileSystem `workspace.fs`} for how to read and write files and folders from * an uri. */ readonly storageUri: Uri | undefined; @@ -6043,10 +6071,10 @@ declare module 'vscode' { * can store private state. The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * - * Use [`workspaceState`](#ExtensionContext.workspaceState) or - * [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.workspaceState `workspaceState`} or + * {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @deprecated Use [storageUri](#ExtensionContext.storageUri) instead. + * @deprecated Use {@link ExtensionContext.storageUri storageUri} instead. */ readonly storagePath: string | undefined; @@ -6055,9 +6083,9 @@ declare module 'vscode' { * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * - * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * @see {@link FileSystem `workspace.fs`} for how to read and write files and folders from * an uri. */ readonly globalStorageUri: Uri; @@ -6067,9 +6095,9 @@ declare module 'vscode' { * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * - * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * Use {@link ExtensionContext.globalState `globalState`} to store key value data. * - * @deprecated Use [globalStorageUri](#ExtensionContext.globalStorageUri) instead. + * @deprecated Use {@link ExtensionContext.globalStorageUri globalStorageUri} instead. */ readonly globalStoragePath: string; @@ -6078,7 +6106,7 @@ declare module 'vscode' { * The directory might not exist on disk and creation is up to the extension. However, * the parent directory is guaranteed to be existent. * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * @see {@link FileSystem `workspace.fs`} for how to read and write files and folders from * an uri. */ readonly logUri: Uri; @@ -6088,7 +6116,7 @@ declare module 'vscode' { * The directory might not exist on disk and creation is up to the extension. However, * the parent directory is guaranteed to be existent. * - * @deprecated Use [logUri](#ExtensionContext.logUri) instead. + * @deprecated Use {@link ExtensionContext.logUri logUri} instead. */ readonly logPath: string; @@ -6561,9 +6589,9 @@ declare module 'vscode' { /** * Constructs a CustomExecution task object. The callback will be executed when the task is run, at which point the * extension should return the Pseudoterminal it will "run in". The task should wait to do further execution until - * [Pseudoterminal.open](#Pseudoterminal.open) is called. Task cancellation should be handled using - * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire - * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). + * {@link Pseudoterminal.open} is called. Task cancellation should be handled using + * {@link Pseudoterminal.close}. When the task is complete fire + * {@link Pseudoterminal.onDidClose}. * @param callback The callback that will be called when the task is started by a user. Any ${} style variables that * were in the task definition will be resolved and passed into the callback as `resolvedDefinition`. */ @@ -6646,7 +6674,7 @@ declare module 'vscode' { /** * A human-readable string which is rendered less prominently on a separate line in places - * where the task's name is displayed. Supports rendering of [theme icons](#ThemeIcon) + * where the task's name is displayed. Supports rendering of {@link ThemeIcon theme icons} * via the `$()`-syntax. */ detail?: string; @@ -6663,7 +6691,7 @@ declare module 'vscode' { /** * A human-readable string describing the source of this shell task, e.g. 'gulp' - * or 'npm'. Supports rendering of [theme icons](#ThemeIcon) via the `$()`-syntax. + * or 'npm'. Supports rendering of {@link ThemeIcon theme icons} via the `$()`-syntax. */ source: string; @@ -6694,7 +6722,7 @@ declare module 'vscode' { /** * A task provider allows to add tasks to the task service. - * A task provider is registered via #tasks.registerTaskProvider. + * A task provider is registered via {@link tasks.registerTaskProvider}. */ export interface TaskProvider { /** @@ -6705,7 +6733,7 @@ declare module 'vscode' { provideTasks(token: CancellationToken): ProviderResult; /** - * Resolves a task that has no [`execution`](#Task.execution) set. Tasks are + * Resolves a task that has no {@link Task.execution `execution`} set. Tasks are * often created from information found in the `tasks.json`-file. Such tasks miss * the information on how to execute them and a task provider must fill in * the missing information in the `resolveTask`-method. This method will not be @@ -6823,7 +6851,7 @@ declare module 'vscode' { * * @param type The task kind type this provider is registered for. * @param provider A task provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; @@ -6851,7 +6879,7 @@ declare module 'vscode' { /** * The currently active task executions or an empty array. */ - export const taskExecutions: ReadonlyArray; + export const taskExecutions: readonly TaskExecution[]; /** * Fires when a task starts. @@ -6991,7 +7019,7 @@ declare module 'vscode' { /** * A code that identifies this error. * - * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), + * Possible values are names of errors, like {@link FileSystemError.FileNotFound `FileNotFound`}, * or `Unknown` for unspecified errors. */ readonly code: string; @@ -7039,22 +7067,22 @@ declare module 'vscode' { * and to manage files and folders. It allows extensions to serve files from remote places, * like ftp-servers, and to seamlessly integrate those into the editor. * - * * *Note 1:* The filesystem provider API works with [uris](#Uri) and assumes hierarchical + * * *Note 1:* The filesystem provider API works with {@link Uri uris} and assumes hierarchical * paths, e.g. `foo:/my/path` is a child of `foo:/my/` and a parent of `foo:/my/path/deeper`. * * *Note 2:* There is an activation event `onFileSystem:` that fires when a file * or folder is being accessed. - * * *Note 3:* The word 'file' is often used to denote all [kinds](#FileType) of files, e.g. + * * *Note 3:* The word 'file' is often used to denote all {@link FileType kinds} of files, e.g. * folders, symbolic links, and regular files. */ export interface FileSystemProvider { /** * An event to signal that a resource has been created, changed, or deleted. This - * event should fire for resources that are being [watched](#FileSystemProvider.watch) + * event should fire for resources that are being {@link FileSystemProvider.watch watched} * by clients of this provider. * * *Note:* It is important that the metadata of the file that changed provides an - * updated `mtime` that advanced from the previous value in the [stat](#FileStat) and a + * updated `mtime` that advanced from the previous value in the {@link FileStat stat} and a * correct `size` value. Otherwise there may be optimizations in place that will not show * the change in an editor for example. */ @@ -7077,21 +7105,21 @@ declare module 'vscode' { * Retrieve metadata about a file. * * Note that the metadata for symbolic links should be the metadata of the file they refer to. - * Still, the [SymbolicLink](#FileType.SymbolicLink)-type must be used in addition to the actual type, e.g. + * Still, the {@link FileType.SymbolicLink SymbolicLink}-type must be used in addition to the actual type, e.g. * `FileType.SymbolicLink | FileType.Directory`. * * @param uri The uri of the file to retrieve metadata about. * @return The file metadata about the file. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. */ stat(uri: Uri): FileStat | Thenable; /** - * Retrieve all entries of a [directory](#FileType.Directory). + * Retrieve all entries of a {@link FileType.Directory directory}. * * @param uri The uri of the folder. * @return An array of name/type-tuples or a thenable that resolves to such. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. */ readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]>; @@ -7099,9 +7127,9 @@ declare module 'vscode' { * Create a new directory (Note, that new files are created via `write`-calls). * * @param uri The uri of the new folder. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `uri` already exists. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ createDirectory(uri: Uri): void | Thenable; @@ -7110,7 +7138,7 @@ declare module 'vscode' { * * @param uri The uri of the file. * @return An array of bytes or a thenable that resolves to such. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. */ readFile(uri: Uri): Uint8Array | Thenable; @@ -7120,10 +7148,10 @@ declare module 'vscode' { * @param uri The uri of the file. * @param content The new content of the file. * @param options Defines if missing files should or must be created. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist and `create` is not set. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists, `create` is set but `overwrite` is not set. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist and `create` is not set. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `uri` already exists, `create` is set but `overwrite` is not set. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void | Thenable; @@ -7132,8 +7160,8 @@ declare module 'vscode' { * * @param uri The resource that is to be deleted. * @param options Defines if deletion of folders is recursive. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `uri` doesn't exist. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ delete(uri: Uri, options: { recursive: boolean }): void | Thenable; @@ -7143,10 +7171,10 @@ declare module 'vscode' { * @param oldUri The existing file. * @param newUri The new location. * @param options Defines if existing files should be overwritten. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `oldUri` doesn't exist. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `newUri` exists and when the `overwrite` option is not `true`. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `oldUri` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `newUri` exists and when the `overwrite` option is not `true`. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void | Thenable; @@ -7157,21 +7185,21 @@ declare module 'vscode' { * @param source The existing file. * @param destination The destination location. * @param options Defines if existing files should be overwritten. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `source` doesn't exist. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `destination` exists and when the `overwrite` option is not `true`. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when `source` doesn't exist. + * @throws {@link FileSystemError.FileNotFound `FileNotFound`} when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@link FileSystemError.FileExists `FileExists`} when `destination` exists and when the `overwrite` option is not `true`. + * @throws {@link FileSystemError.NoPermissions `NoPermissions`} when permissions aren't sufficient. */ copy?(source: Uri, destination: Uri, options: { overwrite: boolean }): void | Thenable; } /** * The file system interface exposes the editor's built-in and contributed - * [file system providers](#FileSystemProvider). It allows extensions to work + * {@link FileSystemProvider file system providers}. It allows extensions to work * with files from the local disk as well as files from remote places, like the * remote extension host or ftp-servers. * - * *Note* that an instance of this interface is available as [`workspace.fs`](#workspace.fs). + * *Note* that an instance of this interface is available as {@link workspace.fs `workspace.fs`}. */ export interface FileSystem { @@ -7184,7 +7212,7 @@ declare module 'vscode' { stat(uri: Uri): Thenable; /** - * Retrieve all entries of a [directory](#FileType.Directory). + * Retrieve all entries of a {@link FileType.Directory directory}. * * @param uri The uri of the folder. * @return An array of name/type-tuples or a thenable that resolves to such. @@ -7299,7 +7327,7 @@ declare module 'vscode' { * * Pass in an empty array to disallow access to any local resources. */ - readonly localResourceRoots?: ReadonlyArray; + readonly localResourceRoots?: readonly Uri[]; /** * Mappings of localhost ports used inside the webview. @@ -7314,7 +7342,7 @@ declare module 'vscode' { * *Note* that port mappings only work for `http` or `https` urls. Websocket urls (e.g. `ws://localhost:3000`) * cannot be mapped to another port. */ - readonly portMapping?: ReadonlyArray; + readonly portMapping?: readonly WebviewPortMapping[]; } /** @@ -7332,7 +7360,7 @@ declare module 'vscode' { * This should be a complete, valid html document. Changing this property causes the webview to be reloaded. * * Webviews are sandboxed from normal extension process, so all communication with the webview must use - * message passing. To send a message from the extension to the webview, use [`postMessage`](#Webview.postMessage). + * message passing. To send a message from the extension to the webview, use {@link Webview.postMessage `postMessage`}. * To send message from the webview back to an extension, use the `acquireVsCodeApi` function inside the webview * to get a handle to VS Code's api and then call `.postMessage()`: * @@ -7343,8 +7371,8 @@ declare module 'vscode' { * * ``` * - * To load a resources from the workspace inside a webview, use the `[asWebviewUri](#Webview.asWebviewUri)` method - * and ensure the resource's directory is listed in [`WebviewOptions.localResourceRoots`](#WebviewOptions.localResourceRoots). + * To load a resources from the workspace inside a webview, use the `{@link Webview.asWebviewUri asWebviewUri}` method + * and ensure the resource's directory is listed in {@link WebviewOptions.localResourceRoots `WebviewOptions.localResourceRoots`}. * * Keep in mind that even though webviews are sandboxed, they still allow running scripts and loading arbitrary content, * so extensions must follow all standard web security best practices when working with webviews. This includes @@ -7369,6 +7397,16 @@ declare module 'vscode' { * background with `retainContextWhenHidden`). * * @param message Body of the message. This must be a string or other json serializable object. + * + * For older versions of vscode, if an `ArrayBuffer` is included in `message`, + * it will not be serialized properly and will not be received by the webview. + * Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently + * serialized and will also not be recreated as a typed array inside the webview. + * + * However if your extension targets vscode 1.57+ in the `engines` field of its + * `package.json`, any `ArrayBuffer` values that appear in `message` will be more + * efficiently transferred to the webview and will also be correctly recreated inside + * of the webview. */ postMessage(message: any): Thenable; @@ -7447,7 +7485,7 @@ declare module 'vscode' { iconPath?: Uri | { light: Uri; dark: Uri }; /** - * [`Webview`](#Webview) belonging to the panel. + * {@link Webview `Webview`} belonging to the panel. */ readonly webview: Webview; @@ -7692,7 +7730,7 @@ declare module 'vscode' { /** * Provider for text based custom editors. * - * Text based custom editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies + * Text based custom editors use a {@link TextDocument `TextDocument`} as their data model. This considerably simplifies * implementing a custom editor as it allows VS Code to handle many common operations such as * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. */ @@ -7711,7 +7749,7 @@ declare module 'vscode' { * * During resolve, the provider must fill in the initial html for the content webview panel and hook up all * the event listeners on it that it is interested in. The provider can also hold onto the `WebviewPanel` to - * use later for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * use later for example in a command. See {@link WebviewPanel `WebviewPanel`} for additional details. * * @param token A cancellation token that indicates the result is no longer needed. * @@ -7721,7 +7759,7 @@ declare module 'vscode' { } /** - * Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider). + * Represents a custom document used by a {@link CustomEditorProvider `CustomEditorProvider`}. * * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is * managed by VS Code. When no more references remain to a `CustomDocument`, it is disposed of. @@ -7742,9 +7780,9 @@ declare module 'vscode' { } /** - * Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomDocument`](#CustomDocument). + * Event triggered by extensions to signal to VS Code that an edit has occurred on an {@link CustomDocument `CustomDocument`}. * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentEditEvent { @@ -7780,10 +7818,10 @@ declare module 'vscode' { } /** - * Event triggered by extensions to signal to VS Code that the content of a [`CustomDocument`](#CustomDocument) + * Event triggered by extensions to signal to VS Code that the content of a {@link CustomDocument `CustomDocument`} * has changed. * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentContentChangeEvent { /** @@ -7793,7 +7831,7 @@ declare module 'vscode' { } /** - * A backup for an [`CustomDocument`](#CustomDocument). + * A backup for an {@link CustomDocument `CustomDocument`}. */ interface CustomDocumentBackup { /** @@ -7813,7 +7851,7 @@ declare module 'vscode' { } /** - * Additional information used to implement [`CustomEditableDocument.backup`](#CustomEditableDocument.backup). + * Additional information used to implement {@link CustomEditableDocument.backup `CustomEditableDocument.backup`}. */ interface CustomDocumentBackupContext { /** @@ -7851,10 +7889,10 @@ declare module 'vscode' { /** * Provider for readonly custom editors that use a custom document model. * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * Custom editors use {@link CustomDocument `CustomDocument`} as their document model instead of a {@link TextDocument `TextDocument`}. * * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * text based documents, use {@link CustomTextEditorProvider `CustomTextEditorProvider`} instead. * * @param T Type of the custom document returned by this provider. */ @@ -7889,7 +7927,7 @@ declare module 'vscode' { * * During resolve, the provider must fill in the initial html for the content webview panel and hook up all * the event listeners on it that it is interested in. The provider can also hold onto the `WebviewPanel` to - * use later for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * use later for example in a command. See {@link WebviewPanel `WebviewPanel`} for additional details. * * @param token A cancellation token that indicates the result is no longer needed. * @@ -7901,11 +7939,11 @@ declare module 'vscode' { /** * Provider for editable custom editors that use a custom document model. * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * Custom editors use {@link CustomDocument `CustomDocument`} as their document model instead of a {@link TextDocument `TextDocument`}. * This gives extensions full control over actions such as edit, save, and backup. * * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * text based documents, use {@link CustomTextEditorProvider `CustomTextEditorProvider`} instead. * * @param T Type of the custom document returned by this provider. */ @@ -8097,7 +8135,7 @@ declare module 'vscode' { export const isTelemetryEnabled: boolean; /** - * An [event](#Event) which fires when the user enabled or disables telemetry. + * An {@link Event} which fires when the user enabled or disables telemetry. * `true` if the user has enabled telemetry or `false` if the user has disabled telemetry. */ export const onDidChangeTelemetryEnabled: Event; @@ -8108,7 +8146,7 @@ declare module 'vscode' { * * *Note* that the value is `undefined` when there is no remote extension host but that the * value is defined in all extension hosts (local and remote) in case a remote extension host - * exists. Use [`Extension#extensionKind`](#Extension.extensionKind) to know if + * exists. Use {@link Extension.extensionKind} to know if * a specific extension runs remote or not. */ export const remoteName: string | undefined; @@ -8134,7 +8172,7 @@ declare module 'vscode' { * * a mail client (`mailto:`) * * VSCode itself (`vscode:` from `vscode.env.uriScheme`) * - * *Note* that [`showTextDocument`](#window.showTextDocument) is the right + * *Note* that {@link window.showTextDocument `showTextDocument`} is the right * way to open a text document inside the editor, not this function. * * @param target The uri that should be opened. @@ -8161,7 +8199,7 @@ declare module 'vscode' { * * #### `vscode.env.uriScheme` * - * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered [UriHandler](#UriHandler) + * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered {@link UriHandler} * to trigger. * * Extensions should not make any assumptions about the resulting uri and should not alter it in anyway. @@ -8169,7 +8207,7 @@ declare module 'vscode' { * argument to the server to authenticate to. * * *Note* that if the server decides to add additional query parameters to the uri (e.g. a token or secret), it - * will appear in the uri that is passed to the [UriHandler](#UriHandler). + * will appear in the uri that is passed to the {@link UriHandler}. * * **Example** of an authentication flow: * ```typescript @@ -8198,9 +8236,9 @@ declare module 'vscode' { * Namespace for dealing with commands. In short, a command is a function with a * unique identifier. The function is sometimes also called _command handler_. * - * Commands can be added to the editor using the [registerCommand](#commands.registerCommand) - * and [registerTextEditorCommand](#commands.registerTextEditorCommand) functions. Commands - * can be executed [manually](#commands.executeCommand) or from a UI gesture. Those are: + * Commands can be added to the editor using the {@link commands.registerCommand registerCommand} + * and {@link commands.registerTextEditorCommand registerTextEditorCommand} functions. Commands + * can be executed {@link commands.executeCommand manually} or from a UI gesture. Those are: * * * palette - Use the `commands`-section in `package.json` to make a command show in * the [command palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette). @@ -8250,14 +8288,14 @@ declare module 'vscode' { * Registers a text editor command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. * - * Text editor commands are different from ordinary [commands](#commands.registerCommand) as + * Text editor commands are different from ordinary {@link commands.registerCommand commands} as * they only execute when there is an active editor when the command is called. Also, the * command handler of an editor command has access to the active editor and to an - * [edit](#TextEditorEdit)-builder. Note that the edit-builder is only valid while the + * {@link TextEditorEdit edit}-builder. Note that the edit-builder is only valid while the * callback executes. * * @param command A unique identifier for the command. - * @param callback A command handler function with access to an [editor](#TextEditor) and an [edit](#TextEditorEdit). + * @param callback A command handler function with access to an {@link TextEditor editor} and an {@link TextEditorEdit edit}. * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ @@ -8268,7 +8306,7 @@ declare module 'vscode' { * * * *Note 1:* When executing an editor command not all types are allowed to * be passed as arguments. Allowed are the primitive types `string`, `boolean`, - * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`Uri`](#Uri) and [`Location`](#Location). + * `number`, `undefined`, and `null`, as well as {@link Position `Position`}, {@link Range `Range`}, {@link Uri `Uri`} and {@link Location `Location`}. * * *Note 2:* There are no restrictions when executing commands that have been contributed * by extensions. * @@ -8301,16 +8339,16 @@ declare module 'vscode' { } /** - * A uri handler is responsible for handling system-wide [uris](#Uri). + * A uri handler is responsible for handling system-wide {@link Uri uris}. * - * @see [window.registerUriHandler](#window.registerUriHandler). + * @see {@link window.registerUriHandler}. */ export interface UriHandler { /** - * Handle the provided system-wide [uri](#Uri). + * Handle the provided system-wide {@link Uri}. * - * @see [window.registerUriHandler](#window.registerUriHandler). + * @see {@link window.registerUriHandler}. */ handleUri(uri: Uri): ProviderResult; } @@ -8335,42 +8373,42 @@ declare module 'vscode' { export let visibleTextEditors: TextEditor[]; /** - * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) + * An {@link Event} which fires when the {@link window.activeTextEditor active editor} * has changed. *Note* that the event also fires when the active editor changes * to `undefined`. */ export const onDidChangeActiveTextEditor: Event; /** - * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) + * An {@link Event} which fires when the array of {@link window.visibleTextEditors visible editors} * has changed. */ export const onDidChangeVisibleTextEditors: Event; /** - * An [event](#Event) which fires when the selection in an editor has changed. + * An {@link Event} which fires when the selection in an editor has changed. */ export const onDidChangeTextEditorSelection: Event; /** - * An [event](#Event) which fires when the visible ranges of an editor has changed. + * An {@link Event} which fires when the visible ranges of an editor has changed. */ export const onDidChangeTextEditorVisibleRanges: Event; /** - * An [event](#Event) which fires when the options of an editor have changed. + * An {@link Event} which fires when the options of an editor have changed. */ export const onDidChangeTextEditorOptions: Event; /** - * An [event](#Event) which fires when the view column of an editor has changed. + * An {@link Event} which fires when the view column of an editor has changed. */ export const onDidChangeTextEditorViewColumn: Event; /** * The currently opened terminals or an empty array. */ - export const terminals: ReadonlyArray; + export const terminals: readonly Terminal[]; /** * The currently active terminal or `undefined`. The active terminal is the one that @@ -8379,20 +8417,20 @@ declare module 'vscode' { export const activeTerminal: Terminal | undefined; /** - * An [event](#Event) which fires when the [active terminal](#window.activeTerminal) + * An {@link Event} which fires when the {@link window.activeTerminal active terminal} * has changed. *Note* that the event also fires when the active terminal changes * to `undefined`. */ export const onDidChangeActiveTerminal: Event; /** - * An [event](#Event) which fires when a terminal has been created, either through the - * [createTerminal](#window.createTerminal) API or commands. + * An {@link Event} which fires when a terminal has been created, either through the + * {@link window.createTerminal createTerminal} API or commands. */ export const onDidOpenTerminal: Event; /** - * An [event](#Event) which fires when a terminal is disposed. + * An {@link Event} which fires when a terminal is disposed. */ export const onDidCloseTerminal: Event; @@ -8402,42 +8440,42 @@ declare module 'vscode' { export const state: WindowState; /** - * An [event](#Event) which fires when the focus state of the current window + * An {@link Event} which fires when the focus state of the current window * changes. The value of the event represents whether the window is focused. */ export const onDidChangeWindowState: Event; /** - * Show the given document in a text editor. A [column](#ViewColumn) can be provided - * to control where the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * Show the given document in a text editor. A {@link ViewColumn column} can be provided + * to control where the editor is being shown. Might change the {@link window.activeTextEditor active editor}. * * @param document A text document to be shown. - * @param column A view column in which the [editor](#TextEditor) should be shown. The default is the [active](#ViewColumn.Active), other values - * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) + * @param column A view column in which the {@link TextEditor editor} should be shown. The default is the {@link ViewColumn.Active active}, other values + * are adjusted to be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is not adjusted. Use {@link ViewColumn.Beside `ViewColumn.Beside`} * to open the editor to the side of the currently active one. * @param preserveFocus When `true` the editor will not take focus. - * @return A promise that resolves to an [editor](#TextEditor). + * @return A promise that resolves to an {@link TextEditor editor}. */ export function showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; /** - * Show the given document in a text editor. [Options](#TextDocumentShowOptions) can be provided - * to control options of the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * Show the given document in a text editor. {@link TextDocumentShowOptions Options} can be provided + * to control options of the editor is being shown. Might change the {@link window.activeTextEditor active editor}. * * @param document A text document to be shown. - * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). - * @return A promise that resolves to an [editor](#TextEditor). + * @param options {@link TextDocumentShowOptions Editor options} to configure the behavior of showing the {@link TextEditor editor}. + * @return A promise that resolves to an {@link TextEditor editor}. */ export function showTextDocument(document: TextDocument, options?: TextDocumentShowOptions): Thenable; /** * A short-hand for `openTextDocument(uri).then(document => showTextDocument(document, options))`. * - * @see [openTextDocument](#openTextDocument) + * @see {@link openTextDocument} * * @param uri A resource identifier. - * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). - * @return A promise that resolves to an [editor](#TextEditor). + * @param options {@link TextDocumentShowOptions Editor options} to configure the behavior of showing the {@link TextEditor editor}. + * @return A promise that resolves to an {@link TextEditor editor}. */ export function showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; @@ -8473,7 +8511,7 @@ declare module 'vscode' { /** * Show an information message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8484,7 +8522,7 @@ declare module 'vscode' { /** * Show an information message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8496,7 +8534,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8507,7 +8545,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8519,7 +8557,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8530,7 +8568,7 @@ declare module 'vscode' { /** * Show a warning message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8542,7 +8580,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8553,7 +8591,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8565,7 +8603,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. @@ -8576,7 +8614,7 @@ declare module 'vscode' { /** * Show an error message. * - * @see [showInformationMessage](#window.showInformationMessage) + * @see {@link window.showInformationMessage showInformationMessage} * * @param message The message to show. * @param options Configures the behaviour of the message. @@ -8593,7 +8631,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ - export function showQuickPick(items: string[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly string[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -8603,7 +8641,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selection or `undefined`. */ - export function showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** * Shows a selection list allowing multiple selections. @@ -8613,7 +8651,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ - export function showQuickPick(items: T[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly T[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -8623,10 +8661,10 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected item or `undefined`. */ - export function showQuickPick(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** - * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. + * Shows a selection list of {@link workspace.workspaceFolders workspace folders} to pick from. * Returns `undefined` if no folder is open. * * @param options Configures the behavior of the workspace folder list. @@ -8666,30 +8704,30 @@ declare module 'vscode' { export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; /** - * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list + * Creates a {@link QuickPick} to let the user pick an item from a list * of items of type T. * - * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) - * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used - * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showQuickPick} + * is easier to use. {@link window.createQuickPick} should be used + * when {@link window.showQuickPick} does not offer the required flexibility. * - * @return A new [QuickPick](#QuickPick). + * @return A new {@link QuickPick}. */ export function createQuickPick(): QuickPick; /** - * Creates a [InputBox](#InputBox) to let the user enter some text input. + * Creates a {@link InputBox} to let the user enter some text input. * - * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) - * is easier to use. [window.createInputBox](#window.createInputBox) should be used - * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showInputBox} + * is easier to use. {@link window.createInputBox} should be used + * when {@link window.showInputBox} does not offer the required flexibility. * - * @return A new [InputBox](#InputBox). + * @return A new {@link InputBox}. */ export function createInputBox(): InputBox; /** - * Creates a new [output channel](#OutputChannel) with the given name. + * Creates a new {@link OutputChannel output channel} with the given name. * * @param name Human-readable string which will be used to represent the channel in the UI. */ @@ -8709,9 +8747,9 @@ declare module 'vscode' { /** * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). + * status bar {@link window.createStatusBarItem items}. * - * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param text The message to show, supports icon substitution as in status bar {@link StatusBarItem.text items}. * @param hideAfterTimeout Timeout in milliseconds after which the message will be disposed. * @return A disposable which hides the status bar message. */ @@ -8719,9 +8757,9 @@ declare module 'vscode' { /** * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). + * status bar {@link window.createStatusBarItem items}. * - * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param text The message to show, supports icon substitution as in status bar {@link StatusBarItem.text items}. * @param hideWhenDone Thenable on which completion (resolve or reject) the message will be disposed. * @return A disposable which hides the status bar message. */ @@ -8729,12 +8767,12 @@ declare module 'vscode' { /** * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). + * status bar {@link window.createStatusBarItem items}. * * *Note* that status bar messages stack and that they must be disposed when no * longer used. * - * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param text The message to show, supports icon substitution as in status bar {@link StatusBarItem.text items}. * @return A disposable which hides the status bar message. */ export function setStatusBarMessage(text: string): Disposable; @@ -8746,7 +8784,7 @@ declare module 'vscode' { * @deprecated Use `withProgress` instead. * * @param task A callback returning a promise. Progress increments can be reported with - * the provided [progress](#Progress)-object. + * the provided {@link Progress}-object. * @return The thenable the task did return. */ export function withScmProgress(task: (progress: Progress) => Thenable): Thenable; @@ -8754,17 +8792,17 @@ declare module 'vscode' { /** * Show progress in the editor. Progress is shown while running the given callback * and while the promise it returned isn't resolved nor rejected. The location at which - * progress should show (and other details) is defined via the passed [`ProgressOptions`](#ProgressOptions). + * progress should show (and other details) is defined via the passed {@link ProgressOptions `ProgressOptions`}. * * @param task A callback returning a promise. Progress state can be reported with - * the provided [progress](#Progress)-object. + * the provided {@link Progress}-object. * * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of * e.g. `10` accounts for `10%` of work done). * Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress. * - * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). + * To monitor if the operation has been cancelled by the user, use the provided {@link CancellationToken `CancellationToken`}. * Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the * long running operation. * @@ -8773,7 +8811,7 @@ declare module 'vscode' { export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable; /** - * Creates a status bar [item](#StatusBarItem). + * Creates a status bar {@link StatusBarItem item}. * * @param alignment The alignment of the item. * @param priority The priority of the item. Higher values mean the item should be shown more to the left. @@ -8782,7 +8820,17 @@ declare module 'vscode' { export function createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; /** - * Creates a [Terminal](#Terminal) with a backing shell process. The cwd of the terminal will be the workspace + * Creates a status bar {@link StatusBarItem item}. + * + * @param id The unique identifier of the item. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createStatusBarItem(id: string, alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + + /** + * Creates a {@link Terminal} with a backing shell process. The cwd of the terminal will be the workspace * directory if it exists. * * @param name Optional human-readable string which will be used to represent the terminal in the UI. @@ -8796,7 +8844,7 @@ declare module 'vscode' { export function createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): Terminal; /** - * Creates a [Terminal](#Terminal) with a backing shell process. + * Creates a {@link Terminal} with a backing shell process. * * @param options A TerminalOptions object describing the characteristics of the new terminal. * @return A new Terminal. @@ -8805,35 +8853,35 @@ declare module 'vscode' { export function createTerminal(options: TerminalOptions): Terminal; /** - * Creates a [Terminal](#Terminal) where an extension controls its input and output. + * Creates a {@link Terminal} where an extension controls its input and output. * - * @param options An [ExtensionTerminalOptions](#ExtensionTerminalOptions) object describing + * @param options An {@link ExtensionTerminalOptions} object describing * the characteristics of the new terminal. * @return A new Terminal. */ export function createTerminal(options: ExtensionTerminalOptions): Terminal; /** - * Register a [TreeDataProvider](#TreeDataProvider) for the view contributed using the extension point `views`. - * This will allow you to contribute data to the [TreeView](#TreeView) and update if the data changes. + * Register a {@link TreeDataProvider} for the view contributed using the extension point `views`. + * This will allow you to contribute data to the {@link TreeView} and update if the data changes. * - * **Note:** To get access to the [TreeView](#TreeView) and perform operations on it, use [createTreeView](#window.createTreeView). + * **Note:** To get access to the {@link TreeView} and perform operations on it, use {@link window.createTreeView createTreeView}. * * @param viewId Id of the view contributed using the extension point `views`. - * @param treeDataProvider A [TreeDataProvider](#TreeDataProvider) that provides tree data for the view + * @param treeDataProvider A {@link TreeDataProvider} that provides tree data for the view */ export function registerTreeDataProvider(viewId: string, treeDataProvider: TreeDataProvider): Disposable; /** - * Create a [TreeView](#TreeView) for the view contributed using the extension point `views`. + * Create a {@link TreeView} for the view contributed using the extension point `views`. * @param viewId Id of the view contributed using the extension point `views`. - * @param options Options for creating the [TreeView](#TreeView) - * @returns a [TreeView](#TreeView). + * @param options Options for creating the {@link TreeView} + * @returns a {@link TreeView}. */ export function createTreeView(viewId: string, options: TreeViewOptions): TreeView; /** - * Registers a [uri handler](#UriHandler) capable of handling system-wide [uris](#Uri). + * Registers a {@link UriHandler uri handler} capable of handling system-wide {@link Uri uris}. * In case there are multiple windows open, the topmost window will handle the uri. * A uri handler is scoped to the extension it is contributed from; it will only * be able to handle uris which are directed to the extension itself. A uri must respect @@ -8859,7 +8907,7 @@ declare module 'vscode' { * Registers a webview panel serializer. * * Extensions that support reviving should have an `"onWebviewPanel:viewType"` activation event and - * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. + * make sure that {@link registerWebviewPanelSerializer} is called during activation. * * Only a single serializer may be registered at a time for a given `viewType`. * @@ -8888,7 +8936,7 @@ declare module 'vscode' { * * Normally the webview's html context is created when the view becomes visible * and destroyed when it is hidden. Extensions that have complex state - * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview * context around, even when the webview moves to a background tab. When a webview using * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. * When the view becomes visible again, the context is automatically restored @@ -8905,9 +8953,9 @@ declare module 'vscode' { /** * Register a provider for custom editors for the `viewType` contributed by the `customEditors` extension point. * - * When a custom editor is opened, VS Code fires an `onCustomEditor:viewType` activation event. Your extension - * must register a [`CustomTextEditorProvider`](#CustomTextEditorProvider), [`CustomReadonlyEditorProvider`](#CustomReadonlyEditorProvider), - * [`CustomEditorProvider`](#CustomEditorProvider)for `viewType` as part of activation. + * When a custom editor is opened, an `onCustomEditor:viewType` activation event is fired. Your extension + * must register a {@link CustomTextEditorProvider `CustomTextEditorProvider`}, {@link CustomReadonlyEditorProvider `CustomReadonlyEditorProvider`}, + * {@link CustomEditorProvider `CustomEditorProvider`}for `viewType` as part of activation. * * @param viewType Unique identifier for the custom editor provider. This should match the `viewType` from the * `customEditors` contribution point. @@ -8928,7 +8976,7 @@ declare module 'vscode' { * Indicates that the provider allows multiple editor instances to be open at the same time for * the same resource. * - * By default, VS Code only allows one editor instance to be open at a time for each resource. If the + * By default, the editor only allows one editor instance to be open at a time for each resource. If the * user tries to open a second editor instance for the resource, the first one is instead moved to where * the second one was to be opened. * @@ -8949,8 +8997,8 @@ declare module 'vscode' { /** * Register a file decoration provider. * - * @param provider A [FileDecorationProvider](#FileDecorationProvider). - * @return A [disposable](#Disposable) that unregisters the provider. + * @param provider A {@link FileDecorationProvider}. + * @return A {@link Disposable} that unregisters the provider. */ export function registerFileDecorationProvider(provider: FileDecorationProvider): Disposable; @@ -8961,13 +9009,13 @@ declare module 'vscode' { export let activeColorTheme: ColorTheme; /** - * An [event](#Event) which fires when the active color theme is changed or has changes. + * An {@link Event} which fires when the active color theme is changed or has changes. */ export const onDidChangeActiveColorTheme: Event; } /** - * Options for creating a [TreeView](#TreeView) + * Options for creating a {@link TreeView} */ export interface TreeViewOptions { @@ -8990,7 +9038,7 @@ declare module 'vscode' { } /** - * The event that is fired when an element in the [TreeView](#TreeView) is expanded or collapsed + * The event that is fired when an element in the {@link TreeView} is expanded or collapsed */ export interface TreeViewExpansionEvent { @@ -9002,7 +9050,7 @@ declare module 'vscode' { } /** - * The event that is fired when there is a change in [tree view's selection](#TreeView.selection) + * The event that is fired when there is a change in {@link TreeView.selection tree view's selection} */ export interface TreeViewSelectionChangeEvent { @@ -9014,12 +9062,12 @@ declare module 'vscode' { } /** - * The event that is fired when there is a change in [tree view's visibility](#TreeView.visible) + * The event that is fired when there is a change in {@link TreeView.visible tree view's visibility} */ export interface TreeViewVisibilityChangeEvent { /** - * `true` if the [tree view](#TreeView) is visible otherwise `false`. + * `true` if the {@link TreeView tree view} is visible otherwise `false`. */ readonly visible: boolean; @@ -9046,17 +9094,17 @@ declare module 'vscode' { readonly selection: T[]; /** - * Event that is fired when the [selection](#TreeView.selection) has changed + * Event that is fired when the {@link TreeView.selection selection} has changed */ readonly onDidChangeSelection: Event>; /** - * `true` if the [tree view](#TreeView) is visible otherwise `false`. + * `true` if the {@link TreeView tree view} is visible otherwise `false`. */ readonly visible: boolean; /** - * Event that is fired when [visibility](#TreeView.visible) has changed + * Event that is fired when {@link TreeView.visible visibility} has changed */ readonly onDidChangeVisibility: Event; @@ -9088,7 +9136,7 @@ declare module 'vscode' { * In order to expand the revealed element, set the option `expand` to `true`. To expand recursively set `expand` to the number of levels to expand. * **NOTE:** You can expand only to 3 levels maximum. * - * **NOTE:** The [TreeDataProvider](#TreeDataProvider) that the `TreeView` [is registered with](#window.createTreeView) with must implement [getParent](#TreeDataProvider.getParent) method to access this API. + * **NOTE:** The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ reveal(element: T, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; } @@ -9105,10 +9153,10 @@ declare module 'vscode' { onDidChangeTreeData?: Event; /** - * Get [TreeItem](#TreeItem) representation of the `element` + * Get {@link TreeItem} representation of the `element` * - * @param element The element for which [TreeItem](#TreeItem) representation is asked for. - * @return [TreeItem](#TreeItem) representation of the element + * @param element The element for which {@link TreeItem} representation is asked for. + * @return {@link TreeItem} representation of the element */ getTreeItem(element: T): TreeItem | Thenable; @@ -9124,7 +9172,7 @@ declare module 'vscode' { * Optional method to return the parent of `element`. * Return `null` or `undefined` if `element` is a child of root. * - * **NOTE:** This method should be implemented in order to access [reveal](#TreeView.reveal) API. + * **NOTE:** This method should be implemented in order to access {@link TreeView.reveal reveal} API. * * @param element The element for which the parent has to be returned. * @return Parent of `element`. @@ -9132,8 +9180,8 @@ declare module 'vscode' { getParent?(element: T): ProviderResult; /** - * Called on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. - * Called on tree item click/open to resolve the [TreeItem](#TreeItem.command) property if it is undefined. + * Called on hover to resolve the {@link TreeItem.tooltip TreeItem} property if it is undefined. + * Called on tree item click/open to resolve the {@link TreeItem.command TreeItem} property if it is undefined. * Only properties that were undefined can be resolved in `resolveTreeItem`. * Functionality may be expanded later to include being called to resolve other missing * properties on selection and/or on open. @@ -9157,7 +9205,7 @@ declare module 'vscode' { export class TreeItem { /** - * A human-readable string describing this item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri). + * A human-readable string describing this item. When `falsy`, it is derived from {@link TreeItem.resourceUri resourceUri}. */ label?: string | TreeItemLabel; @@ -9169,23 +9217,23 @@ declare module 'vscode' { id?: string; /** - * The icon path or [ThemeIcon](#ThemeIcon) for the tree item. - * When `falsy`, [Folder Theme Icon](#ThemeIcon.Folder) is assigned, if item is collapsible otherwise [File Theme Icon](#ThemeIcon.File). - * When a file or folder [ThemeIcon](#ThemeIcon) is specified, icon is derived from the current file icon theme for the specified theme icon using [resourceUri](#TreeItem.resourceUri) (if provided). + * The icon path or {@link ThemeIcon} for the tree item. + * When `falsy`, {@link ThemeIcon.Folder Folder Theme Icon} is assigned, if item is collapsible otherwise {@link ThemeIcon.File File Theme Icon}. + * When a file or folder {@link ThemeIcon} is specified, icon is derived from the current file icon theme for the specified theme icon using {@link TreeItem.resourceUri resourceUri} (if provided). */ iconPath?: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; /** * A human-readable string which is rendered less prominent. - * When `true`, it is derived from [resourceUri](#TreeItem.resourceUri) and when `falsy`, it is not shown. + * When `true`, it is derived from {@link TreeItem.resourceUri resourceUri} and when `falsy`, it is not shown. */ description?: string | boolean; /** - * The [uri](#Uri) of the resource representing this item. + * The {@link Uri} of the resource representing this item. * - * Will be used to derive the [label](#TreeItem.label), when it is not provided. - * Will be used to derive the icon from current file icon theme, when [iconPath](#TreeItem.iconPath) has [ThemeIcon](#ThemeIcon) value. + * Will be used to derive the {@link TreeItem.label label}, when it is not provided. + * Will be used to derive the icon from current file icon theme, when {@link TreeItem.iconPath iconPath} has {@link ThemeIcon} value. */ resourceUri?: Uri; @@ -9195,7 +9243,7 @@ declare module 'vscode' { tooltip?: string | MarkdownString | undefined; /** - * The [command](#Command) that should be executed when the tree item is selected. + * The {@link Command} that should be executed when the tree item is selected. * * Please use `vscode.open` or `vscode.diff` as command IDs when the tree item is opening * something in the editor. Using these commands ensures that the resulting editor will @@ -9204,7 +9252,7 @@ declare module 'vscode' { command?: Command; /** - * [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. + * {@link TreeItemCollapsibleState} of the tree item. */ collapsibleState?: TreeItemCollapsibleState; @@ -9237,13 +9285,13 @@ declare module 'vscode' { /** * @param label A human-readable string describing this item - * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) + * @param collapsibleState {@link TreeItemCollapsibleState} of the tree item. Default is {@link TreeItemCollapsibleState.None} */ constructor(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState); /** - * @param resourceUri The [uri](#Uri) of the resource representing this item. - * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) + * @param resourceUri The {@link Uri} of the resource representing this item. + * @param collapsibleState {@link TreeItemCollapsibleState} of the tree item. Default is {@link TreeItemCollapsibleState.None} */ constructor(resourceUri: Uri, collapsibleState?: TreeItemCollapsibleState); } @@ -9267,12 +9315,12 @@ declare module 'vscode' { } /** - * Label describing the [Tree item](#TreeItem) + * Label describing the {@link TreeItem Tree item} */ export interface TreeItemLabel { /** - * A human-readable string describing the [Tree item](#TreeItem). + * A human-readable string describing the {@link TreeItem Tree item}. */ label: string; @@ -9330,6 +9378,13 @@ declare module 'vscode' { * as normal. */ hideFromUser?: boolean; + + /** + * A message to write to the terminal on first launch, note that this is not sent to the + * process but, rather written directly to the terminal. This supports escape sequences such + * a setting text style. + */ + message?: string; } /** @@ -9342,7 +9397,7 @@ declare module 'vscode' { name: string; /** - * An implementation of [Pseudoterminal](#Pseudoterminal) that allows an extension to + * An implementation of {@link Pseudoterminal} that allows an extension to * control a terminal. */ pty: Pseudoterminal; @@ -9354,7 +9409,7 @@ declare module 'vscode' { interface Pseudoterminal { /** * An event that when fired will write data to the terminal. Unlike - * [Terminal.sendText](#Terminal.sendText) which sends text to the underlying child + * {@link Terminal.sendText} which sends text to the underlying child * pseudo-device (the child), this will write the text to parent pseudo-device (the * _terminal_ itself). * @@ -9380,7 +9435,7 @@ declare module 'vscode' { onDidWrite: Event; /** - * An event that when fired allows overriding the [dimensions](#Pseudoterminal.setDimensions) of the + * An event that when fired allows overriding the {@link Pseudoterminal.setDimensions dimensions} of the * terminal. Note that when set, the overridden dimensions will only take effect when they * are lower than the actual dimensions of the terminal (ie. there will never be a scroll * bar). Set to `undefined` for the terminal to go back to the regular dimensions (fit to @@ -9449,7 +9504,7 @@ declare module 'vscode' { /** * Implement to handle incoming keystrokes in the terminal or when an extension calls - * [Terminal.sendText](#Terminal.sendText). `data` contains the keystrokes/text serialized into + * {@link Terminal.sendText}. `data` contains the keystrokes/text serialized into * their corresponding VT sequence representation. * * @param data The incoming data. @@ -9476,7 +9531,7 @@ declare module 'vscode' { * as the size of a terminal isn't known until it shows up in the user interface. * * When dimensions are overridden by - * [onDidOverrideDimensions](#Pseudoterminal.onDidOverrideDimensions), `setDimensions` will + * {@link Pseudoterminal.onDidOverrideDimensions onDidOverrideDimensions}, `setDimensions` will * continue to be called with the regular panel dimensions, allowing the extension continue * to react dimension changes. * @@ -9672,23 +9727,23 @@ declare module 'vscode' { /** * A light-weight user input UI that is initially not visible. After * configuring it through its properties the extension can make it - * visible by calling [QuickInput.show](#QuickInput.show). + * visible by calling {@link QuickInput.show}. * * There are several reasons why this UI might have to be hidden and - * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), + * the extension will be notified through {@link QuickInput.onDidHide}. + * (Examples include: an explicit call to {@link QuickInput.hide}, * the user pressing Esc, some other input UI opening, etc.) * * A user pressing Enter or some other gesture implying acceptance * of the current state does not automatically hide this UI component. * It is up to the extension to decide whether to accept the user's input - * and if the UI should indeed be hidden through a call to [QuickInput.hide](#QuickInput.hide). + * and if the UI should indeed be hidden through a call to {@link QuickInput.hide}. * * When the extension no longer needs this input UI, it should - * [QuickInput.dispose](#QuickInput.dispose) it to allow for freeing up + * {@link QuickInput.dispose} it to allow for freeing up * any resources associated with it. * - * See [QuickPick](#QuickPick) and [InputBox](#InputBox) for concrete UIs. + * See {@link QuickPick} and {@link InputBox} for concrete UIs. */ export interface QuickInput { @@ -9730,12 +9785,12 @@ declare module 'vscode' { /** * Makes the input UI visible in its current configuration. Any other input - * UI will first fire an [QuickInput.onDidHide](#QuickInput.onDidHide) event. + * UI will first fire an {@link QuickInput.onDidHide} event. */ show(): void; /** - * Hides this input UI. This will also fire an [QuickInput.onDidHide](#QuickInput.onDidHide) + * Hides this input UI. This will also fire an {@link QuickInput.onDidHide} * event. */ hide(): void; @@ -9744,8 +9799,8 @@ declare module 'vscode' { * An event signaling when this input UI is hidden. * * There are several reasons why this UI might have to be hidden and - * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), + * the extension will be notified through {@link QuickInput.onDidHide}. + * (Examples include: an explicit call to {@link QuickInput.hide}, * the user pressing Esc, some other input UI opening, etc.) */ onDidHide: Event; @@ -9760,14 +9815,14 @@ declare module 'vscode' { } /** - * A concrete [QuickInput](#QuickInput) to let the user pick an item from a + * A concrete {@link QuickInput} to let the user pick an item from a * list of items of type T. The items can be filtered through a filter text field and - * there is an option [canSelectMany](#QuickPick.canSelectMany) to allow for + * there is an option {@link QuickPick.canSelectMany canSelectMany} to allow for * selecting multiple items. * - * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) - * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used - * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showQuickPick} + * is easier to use. {@link window.createQuickPick} should be used + * when {@link window.showQuickPick} does not offer the required flexibility. */ export interface QuickPick extends QuickInput { @@ -9794,7 +9849,7 @@ declare module 'vscode' { /** * Buttons for actions in the UI. */ - buttons: ReadonlyArray; + buttons: readonly QuickInputButton[]; /** * An event signaling when a button was triggered. @@ -9802,9 +9857,9 @@ declare module 'vscode' { readonly onDidTriggerButton: Event; /** - * Items to pick from. + * Items to pick from. This can be read and updated by the extension. */ - items: ReadonlyArray; + items: readonly T[]; /** * If multiple items can be selected at the same time. Defaults to false. @@ -9824,7 +9879,7 @@ declare module 'vscode' { /** * Active items. This can be read and updated by the extension. */ - activeItems: ReadonlyArray; + activeItems: readonly T[]; /** * An event signaling when the active items have changed. @@ -9834,7 +9889,7 @@ declare module 'vscode' { /** * Selected items. This can be read and updated by the extension. */ - selectedItems: ReadonlyArray; + selectedItems: readonly T[]; /** * An event signaling when the selected items have changed. @@ -9843,11 +9898,11 @@ declare module 'vscode' { } /** - * A concrete [QuickInput](#QuickInput) to let the user input a text value. + * A concrete {@link QuickInput} to let the user input a text value. * - * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) - * is easier to use. [window.createInputBox](#window.createInputBox) should be used - * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * Note that in many cases the more convenient {@link window.showInputBox} + * is easier to use. {@link window.createInputBox} should be used + * when {@link window.showInputBox} does not offer the required flexibility. */ export interface InputBox extends QuickInput { @@ -9879,7 +9934,7 @@ declare module 'vscode' { /** * Buttons for actions in the UI. */ - buttons: ReadonlyArray; + buttons: readonly QuickInputButton[]; /** * An event signaling when a button was triggered. @@ -9898,7 +9953,7 @@ declare module 'vscode' { } /** - * Button for an action in a [QuickPick](#QuickPick) or [InputBox](#InputBox). + * Button for an action in a {@link QuickPick} or {@link InputBox}. */ export interface QuickInputButton { @@ -9914,12 +9969,12 @@ declare module 'vscode' { } /** - * Predefined buttons for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * Predefined buttons for {@link QuickPick} and {@link InputBox}. */ export class QuickInputButtons { /** - * A back button for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * A back button for {@link QuickPick} and {@link InputBox}. * * When a navigation 'back' button is needed this one should be used for consistency. * It comes with a predefined icon, tooltip and location. @@ -9933,7 +9988,7 @@ declare module 'vscode' { } /** - * An event describing an individual change in the text of a [document](#TextDocument). + * An event describing an individual change in the text of a {@link TextDocument document}. */ export interface TextDocumentContentChangeEvent { /** @@ -9955,7 +10010,7 @@ declare module 'vscode' { } /** - * An event describing a transactional [document](#TextDocument) change. + * An event describing a transactional {@link TextDocument document} change. */ export interface TextDocumentChangeEvent { @@ -9967,7 +10022,7 @@ declare module 'vscode' { /** * An array of content changes. */ - readonly contentChanges: ReadonlyArray; + readonly contentChanges: readonly TextDocumentContentChangeEvent[]; } /** @@ -9993,11 +10048,11 @@ declare module 'vscode' { } /** - * An event that is fired when a [document](#TextDocument) will be saved. + * An event that is fired when a {@link TextDocument document} will be saved. * * To make modifications to the document before it is being saved, call the - * [`waitUntil`](#TextDocumentWillSaveEvent.waitUntil)-function with a thenable - * that resolves to an array of [text edits](#TextEdit). + * {@link TextDocumentWillSaveEvent.waitUntil `waitUntil`}-function with a thenable + * that resolves to an array of {@link TextEdit text edits}. */ export interface TextDocumentWillSaveEvent { @@ -10012,7 +10067,7 @@ declare module 'vscode' { readonly reason: TextDocumentSaveReason; /** - * Allows to pause the event loop and to apply [pre-save-edits](#TextEdit). + * Allows to pause the event loop and to apply {@link TextEdit pre-save-edits}. * Edits of subsequent calls to this function will be applied in order. The * edits will be *ignored* if concurrent modifications of the document happened. * @@ -10029,7 +10084,7 @@ declare module 'vscode' { * }) * ``` * - * @param thenable A thenable that resolves to [pre-save-edits](#TextEdit). + * @param thenable A thenable that resolves to {@link TextEdit pre-save-edits}. */ waitUntil(thenable: Thenable): void; @@ -10047,18 +10102,18 @@ declare module 'vscode' { * An event that is fired when files are going to be created. * * To make modifications to the workspace before the files are created, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). + * call the {@link FileWillCreateEvent.waitUntil `waitUntil`}-function with a + * thenable that resolves to a {@link WorkspaceEdit workspace edit}. */ export interface FileWillCreateEvent { /** * The files that are going to be created. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: @@ -10095,25 +10150,25 @@ declare module 'vscode' { /** * The files that got created. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; } /** * An event that is fired when files are going to be deleted. * * To make modifications to the workspace before the files are deleted, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). + * call the {@link FileWillCreateEvent.waitUntil `waitUntil}-function with a + * thenable that resolves to a {@link WorkspaceEdit workspace edit}. */ export interface FileWillDeleteEvent { /** * The files that are going to be deleted. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: @@ -10150,25 +10205,25 @@ declare module 'vscode' { /** * The files that got deleted. */ - readonly files: ReadonlyArray; + readonly files: readonly Uri[]; } /** * An event that is fired when files are going to be renamed. * * To make modifications to the workspace before the files are renamed, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). + * call the {@link FileWillCreateEvent.waitUntil `waitUntil}-function with a + * thenable that resolves to a {@link WorkspaceEdit workspace edit}. */ export interface FileWillRenameEvent { /** * The files that are going to be renamed. */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + readonly files: ReadonlyArray<{ readonly oldUri: Uri, readonly newUri: Uri }>; /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: @@ -10205,22 +10260,22 @@ declare module 'vscode' { /** * The files that got renamed. */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + readonly files: ReadonlyArray<{ readonly oldUri: Uri, readonly newUri: Uri }>; } /** - * An event describing a change to the set of [workspace folders](#workspace.workspaceFolders). + * An event describing a change to the set of {@link workspace.workspaceFolders workspace folders}. */ export interface WorkspaceFoldersChangeEvent { /** * Added workspace folders. */ - readonly added: ReadonlyArray; + readonly added: readonly WorkspaceFolder[]; /** * Removed workspace folders. */ - readonly removed: ReadonlyArray; + readonly removed: readonly WorkspaceFolder[]; } /** @@ -10232,14 +10287,14 @@ declare module 'vscode' { /** * The associated uri for this workspace folder. * - * *Note:* The [Uri](#Uri)-type was intentionally chosen such that future releases of the editor can support + * *Note:* The {@link Uri}-type was intentionally chosen such that future releases of the editor can support * workspace folders that are not stored on the local disk, e.g. `ftp://server/workspaces/foo`. */ readonly uri: Uri; /** * The name of this workspace folder. Defaults to - * the basename of its [uri-path](#Uri.path) + * the basename of its {@link Uri.path uri-path} */ readonly name: string; @@ -10261,14 +10316,14 @@ declare module 'vscode' { * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information on * the concept of workspaces in VS Code. * - * The workspace offers support for [listening](#workspace.createFileSystemWatcher) to fs - * events and for [finding](#workspace.findFiles) files. Both perform well and run _outside_ + * The workspace offers support for {@link workspace.createFileSystemWatcher listening} to fs + * events and for {@link workspace.findFiles finding} files. Both perform well and run _outside_ * the editor-process so that they should be always used instead of nodejs-equivalents. */ export namespace workspace { /** - * A [file system](#FileSystem) instance that allows to interact with local and remote + * A {@link FileSystem file system} instance that allows to interact with local and remote * files, e.g. `vscode.workspace.fs.readDirectory(someUri)` allows to retrieve all entries * of a directory or `vscode.workspace.fs.stat(anotherUri)` returns the meta data for a * file. @@ -10282,12 +10337,12 @@ declare module 'vscode' { * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information * on workspaces in VS Code. * - * @deprecated Use [`workspaceFolders`](#workspace.workspaceFolders) instead. + * @deprecated Use {@link workspace.workspaceFolders `workspaceFolders`} instead. */ export const rootPath: string | undefined; /** - * List of workspace folders that are open in VS Code. `undefined when no workspace + * List of workspace folders that are open in VS Code. `undefined` when no workspace * has been opened. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information @@ -10295,7 +10350,7 @@ declare module 'vscode' { * * *Note* that the first entry corresponds to the value of `rootPath`. */ - export const workspaceFolders: ReadonlyArray | undefined; + export const workspaceFolders: readonly WorkspaceFolder[] | undefined; /** * The name of the workspace. `undefined` when no workspace @@ -10346,7 +10401,7 @@ declare module 'vscode' { export const onDidChangeWorkspaceFolders: Event; /** - * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. + * Returns the {@link WorkspaceFolder workspace folder} that contains a given uri. * * returns `undefined` when the given uri doesn't match any workspace folder * * returns the *input* when the given uri is a workspace folder itself * @@ -10358,10 +10413,10 @@ declare module 'vscode' { /** * Returns a path that is relative to the workspace folder or folders. * - * When there are no [workspace folders](#workspace.workspaceFolders) or when the path + * When there are no {@link workspace.workspaceFolders workspace folders} or when the path * is not contained in them, the input is returned. * - * @param pathOrUri A path or uri. When a uri is given its [fsPath](#Uri.fsPath) is used. + * @param pathOrUri A path or uri. When a uri is given its {@link Uri.fsPath fsPath} is used. * @param includeWorkspaceFolder When `true` and when the given path is contained inside a * workspace folder the name of the workspace is prepended. Defaults to `true` when there are * multiple workspace folders and `false` otherwise. @@ -10370,7 +10425,7 @@ declare module 'vscode' { export function asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string; /** - * This method replaces `deleteCount` [workspace folders](#workspace.workspaceFolders) starting at index `start` + * This method replaces `deleteCount` {@link workspace.workspaceFolders workspace folders} starting at index `start` * by an optional set of `workspaceFoldersToAdd` on the `vscode.workspace.workspaceFolders` array. This "splice" * behavior can be used to add, remove and change workspace folders in a single operation. * @@ -10378,7 +10433,7 @@ declare module 'vscode' { * one that called this method) will be terminated and restarted so that the (deprecated) `rootPath` property is * updated to point to the first workspace folder. * - * Use the [`onDidChangeWorkspaceFolders()`](#onDidChangeWorkspaceFolders) event to get notified when the + * Use the {@link onDidChangeWorkspaceFolders `onDidChangeWorkspaceFolders()`} event to get notified when the * workspace folders have been updated. * * **Example:** adding a new workspace folder at the end of workspace folders @@ -10399,10 +10454,10 @@ declare module 'vscode' { * It is valid to remove an existing workspace folder and add it again with a different name * to rename that folder. * - * **Note:** it is not valid to call [updateWorkspaceFolders()](#updateWorkspaceFolders) multiple times - * without waiting for the [`onDidChangeWorkspaceFolders()`](#onDidChangeWorkspaceFolders) to fire. + * **Note:** it is not valid to call {@link updateWorkspaceFolders updateWorkspaceFolders()} multiple times + * without waiting for the {@link onDidChangeWorkspaceFolders `onDidChangeWorkspaceFolders()`} to fire. * - * @param start the zero-based location in the list of currently opened [workspace folders](#WorkspaceFolder) + * @param start the zero-based location in the list of currently opened {@link WorkspaceFolder workspace folders} * from which to start deleting workspace folders. * @param deleteCount the optional number of workspace folders to remove. * @param workspaceFoldersToAdd the optional variable set of workspace folders to add in place of the deleted ones. @@ -10418,12 +10473,12 @@ declare module 'vscode' { * A glob pattern that filters the file events on their absolute path must be provided. Optionally, * flags to ignore certain kinds of events can be provided. To stop listening to events the watcher must be disposed. * - * *Note* that only files within the current [workspace folders](#workspace.workspaceFolders) can be watched. + * *Note* that only files within the current {@link workspace.workspaceFolders workspace folders} can be watched. * *Note* that when watching for file changes such as '**โ€‹/*.js', notifications will not be sent when a parent folder is * moved or deleted (this is a known limitation of the current implementation and may change in the future). * - * @param globPattern A [glob pattern](#GlobPattern) that is applied to the absolute paths of created, changed, - * and deleted files. Use a [relative pattern](#RelativePattern) to limit events to a certain [workspace folder](#WorkspaceFolder). + * @param globPattern A {@link GlobPattern glob pattern} that is applied to the absolute paths of created, changed, + * and deleted files. Use a {@link RelativePattern relative pattern} to limit events to a certain {@link WorkspaceFolder workspace folder}. * @param ignoreCreateEvents Ignore when files have been created. * @param ignoreChangeEvents Ignore when files have been changed. * @param ignoreDeleteEvents Ignore when files have been deleted. @@ -10432,21 +10487,21 @@ declare module 'vscode' { export function createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; /** - * Find files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * Find files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * * @example * findFiles('**โ€‹/*.js', '**โ€‹/node_modules/**', 10) * - * @param include A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern) - * to restrict the search results to a [workspace folder](#WorkspaceFolder). - * @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will - * apply, when `null` no excludes will apply. + * @param include A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. Use a {@link RelativePattern relative pattern} + * to restrict the search results to a {@link WorkspaceFolder workspace folder}. + * @param exclude A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default excludes and the user's + * configured excludes will apply. When `null`, no excludes will apply. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no - * [workspace folders](#workspace.workspaceFolders) are opened. + * {@link workspace.workspaceFolders workspace folders} are opened. */ export function findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Thenable; @@ -10460,7 +10515,7 @@ declare module 'vscode' { /** * Make changes to one or many resources or create, delete, and rename resources as defined by the given - * [workspace edit](#WorkspaceEdit). + * {@link WorkspaceEdit workspace edit}. * * All changes of a workspace edit are applied in the same order in which they have been added. If * multiple textual inserts are made at the same position, these strings appear in the resulting text @@ -10477,36 +10532,36 @@ declare module 'vscode' { export function applyEdit(edit: WorkspaceEdit): Thenable; /** - * All text documents currently known to the system. + * All text documents currently known to the editor. */ - export const textDocuments: ReadonlyArray; + export const textDocuments: readonly TextDocument[]; /** * Opens a document. Will return early if this document is already open. Otherwise - * the document is loaded and the [didOpen](#workspace.onDidOpenTextDocument)-event fires. + * the document is loaded and the {@link workspace.onDidOpenTextDocument didOpen}-event fires. * - * The document is denoted by an [uri](#Uri). Depending on the [scheme](#Uri.scheme) the + * The document is denoted by an {@link Uri}. Depending on the {@link Uri.scheme scheme} the * following rules apply: * * `file`-scheme: Open a file on disk, will be rejected if the file does not exist or cannot be loaded. * * `untitled`-scheme: A new file that should be saved on disk, e.g. `untitled:c:\frodo\new.js`. The language * will be derived from the file name. - * * For all other schemes contributed [text document content providers](#TextDocumentContentProvider) and - * [file system providers](#FileSystemProvider) are consulted. + * * For all other schemes contributed {@link TextDocumentContentProvider text document content providers} and + * {@link FileSystemProvider file system providers} are consulted. * * *Note* that the lifecycle of the returned document is owned by the editor and not by the extension. That means an - * [`onDidClose`](#workspace.onDidCloseTextDocument)-event can occur at any time after opening it. + * {@link workspace.onDidCloseTextDocument `onDidClose`}-event can occur at any time after opening it. * * @param uri Identifies the resource to open. - * @return A promise that resolves to a [document](#TextDocument). + * @return A promise that resolves to a {@link TextDocument document}. */ export function openTextDocument(uri: Uri): Thenable; /** * A short-hand for `openTextDocument(Uri.file(fileName))`. * - * @see [openTextDocument](#openTextDocument) + * @see {@link openTextDocument} * @param fileName A name of a file on disk. - * @return A promise that resolves to a [document](#TextDocument). + * @return A promise that resolves to a {@link TextDocument document}. */ export function openTextDocument(fileName: string): Thenable; @@ -10516,7 +10571,7 @@ declare module 'vscode' { * specify the *language* and/or the *content* of the document. * * @param options Options to control how the document will be created. - * @return A promise that resolves to a [document](#TextDocument). + * @return A promise that resolves to a {@link TextDocument document}. */ export function openTextDocument(options?: { language?: string; content?: string; }): Thenable; @@ -10527,30 +10582,30 @@ declare module 'vscode' { * * @param scheme The uri-scheme to register for. * @param provider A content provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTextDocumentContentProvider(scheme: string, provider: TextDocumentContentProvider): Disposable; /** - * An event that is emitted when a [text document](#TextDocument) is opened or when the language id - * of a text document [has been changed](#languages.setTextDocumentLanguage). + * An event that is emitted when a {@link TextDocument text document} is opened or when the language id + * of a text document {@link languages.setTextDocumentLanguage has been changed}. * - * To add an event listener when a visible text document is opened, use the [TextEditor](#TextEditor) events in the - * [window](#window) namespace. Note that: + * To add an event listener when a visible text document is opened, use the {@link TextEditor} events in the + * {@link window} namespace. Note that: * - * - The event is emitted before the [document](#TextDocument) is updated in the - * [active text editor](#window.activeTextEditor) - * - When a [text document](#TextDocument) is already open (e.g.: open in another [visible text editor](#window.visibleTextEditors)) this event is not emitted + * - The event is emitted before the {@link TextDocument document} is updated in the + * {@link window.activeTextEditor active text editor} + * - When a {@link TextDocument text document} is already open (e.g.: open in another {@link window.visibleTextEditors visible text editor}) this event is not emitted * */ export const onDidOpenTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) is disposed or when the language id - * of a text document [has been changed](#languages.setTextDocumentLanguage). + * An event that is emitted when a {@link TextDocument text document} is disposed or when the language id + * of a text document {@link languages.setTextDocumentLanguage has been changed}. * * *Note 1:* There is no guarantee that this event fires when an editor tab is closed, use the - * [`onDidChangeVisibleTextEditors`](#window.onDidChangeVisibleTextEditors)-event to know when editors change. + * {@link window.onDidChangeVisibleTextEditors `onDidChangeVisibleTextEditors`}-event to know when editors change. * * *Note 2:* A document can be open but not shown in an editor which means this event can fire * for a document that has not been shown in an editor. @@ -10558,19 +10613,19 @@ declare module 'vscode' { export const onDidCloseTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) is changed. This usually happens - * when the [contents](#TextDocument.getText) changes but also when other things like the - * [dirty](#TextDocument.isDirty)-state changes. + * An event that is emitted when a {@link TextDocument text document} is changed. This usually happens + * when the {@link TextDocument.getText contents} changes but also when other things like the + * {@link TextDocument.isDirty dirty}-state changes. */ export const onDidChangeTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) will be saved to disk. + * An event that is emitted when a {@link TextDocument text document} will be saved to disk. * * *Note 1:* Subscribers can delay saving by registering asynchronous work. For the sake of data integrity the editor * might save without firing this event. For instance when shutting down with dirty files. * - * *Note 2:* Subscribers are called sequentially and they can [delay](#TextDocumentWillSaveEvent.waitUntil) saving + * *Note 2:* Subscribers are called sequentially and they can {@link TextDocumentWillSaveEvent.waitUntil delay} saving * by registering asynchronous work. Protection against misbehaving listeners is implemented as such: * * there is an overall time budget that all listeners share and if that is exhausted no further listener is called * * listeners that take a long time or produce errors frequently will not be called anymore @@ -10580,17 +10635,76 @@ declare module 'vscode' { export const onWillSaveTextDocument: Event; /** - * An event that is emitted when a [text document](#TextDocument) is saved to disk. + * An event that is emitted when a {@link TextDocument text document} is saved to disk. */ export const onDidSaveTextDocument: Event; + /** + * All notebook documents currently known to the editor. + */ + export const notebookDocuments: readonly NotebookDocument[]; + + /** + * Open a notebook. Will return early if this notebook is already {@link notebook.notebookDocuments loaded}. Otherwise + * the notebook is loaded and the {@link notebook.onDidOpenNotebookDocument `onDidOpenNotebookDocument`}-event fires. + * + * *Note* that the lifecycle of the returned notebook is owned by the editor and not by the extension. That means an + * {@link notebook.onDidCloseNotebookDocument `onDidCloseNotebookDocument`}-event can occur at any time after. + * + * *Note* that opening a notebook does not show a notebook editor. This function only returns a notebook document which + * can be showns in a notebook editor but it can also be used for other things. + * + * @param uri The resource to open. + * @returns A promise that resolves to a {@link NotebookDocument notebook} + */ + export function openNotebookDocument(uri: Uri): Thenable; + + /** + * Open an untitled notebook. The editor will prompt the user for a file + * path when the document is to be saved. + * + * @see {@link openNotebookDocument} + * @param notebookType The notebook type that should be used. + * @param content The initial contents of the notebook. + * @returns A promise that resolves to a {@link NotebookDocument notebook}. + */ + export function openNotebookDocument(notebookType: string, content?: NotebookData): Thenable; + + /** + * Register a {@link NotebookSerializer notebook serializer}. + * + * A notebook serializer must be contributed through the `notebooks` extension point. When opening a notebook file, the editor will send + * the `onNotebook:` activation event, and extensions must register their serializer in return. + * + * @param notebookType A notebook. + * @param serializer A notebook serialzier. + * @param options Optional context options that define what parts of a notebook should be persisted + * @return A {@link Disposable} that unregisters this serializer when being disposed. + */ + export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions): Disposable; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is opened. + */ + export const onDidOpenNotebookDocument: Event; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is disposed. + * + * *Note 1:* There is no guarantee that this event fires when an editor tab is closed. + * + * *Note 2:* A notebook can be open but not shown in an editor which means this event can fire + * for a notebook that has not been shown in an editor. + */ + export const onDidCloseNotebookDocument: Event; + /** * An event that is emitted when files are being created. * * *Note 1:* This event is triggered by user gestures, like creating a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api. This event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api. This event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When this event is fired, edits to files that are are being created cannot be applied. */ @@ -10600,9 +10714,9 @@ declare module 'vscode' { * An event that is emitted when files have been created. * * *Note:* This event is triggered by user gestures, like creating a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. */ export const onDidCreateFiles: Event; @@ -10610,9 +10724,9 @@ declare module 'vscode' { * An event that is emitted when files are being deleted. * * *Note 1:* This event is triggered by user gestures, like deleting a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When deleting a folder with children only one event is fired. */ @@ -10622,9 +10736,9 @@ declare module 'vscode' { * An event that is emitted when files have been deleted. * * *Note 1:* This event is triggered by user gestures, like deleting a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, or from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When deleting a folder with children only one event is fired. */ @@ -10634,9 +10748,9 @@ declare module 'vscode' { * An event that is emitted when files are being renamed. * * *Note 1:* This event is triggered by user gestures, like renaming a file from the - * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, and from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When renaming a folder with children only one event is fired. */ @@ -10646,9 +10760,9 @@ declare module 'vscode' { * An event that is emitted when files have been renamed. * * *Note 1:* This event is triggered by user gestures, like renaming a file from the - * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * explorer, and from the {@link workspace.applyEdit `workspace.applyEdit`}-api, but this event is *not* fired when * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. + * {@link FileSystem `workspace.fs`}-api. * * *Note 2:* When renaming a folder with children only one event is fired. */ @@ -10670,7 +10784,7 @@ declare module 'vscode' { export function getConfiguration(section?: string | undefined, scope?: ConfigurationScope | null): WorkspaceConfiguration; /** - * An event that is emitted when the [configuration](#WorkspaceConfiguration) changed. + * An event that is emitted when the {@link WorkspaceConfiguration configuration} changed. */ export const onDidChangeConfiguration: Event; @@ -10681,7 +10795,7 @@ declare module 'vscode' { * * @param type The task kind type this provider is registered for. * @param provider A task provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; @@ -10691,10 +10805,10 @@ declare module 'vscode' { * There can only be one provider per scheme and an error is being thrown when a scheme * has been claimed by another provider or when it is reserved. * - * @param scheme The uri-[scheme](#Uri.scheme) the provider registers for. + * @param scheme The uri-{@link Uri.scheme scheme} the provider registers for. * @param provider The filesystem provider. * @param options Immutable metadata about the provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean }): Disposable; @@ -10712,8 +10826,8 @@ declare module 'vscode' { /** * The configuration scope which can be a * a 'resource' or a languageId or both or - * a '[TextDocument](#TextDocument)' or - * a '[WorkspaceFolder](#WorkspaceFolder)' + * a '{@link TextDocument}' or + * a '{@link WorkspaceFolder}' */ export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { uri?: Uri, languageId: string }; @@ -10743,7 +10857,7 @@ declare module 'vscode' { * * The editor provides an API that makes it simple to provide such common features by having all UI and actions already in place and * by allowing you to participate by providing data only. For instance, to contribute a hover all you have to do is provide a function - * that can be called with a [TextDocument](#TextDocument) and a [Position](#Position) returning hover info. The rest, like tracking the + * that can be called with a {@link TextDocument} and a {@link Position} returning hover info. The rest, like tracking the * mouse, positioning the hover, keeping the hover stable etc. is taken care of by the editor. * * ```javascript @@ -10754,11 +10868,11 @@ declare module 'vscode' { * }); * ``` * - * Registration is done using a [document selector](#DocumentSelector) which is either a language id, like `javascript` or - * a more complex [filter](#DocumentFilter) like `{ language: 'typescript', scheme: 'file' }`. Matching a document against such - * a selector will result in a [score](#languages.match) that is used to determine if and how a provider shall be used. When - * scores are equal the provider that came last wins. For features that allow full arity, like [hover](#languages.registerHoverProvider), - * the score is only checked to be `>0`, for other features, like [IntelliSense](#languages.registerCompletionItemProvider) the + * Registration is done using a {@link DocumentSelector document selector} which is either a language id, like `javascript` or + * a more complex {@link DocumentFilter filter} like `{ language: 'typescript', scheme: 'file' }`. Matching a document against such + * a selector will result in a {@link languages.match score} that is used to determine if and how a provider shall be used. When + * scores are equal the provider that came last wins. For features that allow full arity, like {@link languages.registerHoverProvider hover}, + * the score is only checked to be `>0`, for other features, like {@link languages.registerCompletionItemProvider IntelliSense} the * score is used for determining the order in which providers are asked to participate. */ export namespace languages { @@ -10770,11 +10884,11 @@ declare module 'vscode' { export function getLanguages(): Thenable; /** - * Set (and change) the [language](#TextDocument.languageId) that is associated + * Set (and change) the {@link TextDocument.languageId language} that is associated * with the given document. * - * *Note* that calling this function will trigger the [`onDidCloseTextDocument`](#workspace.onDidCloseTextDocument) event - * followed by the [`onDidOpenTextDocument`](#workspace.onDidOpenTextDocument) event. + * *Note* that calling this function will trigger the {@link workspace.onDidCloseTextDocument `onDidCloseTextDocument`} event + * followed by the {@link workspace.onDidOpenTextDocument `onDidOpenTextDocument`} event. * * @param document The document which language is to be changed * @param languageId The new language identifier. @@ -10783,13 +10897,13 @@ declare module 'vscode' { export function setTextDocumentLanguage(document: TextDocument, languageId: string): Thenable; /** - * Compute the match between a document [selector](#DocumentSelector) and a document. Values + * Compute the match between a document {@link DocumentSelector selector} and a document. Values * greater than zero mean the selector matches the document. * * A match is computed according to these rules: - * 1. When [`DocumentSelector`](#DocumentSelector) is an array, compute the match for each contained `DocumentFilter` or language identifier and take the maximum value. - * 2. A string will be desugared to become the `language`-part of a [`DocumentFilter`](#DocumentFilter), so `"fooLang"` is like `{ language: "fooLang" }`. - * 3. A [`DocumentFilter`](#DocumentFilter) will be matched against the document by comparing its parts with the document. The following rules apply: + * 1. When {@link DocumentSelector `DocumentSelector`} is an array, compute the match for each contained `DocumentFilter` or language identifier and take the maximum value. + * 2. A string will be desugared to become the `language`-part of a {@link DocumentFilter `DocumentFilter`}, so `"fooLang"` is like `{ language: "fooLang" }`. + * 3. A {@link DocumentFilter `DocumentFilter`} will be matched against the document by comparing its parts with the document. The following rules apply: * 1. When the `DocumentFilter` is empty (`{}`) the result is `0` * 2. When `scheme`, `language`, or `pattern` are defined but one doesnโ€™t match, the result is `0` * 3. Matching against `*` gives a score of `5`, matching via equality or via a glob-pattern gives a score of `10` @@ -10822,7 +10936,7 @@ declare module 'vscode' { export function match(selector: DocumentSelector, document: TextDocument): number; /** - * An [event](#Event) which fires when the global set of diagnostics changes. This is + * An {@link Event} which fires when the global set of diagnostics changes. This is * newly added and removed diagnostics. */ export const onDidChangeDiagnostics: Event; @@ -10831,7 +10945,7 @@ declare module 'vscode' { * Get all diagnostics for a given resource. * * @param resource A resource - * @returns An array of [diagnostics](#Diagnostic) objects or an empty array. + * @returns An array of {@link Diagnostic diagnostics} objects or an empty array. */ export function getDiagnostics(resource: Uri): Diagnostic[]; @@ -10845,7 +10959,7 @@ declare module 'vscode' { /** * Create a diagnostics collection. * - * @param name The [name](#DiagnosticCollection.name) of the collection. + * @param name The {@link DiagnosticCollection.name name} of the collection. * @return A new diagnostic collection. */ export function createDiagnosticCollection(name?: string): DiagnosticCollection; @@ -10854,20 +10968,20 @@ declare module 'vscode' { * Register a completion provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and groups of equal score are sequentially asked for + * by their {@link languages.match score} and groups of equal score are sequentially asked for * completion items. The process stops when one or many providers of a group return a * result. A failing provider (rejected promise or exception) will not fail the whole * operation. * * A completion item provider can be associated with a set of `triggerCharacters`. When trigger * characters are being typed, completions are requested but only from providers that registered - * the typed character. Because of that trigger characters should be different than [word characters](#LanguageConfiguration.wordPattern), + * the typed character. Because of that trigger characters should be different than {@link LanguageConfiguration.wordPattern word characters}, * a common trigger character is `.` to trigger member completions. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A completion provider. * @param triggerCharacters Trigger completion when the user types one of the characters. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable; @@ -10881,7 +10995,7 @@ declare module 'vscode' { * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code action provider. * @param metadata Metadata about the kind of code actions the provider provides. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCodeActionsProvider(selector: DocumentSelector, provider: CodeActionProvider, metadata?: CodeActionProviderMetadata): Disposable; @@ -10894,7 +11008,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code lens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCodeLensProvider(selector: DocumentSelector, provider: CodeLensProvider): Disposable; @@ -10907,7 +11021,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A definition provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDefinitionProvider(selector: DocumentSelector, provider: DefinitionProvider): Disposable; @@ -10920,7 +11034,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An implementation provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerImplementationProvider(selector: DocumentSelector, provider: ImplementationProvider): Disposable; @@ -10933,7 +11047,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A type definition provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTypeDefinitionProvider(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable; @@ -10946,7 +11060,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A declaration provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDeclarationProvider(selector: DocumentSelector, provider: DeclarationProvider): Disposable; @@ -10959,7 +11073,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A hover provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable; @@ -10971,7 +11085,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An evaluatable expression provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; @@ -10986,7 +11100,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An inline values provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerInlineValuesProvider(selector: DocumentSelector, provider: InlineValuesProvider): Disposable; @@ -10994,12 +11108,12 @@ declare module 'vscode' { * Register a document highlight provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and groups sequentially asked for document highlights. + * by their {@link languages.match score} and groups sequentially asked for document highlights. * The process stops when a provider returns a `non-falsy` or `non-failure` result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document highlight provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentHighlightProvider(selector: DocumentSelector, provider: DocumentHighlightProvider): Disposable; @@ -11013,7 +11127,7 @@ declare module 'vscode' { * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document symbol provider. * @param metaData metadata about the provider - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentSymbolProvider(selector: DocumentSelector, provider: DocumentSymbolProvider, metaData?: DocumentSymbolProviderMetadata): Disposable; @@ -11025,7 +11139,7 @@ declare module 'vscode' { * a failure of the whole operation. * * @param provider A workspace symbol provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerWorkspaceSymbolProvider(provider: WorkspaceSymbolProvider): Disposable; @@ -11038,7 +11152,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A reference provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerReferenceProvider(selector: DocumentSelector, provider: ReferenceProvider): Disposable; @@ -11046,12 +11160,12 @@ declare module 'vscode' { * Register a rename provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and asked in sequence. The first provider producing a result + * by their {@link languages.match score} and asked in sequence. The first provider producing a result * defines the result of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A rename provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable; @@ -11059,12 +11173,12 @@ declare module 'vscode' { * Register a semantic tokens provider for a whole document. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document semantic tokens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; @@ -11078,12 +11192,12 @@ declare module 'vscode' { * will be used. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range semantic tokens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; @@ -11091,29 +11205,29 @@ declare module 'vscode' { * Register a formatting provider for a document. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document formatting edit provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentFormattingEditProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider): Disposable; /** * Register a formatting provider for a document range. * - * *Note:* A document range provider is also a [document formatter](#DocumentFormattingEditProvider) - * which means there is no need to [register](#languages.registerDocumentFormattingEditProvider) a document + * *Note:* A document range provider is also a {@link DocumentFormattingEditProvider document formatter} + * which means there is no need to {@link languages.registerDocumentFormattingEditProvider register} a document * formatter when also registering a range provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range formatting edit provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentRangeFormattingEditProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider): Disposable; @@ -11121,14 +11235,14 @@ declare module 'vscode' { * Register a formatting provider that works on type. The provider is active when the user enables the setting `editor.formatOnType`. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure + * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An on type formatting edit provider. * @param firstTriggerCharacter A character on which formatting should be triggered, like `}`. * @param moreTriggerCharacter More trigger characters. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerOnTypeFormattingEditProvider(selector: DocumentSelector, provider: OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacter: string[]): Disposable; @@ -11136,14 +11250,14 @@ declare module 'vscode' { * Register a signature help provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and called sequentially until a provider returns a + * by their {@link languages.match score} and called sequentially until a provider returns a * valid result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A signature help provider. * @param triggerCharacters Trigger signature help when the user types one of the characters, like `,` or `(`. * @param metadata Information about the provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerSignatureHelpProvider(selector: DocumentSelector, provider: SignatureHelpProvider, ...triggerCharacters: string[]): Disposable; export function registerSignatureHelpProvider(selector: DocumentSelector, provider: SignatureHelpProvider, metadata: SignatureHelpProviderMetadata): Disposable; @@ -11157,7 +11271,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document link provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentLinkProvider(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable; @@ -11170,7 +11284,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A color provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable; @@ -11187,7 +11301,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A folding range provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerFoldingRangeProvider(selector: DocumentSelector, provider: FoldingRangeProvider): Disposable; @@ -11200,7 +11314,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A selection range provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerSelectionRangeProvider(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable; @@ -11209,7 +11323,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A call hierarchy provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable; @@ -11217,26 +11331,825 @@ declare module 'vscode' { * Register a linked editing range provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider that has a result is used. Failure + * by their {@link languages.match score} and the best-matching provider that has a result is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A linked editing range provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerLinkedEditingRangeProvider(selector: DocumentSelector, provider: LinkedEditingRangeProvider): Disposable; /** - * Set a [language configuration](#LanguageConfiguration) for a language. + * Set a {@link LanguageConfiguration language configuration} for a language. * * @param language A language identifier like `typescript`. * @param configuration Language configuration. - * @return A [disposable](#Disposable) that unsets this configuration. + * @return A {@link Disposable} that unsets this configuration. */ export function setLanguageConfiguration(language: string, configuration: LanguageConfiguration): Disposable; } + /** + * A notebook cell kind. + */ + export enum NotebookCellKind { + + /** + * A markup-cell is formatted source that is used for display. + */ + Markup = 1, + + /** + * A code-cell is source that can be {@link NotebookController executed} and that + * produces {@link NotebookCellOutput output}. + */ + Code = 2 + } + + /** + * Represents a cell of a {@link NotebookDocument notebook}, either a {@link NotebookCellKind.Code code}-cell + * or {@link NotebookCellKind.Markup markup}-cell. + * + * NotebookCell instances are immutable and are kept in sync for as long as they are part of their notebook. + */ + export interface NotebookCell { + + /** + * The index of this cell in its {@link NotebookDocument.cellAt containing notebook}. The + * index is updated when a cell is moved within its notebook. The index is `-1` + * when the cell has been removed from its notebook. + */ + readonly index: number; + + /** + * The {@link NotebookDocument notebook} that contains this cell. + */ + readonly notebook: NotebookDocument; + + /** + * The kind of this cell. + */ + readonly kind: NotebookCellKind; + + /** + * The {@link TextDocument text} of this cell, represented as text document. + */ + readonly document: TextDocument; + + /** + * The metadata of this cell. Can be anything but must be JSON-stringifyable. + */ + readonly metadata: { [key: string]: any } + + /** + * The outputs of this cell. + */ + readonly outputs: readonly NotebookCellOutput[]; + + /** + * The most recent {@link NotebookCellExecutionSummary excution summary} for this cell. + */ + readonly executionSummary?: NotebookCellExecutionSummary; + } + + /** + * Represents a notebook which itself is a sequence of {@link NotebookCell code or markup cells}. Notebook documents are + * created from {@link NotebookData notebook data}. + */ + export interface NotebookDocument { + + /** + * The associated uri for this notebook. + * + * *Note* that most notebooks use the `file`-scheme, which means they are files on disk. However, **not** all notebooks are + * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. + * + * @see {@link FileSystemProvider} + */ + readonly uri: Uri; + + /** + * The type of notebook. + */ + readonly notebookType: string; + + /** + * The version number of this notebook (it will strictly increase after each + * change, including undo/redo). + */ + readonly version: number; + + /** + * `true` if there are unpersisted changes. + */ + readonly isDirty: boolean; + + /** + * Is this notebook representing an untitled file which has not been saved yet. + */ + readonly isUntitled: boolean; + + /** + * `true` if the notebook has been closed. A closed notebook isn't synchronized anymore + * and won't be re-used when the same resource is opened again. + */ + readonly isClosed: boolean; + + /** + * Arbitrary metadata for this notebook. Can be anything but must be JSON-stringifyable. + */ + readonly metadata: { [key: string]: any }; + + /** + * The number of cells in the notebook. + */ + readonly cellCount: number; + + /** + * Return the cell at the specified index. The index will be adjusted to the notebook. + * + * @param index - The index of the cell to retrieve. + * @return A {@link NotebookCell cell}. + */ + cellAt(index: number): NotebookCell; + + /** + * Get the cells of this notebook. A subset can be retrieved by providing + * a range. The range will be adjuset to the notebook. + * + * @param range A notebook range. + * @returns The cells contained by the range or all cells. + */ + getCells(range?: NotebookRange): NotebookCell[]; + + /** + * Save the document. The saving will be handled by the corresponding {@link NotebookSerializer serializer}. + * + * @return A promise that will resolve to true when the document + * has been saved. Will return false if the file was not dirty or when save failed. + */ + save(): Thenable; + } + + /** + * The summary of a notebook cell execution. + */ + export interface NotebookCellExecutionSummary { + + /** + * The order in which the execution happened. + */ + readonly executionOrder?: number; + + /** + * If the exclusive finished successfully. + */ + readonly success?: boolean; + + /** + * The times at which execution started and ended, as unix timestamps + */ + readonly timing?: { startTime: number, endTime: number }; + } + + /** + * A notebook range represents an ordered pair of two cell indicies. + * It is guaranteed that start is less than or equal to end. + */ + export class NotebookRange { + + /** + * The zero-based start index of this range. + */ + readonly start: number; + + /** + * The exclusive end index of this range (zero-based). + */ + readonly end: number; + + /** + * `true` if `start` and `end` are equal. + */ + readonly isEmpty: boolean; + + /** + * Create a new notebook range. If `start` is not + * before or equal to `end`, the values will be swapped. + * + * @param start start index + * @param end end index. + */ + constructor(start: number, end: number); + + /** + * Derive a new range for this range. + * + * @param change An object that describes a change to this range. + * @return A range that reflects the given change. Will return `this` range if the change + * is not changing anything. + */ + with(change: { start?: number, end?: number }): NotebookRange; + } + + /** + * One representation of a {@link NotebookCellOutput notebook output}, defined by MIME type and data. + */ + export class NotebookCellOutputItem { + + /** + * Factory function to create a `NotebookCellOutputItem` from a string. + * + * *Note* that an UTF-8 encoder is used to create bytes for the string. + * + * @param value A string. + * @param mime Optional MIME type, defaults to `text/plain`. + * @returns A new output item object. + */ + static text(value: string, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` from + * a JSON object. + * + * *Note* that this function is not expecting "stringified JSON" but + * an object that can be stringified. This function will throw an error + * when the passed value cannot be JSON-stringified. + * + * @param value A JSON-stringifyable value. + * @param mime Optional MIME type, defaults to `application/json` + * @returns A new output item object. + */ + static json(value: any, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stdout` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stdout(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stderr` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stderr(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.error` mime type. + * + * @param value An error object. + * @returns A new output item object. + */ + static error(value: Error): NotebookCellOutputItem; + + /** + * The mime type which determines how the {@link NotebookCellOutputItem.data `data`}-property + * is interpreted. + * + * Notebooks have built-in support for certain mime-types, extensions can add support for new + * types and override existing types. + */ + mime: string; + + /** + * The data of this output item. Must always be an array of unsigned 8-bit integers. + */ + data: Uint8Array; + + /** + * Create a new notbook cell output item. + * + * @param data The value of the output item. + * @param mime The mime type of the output item. + */ + constructor(data: Uint8Array, mime: string); + } + + /** + * Notebook cell output represents a result of executing a cell. It is a container type for multiple + * {@link NotebookCellOutputItem output items} where contained items represent the same result but + * use different MIME types. + */ + export class NotebookCellOutput { + + /** + * The output items of this output. Each item must represent the same result. _Note_ that repeated + * MIME types per output is invalid and that the editor will just pick one of them. + * + * ```ts + * new vscode.NotebookCellOutput([ + * vscode.NotebookCellOutputItem.text('Hello', 'text/plain'), + * vscode.NotebookCellOutputItem.text('Hello', 'text/html'), + * vscode.NotebookCellOutputItem.text('_Hello_', 'text/markdown'), + * vscode.NotebookCellOutputItem.text('Hey', 'text/plain'), // INVALID: repeated type, editor will pick just one + * ]) + * ``` + */ + items: NotebookCellOutputItem[]; + + /** + * Arbitrary metadata for this cell output. Can be anything but must be JSON-stringifyable. + */ + metadata?: { [key: string]: any }; + + /** + * Create new notebook output. + * + * @param items Notebook output items. + * @param metadata Optional metadata. + */ + constructor(items: NotebookCellOutputItem[], metadata?: { [key: string]: any }); + } + + /** + * NotebookCellData is the raw representation of notebook cells. Its is part of {@link NotebookData `NotebookData`}. + */ + export class NotebookCellData { + + /** + * The {@link NotebookCellKind kind} of this cell data. + */ + kind: NotebookCellKind; + + /** + * The source value of this cell data - either source code or formatted text. + */ + value: string; + + /** + * The language identifier of the source value of this cell data. Any value from + * {@link languages.getLanguages `getLanguages`} is possible. + */ + languageId: string; + + /** + * The outputs of this cell data. + */ + outputs?: NotebookCellOutput[]; + + /** + * Arbitrary metadata of this cell data. Can be anything but must be JSON-stringifyable. + */ + metadata?: { [key: string]: any }; + + /** + * The execution summary of this cell data. + */ + executionSummary?: NotebookCellExecutionSummary; + + /** + * Create new cell data. Minimal cell data specifies its kind, its source value, and the + * language identifier of its source. + * + * @param kind The kind. + * @param value The source value. + * @param languageId The language identifier of the source value. + */ + constructor(kind: NotebookCellKind, value: string, languageId: string); + } + + /** + * NotebookData is the raw representation of notebooks. + * + * Extensions are responsible to create {@link NotebookData `NotebookData`} so that the editor + * can create a {@link NotebookDocument `NotebookDocument`}. + * + * @see {@link NotebookSerializer} + */ + export class NotebookData { + /** + * The cell data of this notebook data. + */ + cells: NotebookCellData[]; + + /** + * Arbitrary metadata of notebook data. + */ + metadata?: { [key: string]: any }; + + /** + * Create new notebook data. + * + * @param cells An array of cell data. + */ + constructor(cells: NotebookCellData[]); + } + + /** + * The notebook serializer enables the editor to open notebook files. + * + * At its core the editor only knows a {@link NotebookData notebook data structure} but not + * how that data structure is written to a file, nor how it is read from a file. The + * notebook serializer bridges this gap by deserializing bytes into notebook data and + * vice versa. + */ + export interface NotebookSerializer { + + /** + * Deserialize contents of a notebook file into the notebook data structure. + * + * @param content Contents of a notebook file. + * @param token A cancellation token. + * @return Notebook data or a thenable that resolves to such. + */ + deserializeNotebook(content: Uint8Array, token: CancellationToken): NotebookData | Thenable; + + /** + * Serialize notebook data into file contents. + * + * @param data A notebook data structure. + * @param token A cancellation token. + * @returns An array of bytes or a thenable that resolves to such. + */ + serializeNotebook(data: NotebookData, token: CancellationToken): Uint8Array | Thenable; + } + + /** + * Notebook content options define what parts of a notebook are persisted. Note + * + * For instance, a notebook serializer can opt-out of saving outputs and in that case the editor doesn't mark a + * notebooks as {@link NotebookDocument.isDirty dirty} when its output has changed. + */ + export interface NotebookDocumentContentOptions { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs?: boolean; + + /** + * Controls if a cell metadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientCellMetadata?: { [key: string]: boolean | undefined }; + + /** + * Controls if a document metadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientDocumentMetadata?: { [key: string]: boolean | undefined }; + } + + /** + * Notebook controller affinity for notebook documents. + * + * @see {@link NotebookController.updateNotebookAffinity} + */ + export enum NotebookControllerAffinity { + /** + * Default affinity. + */ + Default = 1, + /** + * A controller is preferred for a notebook. + */ + Preferred = 2 + } + + /** + * A notebook controller represents an entity that can execute notebook cells. This is often referred to as a kernel. + * + * There can be multiple controllers and the editor will let users choose which controller to use for a certain notebook. The + * {@link NotebookController.notebookType `notebookType`}-property defines for what kind of notebooks a controller is for and + * the {@link NotebookController.updateNotebookAffinity `updateNotebookAffinity`}-function allows controllers to set a preference + * for specific notebook documents. When a controller has been selected its + * {@link NotebookController.onDidChangeSelectedNotebooks onDidChangeSelectedNotebooks}-event fires. + * + * When a cell is being run the editor will invoke the {@link NotebookController.executeHandler `executeHandler`} and a controller + * is expected to create and finalize a {@link NotebookCellExecution notebook cell execution}. However, controllers are also free + * to create executions by themselves. + */ + export interface NotebookController { + + /** + * The identifier of this notebook controller. + * + * _Note_ that controllers are remembered by their identifier and that extensions should use + * stable identifiers across sessions. + */ + readonly id: string; + + /** + * The notebook type this controller is for. + */ + readonly notebookType: string; + + /** + * An array of language identifiers that are supported by this + * controller. Any language identifier from {@link languages.getLanguages `getLanguages`} + * is possible. When falsy all languages are supported. + * + * Samples: + * ```js + * // support JavaScript and TypeScript + * myController.supportedLanguages = ['javascript', 'typescript'] + * + * // support all languages + * myController.supportedLanguages = undefined; // falsy + * myController.supportedLanguages = []; // falsy + * ``` + */ + supportedLanguages?: string[]; + + /** + * The human-readable label of this notebook controller. + */ + label: string; + + /** + * The human-readable description which is rendered less prominent. + */ + description?: string; + + /** + * The human-readable detail which is rendered less prominent. + */ + detail?: string; + + /** + * Whether this controller supports execution order so that the + * editor can render placeholders for them. + */ + supportsExecutionOrder?: boolean; + + /** + * Create a cell execution task. + * + * _Note_ that there can only be one execution per cell at a time and that an error is thrown if + * a cell execution is created while another is still active. + * + * This should be used in response to the {@link NotebookController.executeHandler execution handler} + * being called or when cell execution has been started else, e.g when a cell was already + * executing or when cell execution was triggered from another source. + * + * @param cell The notebook cell for which to create the execution. + * @returns A notebook cell execution. + */ + createNotebookCellExecution(cell: NotebookCell): NotebookCellExecution; + + /** + * The execute handler is invoked when the run gestures in the UI are selected, e.g Run Cell, Run All, + * Run Selection etc. The execute handler is responsible for creating and managing {@link NotebookCellExecution execution}-objects. + */ + executeHandler: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable; + + /** + * Optional interrupt handler. + * + * By default cell execution is canceled via {@link NotebookCellExecution.token tokens}. Cancellation + * tokens require that a controller can keep track of its execution so that it can cancel a specific execution at a later + * point. Not all scenarios allow for that, eg. REPL-style controllers often work by interrupting whatever is currently + * running. For those cases the interrupt handler exists - it can be thought of as the equivalent of `SIGINT` + * or `Control+C` in terminals. + * + * _Note_ that supporting {@link NotebookCellExecution.token cancellation tokens} is preferred and that interrupt handlers should + * only be used when tokens cannot be supported. + */ + interruptHandler?: (notebook: NotebookDocument) => void | Thenable; + + /** + * An event that fires whenever a controller has been selected or un-selected for a notebook document. + * + * There can be multiple controllers for a notebook and in that case a controllers needs to be _selected_. This is a user gesture + * and happens either explicitly or implicitly when interacting with a notebook for which a controller was _suggested_. When possible, + * the editor _suggests_ a controller that is most likely to be _selected_. + * + * _Note_ that controller selection is persisted (by the controllers {@link NotebookController.id id}) and restored as soon as a + * controller is re-created or as a notebook is {@link workspace.onDidOpenNotebookDocument opened}. + */ + readonly onDidChangeSelectedNotebooks: Event<{ notebook: NotebookDocument, selected: boolean }>; + + /** + * A controller can set affinities for specific notebook documents. This allows a controller + * to be presented more prominent for some notebooks. + * + * @param notebook The notebook for which a priority is set. + * @param affinity A controller affinity + */ + updateNotebookAffinity(notebook: NotebookDocument, affinity: NotebookControllerAffinity): void; + + /** + * Dispose and free associated resources. + */ + dispose(): void; + } + + /** + * A NotebookCellExecution is how {@link NotebookController notebook controller} modify a notebook cell as + * it is executing. + * + * When a cell execution object is created, the cell enters the {@link NotebookCellExecutionState.Pending `Pending`} state. + * When {@link NotebookCellExecution.start `start(...)`} is called on the execution task, it enters the {@link NotebookCellExecutionState.Executing `Executing`} state. When + * {@link NotebookCellExecution.end `end(...)`} is called, it enters the {@link NotebookCellExecutionState.Idle `Idle`} state. + */ + export interface NotebookCellExecution { + + /** + * The {@link NotebookCell cell} for which this execution has been created. + */ + readonly cell: NotebookCell; + + /** + * A cancellation token which will be triggered when the cell execution is canceled + * from the UI. + * + * _Note_ that the cancellation token will not be triggered when the {@link NotebookController controller} + * that created this execution uses an {@link NotebookController.interruptHandler interrupt-handler}. + */ + readonly token: CancellationToken; + + /** + * Set and unset the order of this cell execution. + */ + executionOrder: number | undefined; + + /** + * Signal that the execution has begun. + * + * @param startTime The time that execution began, in milliseconds in the Unix epoch. Used to drive the clock + * that shows for how long a cell has been running. If not given, the clock won't be shown. + */ + start(startTime?: number): void; + + /** + * Signal that execution has ended. + * + * @param success If true, a green check is shown on the cell status bar. + * If false, a red X is shown. + * If undefined, no check or X icon is shown. + * @param endTime The time that execution finished, in milliseconds in the Unix epoch. + */ + end(success: boolean | undefined, endTime?: number): void; + + /** + * Clears the output of the cell that is executing or of another cell that is affected by this execution. + * + * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of + * this execution. + * @return A thenable that resolves when the operation finished. + */ + clearOutput(cell?: NotebookCell): Thenable; + + /** + * Replace the output of the cell that is executing or of another cell that is affected by this execution. + * + * @param out Output that replaces the current output. + * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of + * this execution. + * @return A thenable that resolves when the operation finished. + */ + replaceOutput(out: NotebookCellOutput | NotebookCellOutput[], cell?: NotebookCell): Thenable; + + /** + * Append to the output of the cell that is executing or to another cell that is affected by this execution. + * + * @param out Output that is appended to the current output. + * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of + * this execution. + * @return A thenable that resolves when the operation finished. + */ + appendOutput(out: NotebookCellOutput | NotebookCellOutput[], cell?: NotebookCell): Thenable; + + /** + * Replace all output items of existing cell output. + * + * @param items Output items that replace the items of existing output. + * @param output Output object that already exists. + * @return A thenable that resolves when the operation finished. + */ + replaceOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; + + /** + * Append output items to existing cell output. + * + * @param items Output items that are append to existing output. + * @param output Output object that already exists. + * @return A thenable that resolves when the operation finished. + */ + appendOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; + } + + /** + * Represents the alignment of status bar items. + */ + export enum NotebookCellStatusBarAlignment { + + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 + } + + /** + * A contribution to a cell's status bar + */ + export class NotebookCellStatusBarItem { + /** + * The text to show for the item. + */ + text: string; + + /** + * Whether the item is aligned to the left or right. + */ + alignment: NotebookCellStatusBarAlignment; + + /** + * An optional {@link Command `Command`} or identifier of a command to run on click. + * + * The command must be {@link commands.getCommands known}. + * + * Note that if this is a {@link Command `Command`} object, only the {@link Command.command `command`} and {@link Command.arguments `arguments`} + * are used by VS Code. + */ + command?: string | Command; + + /** + * A tooltip to show when the item is hovered. + */ + tooltip?: string; + + /** + * The priority of the item. A higher value item will be shown more to the left. + */ + priority?: number; + + /** + * Accessibility information used when a screen reader interacts with this item. + */ + accessibilityInformation?: AccessibilityInformation; + + /** + * Creates a new NotebookCellStatusBarItem. + * @param text The text to show for the item. + * @param alignment Whether the item is aligned to the left or right. + */ + constructor(text: string, alignment: NotebookCellStatusBarAlignment); + } + + /** + * A provider that can contribute items to the status bar that appears below a cell's editor. + */ + export interface NotebookCellStatusBarItemProvider { + /** + * An optional event to signal that statusbar items have changed. The provide method will be called again. + */ + onDidChangeCellStatusBarItems?: Event; + + /** + * The provider will be called when the cell scrolls into view, when its content, outputs, language, or metadata change, and when it changes execution state. + * @param cell The cell for which to return items. + * @param token A token triggered if this request should be cancelled. + * @return One or more {@link NotebookCellStatusBarItem cell statusbar items} + */ + provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): ProviderResult; + } + + /** + * Namespace for notebooks. + * + * The notebooks functionality is composed of three loosly coupled components: + * + * 1. {@link NotebookSerializer} enable the editor to open, show, and save notebooks + * 2. {@link NotebookController} own the execution of notebooks, e.g they create output from code cells. + * 3. NotebookRenderer present notebook output in the editor. They run in a separate context. + */ + export namespace notebooks { + + /** + * Creates a new notebook controller. + * + * @param id Identifier of the controller. Must be unique per extension. + * @param notebookType A notebook type for which this controller is for. + * @param label The label of the controller. + * @param handler The execute-handler of the controller. + */ + export function createNotebookController(id: string, notebookType: string, label: string, handler?: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable): NotebookController; + + /** + * Register a {@link NotebookCellStatusBarItemProvider cell statusbar item provider} for the given notebook type. + * + * @param notebookType The notebook type to register for. + * @param provider A cell status bar provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerNotebookCellStatusBarItemProvider(notebookType: string, provider: NotebookCellStatusBarItemProvider): Disposable; + } + /** * Represents the input box in the Source Control viewlet. */ @@ -11261,7 +12174,7 @@ declare module 'vscode' { interface QuickDiffProvider { /** - * Provide a [uri](#Uri) to the original resource of any given resource uri. + * Provide a {@link Uri} to the original resource of any given resource uri. * * @param uri The uri of the resource open in a text editor. * @param token A cancellation token. @@ -11272,38 +12185,38 @@ declare module 'vscode' { /** * The theme-aware decorations for a - * [source control resource state](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource state}. */ export interface SourceControlResourceThemableDecorations { /** * The icon path for a specific - * [source control resource state](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource state}. */ readonly iconPath?: string | Uri; } /** - * The decorations for a [source control resource state](#SourceControlResourceState). + * The decorations for a {@link SourceControlResourceState source control resource state}. * Can be independently specified for light and dark themes. */ export interface SourceControlResourceDecorations extends SourceControlResourceThemableDecorations { /** - * Whether the [source control resource state](#SourceControlResourceState) should + * Whether the {@link SourceControlResourceState source control resource state} should * be striked-through in the UI. */ readonly strikeThrough?: boolean; /** - * Whether the [source control resource state](#SourceControlResourceState) should + * Whether the {@link SourceControlResourceState source control resource state} should * be faded in the UI. */ readonly faded?: boolean; /** * The title for a specific - * [source control resource state](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource state}. */ readonly tooltip?: string; @@ -11320,23 +12233,23 @@ declare module 'vscode' { /** * An source control resource state represents the state of an underlying workspace - * resource within a certain [source control group](#SourceControlResourceGroup). + * resource within a certain {@link SourceControlResourceGroup source control group}. */ export interface SourceControlResourceState { /** - * The [uri](#Uri) of the underlying resource inside the workspace. + * The {@link Uri} of the underlying resource inside the workspace. */ readonly resourceUri: Uri; /** - * The [command](#Command) which should be run when the resource + * The {@link Command} which should be run when the resource * state is open in the Source Control viewlet. */ readonly command?: Command; /** - * The [decorations](#SourceControlResourceDecorations) for this source control + * The {@link SourceControlResourceDecorations decorations} for this source control * resource state. */ readonly decorations?: SourceControlResourceDecorations; @@ -11364,7 +12277,7 @@ declare module 'vscode' { /** * A source control resource group is a collection of - * [source control resource states](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource states}. */ export interface SourceControlResourceGroup { @@ -11380,13 +12293,13 @@ declare module 'vscode' { /** * Whether this source control resource group is hidden when it contains - * no [source control resource states](#SourceControlResourceState). + * no {@link SourceControlResourceState source control resource states}. */ hideWhenEmpty?: boolean; /** * This group's collection of - * [source control resource states](#SourceControlResourceState). + * {@link SourceControlResourceState source control resource states}. */ resourceStates: SourceControlResourceState[]; @@ -11397,7 +12310,7 @@ declare module 'vscode' { } /** - * An source control is able to provide [resource states](#SourceControlResourceState) + * An source control is able to provide {@link SourceControlResourceState resource states} * to the editor and interact with the editor in several source control related ways. */ export interface SourceControl { @@ -11418,21 +12331,21 @@ declare module 'vscode' { readonly rootUri: Uri | undefined; /** - * The [input box](#SourceControlInputBox) for this source control. + * The {@link SourceControlInputBox input box} for this source control. */ readonly inputBox: SourceControlInputBox; /** - * The UI-visible count of [resource states](#SourceControlResourceState) of + * The UI-visible count of {@link SourceControlResourceState resource states} of * this source control. * - * Equals to the total number of [resource state](#SourceControlResourceState) + * Equals to the total number of {@link SourceControlResourceState resource state} * of this source control, if undefined. */ count?: number; /** - * An optional [quick diff provider](#QuickDiffProvider). + * An optional {@link QuickDiffProvider quick diff provider}. */ quickDiffProvider?: QuickDiffProvider; @@ -11460,7 +12373,7 @@ declare module 'vscode' { statusBarCommands?: Command[]; /** - * Create a new [resource group](#SourceControlResourceGroup). + * Create a new {@link SourceControlResourceGroup resource group}. */ createResourceGroup(id: string, label: string): SourceControlResourceGroup; @@ -11473,7 +12386,7 @@ declare module 'vscode' { export namespace scm { /** - * The [input box](#SourceControlInputBox) for the last source control + * The {@link SourceControlInputBox input box} for the last source control * created by the extension. * * @deprecated Use SourceControl.inputBox instead @@ -11481,12 +12394,12 @@ declare module 'vscode' { export const inputBox: SourceControlInputBox; /** - * Creates a new [source control](#SourceControl) instance. + * Creates a new {@link SourceControl source control} instance. * * @param id An `id` for the source control. Something short, e.g.: `git`. * @param label A human-readable string for the source control. E.g.: `Git`. * @param rootUri An optional Uri of the root of the source control. E.g.: `Uri.parse(workspaceRoot)`. - * @return An instance of [source control](#SourceControl). + * @return An instance of {@link SourceControl source control}. */ export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } @@ -11548,12 +12461,18 @@ declare module 'vscode' { readonly id: string; /** - * The debug session's type from the [debug configuration](#DebugConfiguration). + * The debug session's type from the {@link DebugConfiguration debug configuration}. */ readonly type: string; /** - * The debug session's name is initially taken from the [debug configuration](#DebugConfiguration). + * The parent session of this debug session, if it was created as a child. + * @see DebugSessionOptions.parentSession + */ + readonly parentSession?: DebugSession; + + /** + * The debug session's name is initially taken from the {@link DebugConfiguration debug configuration}. * Any changes will be properly reflected in the UI. */ name: string; @@ -11564,7 +12483,7 @@ declare module 'vscode' { readonly workspaceFolder: WorkspaceFolder | undefined; /** - * The "resolved" [debug configuration](#DebugConfiguration) of this session. + * The "resolved" {@link DebugConfiguration debug configuration} of this session. * "Resolved" means that * - all variables have been substituted and * - platform specific attribute sections have been "flattened" for the matching platform and removed for non-matching platforms. @@ -11580,18 +12499,18 @@ declare module 'vscode' { * Maps a VS Code breakpoint to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. * If no DAP breakpoint exists (either because the VS Code breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. * - * @param breakpoint A VS Code [breakpoint](#Breakpoint). + * @param breakpoint A VS Code {@link Breakpoint}. * @return A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. */ getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; } /** - * A custom Debug Adapter Protocol event received from a [debug session](#DebugSession). + * A custom Debug Adapter Protocol event received from a {@link DebugSession debug session}. */ export interface DebugSessionCustomEvent { /** - * The [debug session](#DebugSession) for which the custom event was received. + * The {@link DebugSession debug session} for which the custom event was received. */ readonly session: DebugSession; @@ -11609,28 +12528,28 @@ declare module 'vscode' { /** * A debug configuration provider allows to add debug configurations to the debug service * and to resolve launch configurations before they are used to start a debug session. - * A debug configuration provider is registered via #debug.registerDebugConfigurationProvider. + * A debug configuration provider is registered via {@link debug.registerDebugConfigurationProvider}. */ export interface DebugConfigurationProvider { /** - * Provides [debug configuration](#DebugConfiguration) to the debug service. If more than one debug configuration provider is + * Provides {@link DebugConfiguration debug configuration} to the debug service. If more than one debug configuration provider is * registered for the same type, debug configurations are concatenated in arbitrary order. * * @param folder The workspace folder for which the configurations are used or `undefined` for a folderless setup. * @param token A cancellation token. - * @return An array of [debug configurations](#DebugConfiguration). + * @return An array of {@link DebugConfiguration debug configurations}. */ provideDebugConfigurations?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; /** - * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. + * Resolves a {@link DebugConfiguration debug configuration} by filling in missing values or by adding/changing/removing attributes. * If more than one debug configuration provider is registered for the same type, the resolveDebugConfiguration calls are chained * in arbitrary order and the initial debug configuration is piped through the chain. * Returning the value 'undefined' prevents the debug session from starting. * Returning the value 'null' prevents the debug session from starting and opens the underlying debug configuration instead. * * @param folder The workspace folder from which the configuration originates from or `undefined` for a folderless setup. - * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @param debugConfiguration The {@link DebugConfiguration debug configuration} to resolve. * @param token A cancellation token. * @return The resolved debug configuration or undefined or null. */ @@ -11638,14 +12557,14 @@ declare module 'vscode' { /** * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted. - * It can be used to resolve or verify a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. + * It can be used to resolve or verify a {@link DebugConfiguration debug configuration} by filling in missing values or by adding/changing/removing attributes. * If more than one debug configuration provider is registered for the same type, the 'resolveDebugConfigurationWithSubstitutedVariables' calls are chained * in arbitrary order and the initial debug configuration is piped through the chain. * Returning the value 'undefined' prevents the debug session from starting. * Returning the value 'null' prevents the debug session from starting and opens the underlying debug configuration instead. * * @param folder The workspace folder from which the configuration originates from or `undefined` for a folderless setup. - * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @param debugConfiguration The {@link DebugConfiguration debug configuration} to resolve. * @param token A cancellation token. * @return The resolved debug configuration or undefined or null. */ @@ -11775,10 +12694,10 @@ declare module 'vscode' { export interface DebugAdapterDescriptorFactory { /** * 'createDebugAdapterDescriptor' is called at the start of a debug session to provide details about the debug adapter to use. - * These details must be returned as objects of type [DebugAdapterDescriptor](#DebugAdapterDescriptor). + * These details must be returned as objects of type {@link DebugAdapterDescriptor}. * Currently two types of debug adapters are supported: - * - a debug adapter executable is specified as a command path and arguments (see [DebugAdapterExecutable](#DebugAdapterExecutable)), - * - a debug adapter server reachable via a communication port (see [DebugAdapterServer](#DebugAdapterServer)). + * - a debug adapter executable is specified as a command path and arguments (see {@link DebugAdapterExecutable}), + * - a debug adapter server reachable via a communication port (see {@link DebugAdapterServer}). * If the method is not implemented the default behavior is this: * createDebugAdapter(session: DebugSession, executable: DebugAdapterExecutable) { * if (typeof session.configuration.debugServer === 'number') { @@ -11786,9 +12705,9 @@ declare module 'vscode' { * } * return executable; * } - * @param session The [debug session](#DebugSession) for which the debug adapter will be used. + * @param session The {@link DebugSession debug session} for which the debug adapter will be used. * @param executable The debug adapter's executable information as specified in the package.json (or undefined if no such information exists). - * @return a [debug adapter descriptor](#DebugAdapterDescriptor) or undefined. + * @return a {@link DebugAdapterDescriptor debug adapter descriptor} or undefined. */ createDebugAdapterDescriptor(session: DebugSession, executable: DebugAdapterExecutable | undefined): ProviderResult; } @@ -11828,8 +12747,8 @@ declare module 'vscode' { * The method 'createDebugAdapterTracker' is called at the start of a debug session in order * to return a "tracker" object that provides read-access to the communication between VS Code and a debug adapter. * - * @param session The [debug session](#DebugSession) for which the debug adapter tracker will be used. - * @return A [debug adapter tracker](#DebugAdapterTracker) or undefined. + * @param session The {@link DebugSession debug session} for which the debug adapter tracker will be used. + * @return A {@link DebugAdapterTracker debug adapter tracker} or undefined. */ createDebugAdapterTracker(session: DebugSession): ProviderResult; } @@ -11855,23 +12774,23 @@ declare module 'vscode' { } /** - * An event describing the changes to the set of [breakpoints](#Breakpoint). + * An event describing the changes to the set of {@link Breakpoint breakpoints}. */ export interface BreakpointsChangeEvent { /** * Added breakpoints. */ - readonly added: ReadonlyArray; + readonly added: readonly Breakpoint[]; /** * Removed breakpoints. */ - readonly removed: ReadonlyArray; + readonly removed: readonly Breakpoint[]; /** * Changed breakpoints. */ - readonly changed: ReadonlyArray; + readonly changed: readonly Breakpoint[]; } /** @@ -11933,7 +12852,7 @@ declare module 'vscode' { } /** - * Debug console mode used by debug session, see [options](#DebugSessionOptions). + * Debug console mode used by debug session, see {@link DebugSessionOptions options}. */ export enum DebugConsoleMode { /** @@ -11949,7 +12868,7 @@ declare module 'vscode' { } /** - * Options for [starting a debug session](#debug.startDebugging). + * Options for {@link debug.startDebugging starting a debug session}. */ export interface DebugSessionOptions { @@ -11984,7 +12903,7 @@ declare module 'vscode' { * A DebugConfigurationProviderTriggerKind specifies when the `provideDebugConfigurations` method of a `DebugConfigurationProvider` is triggered. * Currently there are two situations: to provide the initial debug configurations for a newly created launch.json or * to provide dynamically generated debug configurations when the user asks for them through the UI (e.g. via the "Select and Start Debugging" command). - * A trigger kind is used when registering a `DebugConfigurationProvider` with #debug.registerDebugConfigurationProvider. + * A trigger kind is used when registering a `DebugConfigurationProvider` with {@link debug.registerDebugConfigurationProvider}. */ export enum DebugConfigurationProviderTriggerKind { /** @@ -12003,14 +12922,14 @@ declare module 'vscode' { export namespace debug { /** - * The currently active [debug session](#DebugSession) or `undefined`. The active debug session is the one + * The currently active {@link DebugSession debug session} or `undefined`. The active debug session is the one * represented by the debug action floating window or the one currently shown in the drop down menu of the debug action floating window. * If no debug session is active, the value is `undefined`. */ export let activeDebugSession: DebugSession | undefined; /** - * The currently active [debug console](#DebugConsole). + * The currently active {@link DebugConsole debug console}. * If no debug session is active, output sent to the debug console is not shown. */ export let activeDebugConsole: DebugConsole; @@ -12021,35 +12940,35 @@ declare module 'vscode' { export let breakpoints: Breakpoint[]; /** - * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) + * An {@link Event} which fires when the {@link debug.activeDebugSession active debug session} * has changed. *Note* that the event also fires when the active debug session changes * to `undefined`. */ export const onDidChangeActiveDebugSession: Event; /** - * An [event](#Event) which fires when a new [debug session](#DebugSession) has been started. + * An {@link Event} which fires when a new {@link DebugSession debug session} has been started. */ export const onDidStartDebugSession: Event; /** - * An [event](#Event) which fires when a custom DAP event is received from the [debug session](#DebugSession). + * An {@link Event} which fires when a custom DAP event is received from the {@link DebugSession debug session}. */ export const onDidReceiveDebugSessionCustomEvent: Event; /** - * An [event](#Event) which fires when a [debug session](#DebugSession) has terminated. + * An {@link Event} which fires when a {@link DebugSession debug session} has terminated. */ export const onDidTerminateDebugSession: Event; /** - * An [event](#Event) that is emitted when the set of breakpoints is added, removed, or changed. + * An {@link Event} that is emitted when the set of breakpoints is added, removed, or changed. */ export const onDidChangeBreakpoints: Event; /** - * Register a [debug configuration provider](#DebugConfigurationProvider) for a specific debug type. - * The optional [triggerKind](#DebugConfigurationProviderTriggerKind) can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. + * Register a {@link DebugConfigurationProvider debug configuration provider} for a specific debug type. + * The optional {@link DebugConfigurationProviderTriggerKind triggerKind} can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. * Currently two trigger kinds are possible: with the value `Initial` (or if no trigger kind argument is given) the `provideDebugConfigurations` method is used to provide the initial debug configurations to be copied into a newly created launch.json. * With the trigger kind `Dynamic` the `provideDebugConfigurations` method is used to dynamically determine debug configurations to be presented to the user (in addition to the static configurations from the launch.json). * Please note that the `triggerKind` argument only applies to the `provideDebugConfigurations` method: so the `resolveDebugConfiguration` methods are not affected at all. @@ -12057,20 +12976,20 @@ declare module 'vscode' { * More than one provider can be registered for the same type. * * @param type The debug type for which the provider is registered. - * @param provider The [debug configuration provider](#DebugConfigurationProvider) to register. - * @param triggerKind The [trigger](#DebugConfigurationProviderTrigger) for which the 'provideDebugConfiguration' method of the provider is registered. If `triggerKind` is missing, the value `DebugConfigurationProviderTriggerKind.Initial` is assumed. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @param provider The {@link DebugConfigurationProvider debug configuration provider} to register. + * @param triggerKind The {@link DebugConfigurationProviderTrigger trigger} for which the 'provideDebugConfiguration' method of the provider is registered. If `triggerKind` is missing, the value `DebugConfigurationProviderTriggerKind.Initial` is assumed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider, triggerKind?: DebugConfigurationProviderTriggerKind): Disposable; /** - * Register a [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) for a specific debug type. + * Register a {@link DebugAdapterDescriptorFactory debug adapter descriptor factory} for a specific debug type. * An extension is only allowed to register a DebugAdapterDescriptorFactory for the debug type(s) defined by the extension. Otherwise an error is thrown. * Registering more than one DebugAdapterDescriptorFactory for a debug type results in an error. * * @param debugType The debug type for which the factory is registered. - * @param factory The [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) to register. - * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + * @param factory The {@link DebugAdapterDescriptorFactory debug adapter descriptor factory} to register. + * @return A {@link Disposable} that unregisters this factory when being disposed. */ export function registerDebugAdapterDescriptorFactory(debugType: string, factory: DebugAdapterDescriptorFactory): Disposable; @@ -12078,27 +12997,27 @@ declare module 'vscode' { * Register a debug adapter tracker factory for the given debug type. * * @param debugType The debug type for which the factory is registered or '*' for matching all debug types. - * @param factory The [debug adapter tracker factory](#DebugAdapterTrackerFactory) to register. - * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + * @param factory The {@link DebugAdapterTrackerFactory debug adapter tracker factory} to register. + * @return A {@link Disposable} that unregisters this factory when being disposed. */ export function registerDebugAdapterTrackerFactory(debugType: string, factory: DebugAdapterTrackerFactory): Disposable; /** * Start debugging by using either a named launch or named compound configuration, - * or by directly passing a [DebugConfiguration](#DebugConfiguration). + * or by directly passing a {@link DebugConfiguration}. * The named configurations are looked up in '.vscode/launch.json' found in the given folder. * Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date. * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. - * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. - * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. - * @param parentSessionOrOptions Debug session options. When passed a parent [debug session](#DebugSession), assumes options with just this parent session. + * @param folder The {@link WorkspaceFolder workspace folder} for looking up named configurations and resolving variables or `undefined` for a non-folder setup. + * @param nameOrConfiguration Either the name of a debug or compound configuration or a {@link DebugConfiguration} object. + * @param parentSessionOrOptions Debug session options. When passed a parent {@link DebugSession debug session}, assumes options with just this parent session. * @return A thenable that resolves when debugging could be successfully started. */ export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSessionOrOptions?: DebugSession | DebugSessionOptions): Thenable; /** * Stop the given debug session or stop all debug sessions if session is omitted. - * @param session The [debug session](#DebugSession) to stop; if omitted all sessions are stopped. + * @param session The {@link DebugSession debug session} to stop; if omitted all sessions are stopped. */ export function stopDebugging(session?: DebugSession): Thenable; @@ -12106,13 +13025,13 @@ declare module 'vscode' { * Add breakpoints. * @param breakpoints The breakpoints to add. */ - export function addBreakpoints(breakpoints: Breakpoint[]): void; + export function addBreakpoints(breakpoints: readonly Breakpoint[]): void; /** * Remove breakpoints. * @param breakpoints The breakpoints to remove. */ - export function removeBreakpoints(breakpoints: Breakpoint[]): void; + export function removeBreakpoints(breakpoints: readonly Breakpoint[]): void; /** * Converts a "Source" descriptor object received via the Debug Adapter Protocol into a Uri that can be used to load its contents. @@ -12130,7 +13049,7 @@ declare module 'vscode' { /** * Namespace for dealing with installed extensions. Extensions are represented - * by an [extension](#Extension)-interface which enables reflection on them. + * by an {@link Extension}-interface which enables reflection on them. * * Extension writers can provide APIs to other extensions by returning their API public * surface from the `activate`-call. @@ -12150,8 +13069,8 @@ declare module 'vscode' { * } * ``` * When depending on the API of another extension add an `extensionDependencies`-entry - * to `package.json`, and use the [getExtension](#extensions.getExtension)-function - * and the [exports](#Extension.exports)-property, like below: + * to `package.json`, and use the {@link extensions.getExtension getExtension}-function + * and the {@link Extension.exports exports}-property, like below: * * ```javascript * let mathExt = extensions.getExtension('genius.math'); @@ -12181,7 +13100,7 @@ declare module 'vscode' { /** * All extensions currently known to the system. */ - export const all: ReadonlyArray>; + export const all: readonly Extension[]; /** * An event which fires when `extensions.all` changes. This can happen when extensions are @@ -12191,7 +13110,7 @@ declare module 'vscode' { } /** - * Collapsible state of a [comment thread](#CommentThread) + * Collapsible state of a {@link CommentThread comment thread} */ export enum CommentThreadCollapsibleState { /** @@ -12206,7 +13125,7 @@ declare module 'vscode' { } /** - * Comment mode of a [comment](#Comment) + * Comment mode of a {@link Comment} */ export enum CommentMode { /** @@ -12221,7 +13140,7 @@ declare module 'vscode' { } /** - * A collection of [comments](#Comment) representing a conversation at a particular range in a document. + * A collection of {@link Comment comments} representing a conversation at a particular range in a document. */ export interface CommentThread { /** @@ -12238,7 +13157,7 @@ declare module 'vscode' { /** * The ordered comments of the thread. */ - comments: ReadonlyArray; + comments: readonly Comment[]; /** * Whether the thread should be collapsed or expanded when opening the document. @@ -12273,7 +13192,7 @@ declare module 'vscode' { contextValue?: string; /** - * The optional human-readable label describing the [Comment Thread](#CommentThread) + * The optional human-readable label describing the {@link CommentThread Comment Thread} */ label?: string; @@ -12286,7 +13205,7 @@ declare module 'vscode' { } /** - * Author information of a [comment](#Comment) + * Author information of a {@link Comment} */ export interface CommentAuthorInformation { /** @@ -12301,7 +13220,7 @@ declare module 'vscode' { } /** - * Reactions of a [comment](#Comment) + * Reactions of a {@link Comment} */ export interface CommentReaction { /** @@ -12335,12 +13254,12 @@ declare module 'vscode' { body: string | MarkdownString; /** - * [Comment mode](#CommentMode) of the comment + * {@link CommentMode Comment mode} of the comment */ mode: CommentMode; /** - * The [author information](#CommentAuthorInformation) of the comment + * The {@link CommentAuthorInformation author information} of the comment */ author: CommentAuthorInformation; @@ -12365,12 +13284,12 @@ declare module 'vscode' { contextValue?: string; /** - * Optional reactions of the [comment](#Comment) + * Optional reactions of the {@link Comment} */ reactions?: CommentReaction[]; /** - * Optional label describing the [Comment](#Comment) + * Optional label describing the {@link Comment} * Label will be rendered next to authorName if exists. */ label?: string; @@ -12381,7 +13300,7 @@ declare module 'vscode' { */ export interface CommentReply { /** - * The active [comment thread](#CommentThread) + * The active {@link CommentThread comment thread} */ thread: CommentThread; @@ -12392,7 +13311,7 @@ declare module 'vscode' { } /** - * Commenting range provider for a [comment controller](#CommentController). + * Commenting range provider for a {@link CommentController comment controller}. */ export interface CommentingRangeProvider { /** @@ -12402,7 +13321,7 @@ declare module 'vscode' { } /** - * Represents a [comment controller](#CommentController)'s [options](#CommentController.options). + * Represents a {@link CommentController comment controller}'s {@link CommentController.options options}. */ export interface CommentOptions { /** @@ -12417,7 +13336,7 @@ declare module 'vscode' { } /** - * A comment controller is able to provide [comments](#CommentThread) support to the editor and + * A comment controller is able to provide {@link CommentThread comments} support to the editor and * provide users various ways to interact with comments. */ export interface CommentController { @@ -12437,31 +13356,31 @@ declare module 'vscode' { options?: CommentOptions; /** - * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. + * Optional commenting range provider. Provide a list {@link Range ranges} which support commenting to any given resource uri. * * If not provided, users can leave comments in any document opened in the editor. */ commentingRangeProvider?: CommentingRangeProvider; /** - * Create a [comment thread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) + * Create a {@link CommentThread comment thread}. The comment thread will be displayed in visible text editors (if the resource matches) * and Comments Panel once created. * * @param uri The uri of the document the thread has been created on. * @param range The range the comment thread is located within the document. * @param comments The ordered comments of the thread. */ - createCommentThread(uri: Uri, range: Range, comments: Comment[]): CommentThread; + createCommentThread(uri: Uri, range: Range, comments: readonly Comment[]): CommentThread; /** - * Optional reaction handler for creating and deleting reactions on a [comment](#Comment). + * Optional reaction handler for creating and deleting reactions on a {@link Comment}. */ reactionHandler?: (comment: Comment, reaction: CommentReaction) => Thenable; /** * Dispose this comment controller. * - * Once disposed, all [comment threads](#CommentThread) created by this comment controller will also be removed from the editor + * Once disposed, all {@link CommentThread comment threads} created by this comment controller will also be removed from the editor * and Comments Panel. */ dispose(): void; @@ -12469,11 +13388,11 @@ declare module 'vscode' { namespace comments { /** - * Creates a new [comment controller](#CommentController) instance. + * Creates a new {@link CommentController comment controller} instance. * * @param id An `id` for the comment controller. * @param label A human-readable string for the comment controller. - * @return An instance of [comment controller](#CommentController). + * @return An instance of {@link CommentController comment controller}. */ export function createCommentController(id: string, label: string): CommentController; } @@ -12501,13 +13420,13 @@ declare module 'vscode' { /** * The permissions granted by the session's access token. Available scopes - * are defined by the [AuthenticationProvider](#AuthenticationProvider). + * are defined by the {@link AuthenticationProvider}. */ - readonly scopes: ReadonlyArray; + readonly scopes: readonly string[]; } /** - * The information of an account associated with an [AuthenticationSession](#AuthenticationSession). + * The information of an account associated with an {@link AuthenticationSession}. */ export interface AuthenticationSessionAccountInformation { /** @@ -12523,7 +13442,7 @@ declare module 'vscode' { /** - * Options to be used when getting an [AuthenticationSession](#AuthenticationSession) from an [AuthenticationProvider](#AuthenticationProvider). + * Options to be used when getting an {@link AuthenticationSession} from an {@link AuthenticationProvider}. */ export interface AuthenticationGetSessionOptions { /** @@ -12544,8 +13463,8 @@ declare module 'vscode' { * Whether the existing user session preference should be cleared. * * For authentication providers that support being signed into multiple accounts at once, the user will be - * prompted to select an account to use when [getSession](#authentication.getSession) is called. This preference - * is remembered until [getSession](#authentication.getSession) is called with this flag. + * prompted to select an account to use when {@link authentication.getSession getSession} is called. This preference + * is remembered until {@link authentication.getSession getSession} is called with this flag. * * Defaults to false. */ @@ -12553,7 +13472,7 @@ declare module 'vscode' { } /** - * Basic information about an [authenticationProvider](#AuthenticationProvider) + * Basic information about an {@link AuthenticationProvider} */ export interface AuthenticationProviderInformation { /** @@ -12568,17 +13487,17 @@ declare module 'vscode' { } /** - * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. + * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. */ export interface AuthenticationSessionsChangeEvent { /** - * The [authenticationProvider](#AuthenticationProvider) that has had its sessions change. + * The {@link AuthenticationProvider} that has had its sessions change. */ readonly provider: AuthenticationProviderInformation; } /** - * Options for creating an [AuthenticationProvider](#AuthenticationProvider). + * Options for creating an {@link AuthenticationProvider}. */ export interface AuthenticationProviderOptions { /** @@ -12589,25 +13508,25 @@ declare module 'vscode' { } /** - * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. + * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. */ export interface AuthenticationProviderAuthenticationSessionsChangeEvent { /** - * The [AuthenticationSession](#AuthenticationSession)s of the [AuthenticationProvider](#AuthentiationProvider) that have been added. + * The {@link AuthenticationSession}s of the {@link AuthentiationProvider AuthenticationProvider} that have been added. */ - readonly added?: ReadonlyArray; + readonly added?: readonly AuthenticationSession[]; /** - * The [AuthenticationSession](#AuthenticationSession)s of the [AuthenticationProvider](#AuthentiationProvider) that have been removed. + * The {@link AuthenticationSession}s of the {@link AuthentiationProvider AuthenticationProvider} that have been removed. */ - readonly removed?: ReadonlyArray; + readonly removed?: readonly AuthenticationSession[]; /** - * The [AuthenticationSession](#AuthenticationSession)s of the [AuthenticationProvider](#AuthentiationProvider) that have been changed. + * The {@link AuthenticationSession}s of the {@link AuthentiationProvider AuthenticationProvider} that have been changed. * A session changes when its data excluding the id are updated. An example of this is a session refresh that results in a new * access token being set for the session. */ - readonly changed?: ReadonlyArray; + readonly changed?: readonly AuthenticationSession[]; } /** @@ -12615,7 +13534,7 @@ declare module 'vscode' { */ export interface AuthenticationProvider { /** - * An [event](#Event) which fires when the array of sessions has changed, or data + * An {@link Event} which fires when the array of sessions has changed, or data * within a session has changed. */ readonly onDidChangeSessions: Event; @@ -12626,7 +13545,7 @@ declare module 'vscode' { * these permissions, otherwise all sessions should be returned. * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: string[]): Thenable>; + getSessions(scopes?: readonly string[]): Thenable; /** * Prompts a user to login. @@ -12641,7 +13560,7 @@ declare module 'vscode' { * @param scopes A list of scopes, permissions, that the new session should be created with. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: string[]): Thenable; + createSession(scopes: readonly string[]): Thenable; /** * Removes the session corresponding to session id. @@ -12669,10 +13588,10 @@ declare module 'vscode' { * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @param options The [getSessionOptions](#GetSessionOptions) to use + * @param options The {@link GetSessionOptions} to use * @returns A thenable that resolves to an authentication session */ - export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; + export function getSession(providerId: string, scopes: readonly string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; /** * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not @@ -12684,13 +13603,13 @@ declare module 'vscode' { * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @param options The [getSessionOptions](#GetSessionOptions) to use + * @param options The {@link GetSessionOptions} to use * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions */ - export function getSession(providerId: string, scopes: string[], options?: AuthenticationGetSessionOptions): Thenable; + export function getSession(providerId: string, scopes: readonly string[], options?: AuthenticationGetSessionOptions): Thenable; /** - * An [event](#Event) which fires when the authentication sessions of an authentication provider have + * An {@link Event} which fires when the authentication sessions of an authentication provider have * been added, removed, or changed. */ export const onDidChangeSessions: Event; @@ -12705,7 +13624,7 @@ declare module 'vscode' { * @param label The human-readable name of the provider. * @param provider The authentication provider provider. * @params options Additional options for the provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; } diff --git a/lib/vscode/src/vs/vscode.proposed.d.ts b/lib/vscode/src/vs/vscode.proposed.d.ts index 50c3f1144caa..12d667c74cfb 100644 --- a/lib/vscode/src/vs/vscode.proposed.d.ts +++ b/lib/vscode/src/vs/vscode.proposed.d.ts @@ -19,16 +19,16 @@ declare module 'vscode' { //#region auth provider: https://github.com/microsoft/vscode/issues/88309 /** - * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. + * An {@link Event} which fires when an {@link AuthenticationProvider} is added or removed. */ export interface AuthenticationProvidersChangeEvent { /** - * The ids of the [authenticationProvider](#AuthenticationProvider)s that have been added. + * The ids of the {@link AuthenticationProvider}s that have been added. */ readonly added: ReadonlyArray; /** - * The ids of the [authenticationProvider](#AuthenticationProvider)s that have been removed. + * The ids of the {@link AuthenticationProvider}s that have been removed. */ readonly removed: ReadonlyArray; } @@ -80,16 +80,10 @@ declare module 'vscode' { constructor(host: string, port: number, connectionToken?: string); } - export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 - } - export interface ResolvedOptions { extensionHostEnv?: { [key: string]: string | null; }; - trust?: RemoteTrustOption; + isTrusted?: boolean; } export interface TunnelOptions { @@ -150,7 +144,24 @@ declare module 'vscode' { } export interface RemoteAuthorityResolver { + /** + * Resolve the authority part of the current opened `vscode-remote://` URI. + * + * This method will be invoked once during the startup of VS Code and again each time + * VS Code detects a disconnection. + * + * @param authority The authority part of the current opened `vscode-remote://` URI. + * @param context A context indicating if this is the first call or a subsequent call. + */ resolve(authority: string, context: RemoteAuthorityResolverContext): ResolverResult | Thenable; + + /** + * Get the canonical URI (if applicable) for a `vscode-remote://` URI. + * + * @returns The canonical URI or undefined if the uri is already canonical. + */ + getCanonicalURI?(uri: Uri): ProviderResult; + /** * Can be optionally implemented if the extension can forward ports better than the core. * When not implemented, the core will use its default forwarding logic. @@ -289,7 +300,7 @@ declare module 'vscode' { /** * A file glob pattern to match file paths against. * TODO@roblourens merge this with the GlobPattern docs/definition in vscode.d.ts. - * @see [GlobPattern](#GlobPattern) + * @see {@link GlobPattern} */ export type GlobString = string; @@ -392,13 +403,32 @@ declare module 'vscode' { Warning = 2, } + /** + * A message regarding a completed search. + */ + export interface TextSearchCompleteMessage { + /** + * Markdown text of the message. + */ + text: string, + /** + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + * Messaged are untrusted by default. + */ + trusted?: boolean, + /** + * The message type, this affects how the message will be rendered. + */ + type: TextSearchCompleteMessageType, + } + /** * Information collected when text search is complete. */ export interface TextSearchComplete { /** * Whether the search hit the limit on the maximum number of search results. - * `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results. + * `maxResults` on {@link TextSearchOptions `TextSearchOptions`} specifies the max number of results. * - If exactly that number of matches exist, this should be false. * - If `maxResults` matches are returned and more exist, this should be true. * - If search hits an internal limit which is less than `maxResults`, this should be true. @@ -411,8 +441,10 @@ declare module 'vscode' { * Messages with "Information" tyle support links in markdown syntax: * - Click to [run a command](command:workbench.action.OpenQuickPick) * - Click to [open a website](https://aka.ms) + * + * Commands may optionally return { triggerSearch: true } to signal to VS Code that the original search should run be agian. */ - message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[]; + message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; } /** @@ -545,7 +577,7 @@ declare module 'vscode' { * * @param scheme The provider will be invoked for workspace folders that have this file scheme. * @param provider The provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable; @@ -556,7 +588,7 @@ declare module 'vscode' { * * @param scheme The provider will be invoked for workspace folders that have this file scheme. * @param provider The provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTextSearchProvider(scheme: string, provider: TextSearchProvider): Disposable; } @@ -570,14 +602,14 @@ declare module 'vscode' { */ export interface FindTextInFilesOptions { /** - * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern - * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) - * to restrict the search results to a [workspace folder](#WorkspaceFolder). + * A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a {@link RelativePattern relative pattern} + * to restrict the search results to a {@link WorkspaceFolder workspace folder}. */ include?: GlobPattern; /** - * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default excludes will * apply. */ @@ -635,7 +667,7 @@ declare module 'vscode' { export namespace workspace { /** - * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * Search text in files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. * @param callback A callback, called for each result * @param token A token that can be used to signal cancellation to the underlying search engine. @@ -644,7 +676,7 @@ declare module 'vscode' { export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; /** - * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * Search text in files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. * @param options An optional set of query options. Include and exclude patterns, maxResults, etc. * @param callback A callback, called for each result @@ -674,13 +706,13 @@ declare module 'vscode' { * Registers a diff information command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. * - * Diff information commands are different from ordinary [commands](#commands.registerCommand) as + * Diff information commands are different from ordinary {@link commands.registerCommand commands} as * they only execute when there is an active diff editor when the command is called, and the diff * information has been computed. Also, the command handler of an editor command has access to * the diff information. * * @param command A unique identifier for the command. - * @param callback A command handler function with access to the [diff information](#LineChange). + * @param callback A command handler function with access to the {@link LineChange diff information}. * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ @@ -788,7 +820,7 @@ declare module 'vscode' { export interface TerminalDataWriteEvent { /** - * The [terminal](#Terminal) for which the data was written. + * The {@link Terminal} for which the data was written. */ readonly terminal: Terminal; /** @@ -811,22 +843,22 @@ declare module 'vscode' { //#region Terminal dimensions property and change event https://github.com/microsoft/vscode/issues/55718 /** - * An [event](#Event) which fires when a [Terminal](#Terminal)'s dimensions change. + * An {@link Event} which fires when a {@link Terminal}'s dimensions change. */ export interface TerminalDimensionsChangeEvent { /** - * The [terminal](#Terminal) for which the dimensions have changed. + * The {@link Terminal} for which the dimensions have changed. */ readonly terminal: Terminal; /** - * The new value for the [terminal's dimensions](#Terminal.dimensions). + * The new value for the {@link Terminal.dimensions terminal's dimensions}. */ readonly dimensions: TerminalDimensions; } export namespace window { /** - * An event which fires when the [dimensions](#Terminal.dimensions) of the terminal change. + * An event which fires when the {@link Terminal.dimensions dimensions} of the terminal change. */ export const onDidChangeTerminalDimensions: Event; } @@ -842,15 +874,26 @@ declare module 'vscode' { //#endregion - //#region Terminal initial text https://github.com/microsoft/vscode/issues/120368 + //#region Terminal name change event https://github.com/microsoft/vscode/issues/114898 - export interface TerminalOptions { + export interface Pseudoterminal { /** - * A message to write to the terminal on first launch, note that this is not sent to the - * process but, rather written directly to the terminal. This supports escape sequences such - * a setting text style. + * An event that when fired allows changing the name of the terminal. + * + * **Example:** Change the terminal name to "My new terminal". + * ```typescript + * const writeEmitter = new vscode.EventEmitter(); + * const changeNameEmitter = new vscode.EventEmitter(); + * const pty: vscode.Pseudoterminal = { + * onDidWrite: writeEmitter.event, + * onDidChangeName: changeNameEmitter.event, + * open: () => changeNameEmitter.fire('My new terminal'), + * close: () => {} + * }; + * vscode.window.createTerminal({ name: 'My terminal', pty }); + * ``` */ - readonly message?: string; + onDidChangeName?: Event; } //#endregion @@ -859,9 +902,37 @@ declare module 'vscode' { export interface TerminalOptions { /** - * A codicon ID to associate with this terminal. + * The icon path or {@link ThemeIcon} for the terminal. + */ + readonly iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + + export interface ExtensionTerminalOptions { + /** + * A themeIcon, Uri, or light and dark Uris to use as the terminal tab icon + */ + readonly iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + + //#endregion + + //#region Terminal profile provider https://github.com/microsoft/vscode/issues/120369 + + export namespace window { + /** + * Registers a provider for a contributed terminal profile. + * @param id The ID of the contributed terminal profile. + * @param provider The terminal profile provider. + */ + export function registerTerminalProfileProvider(id: string, provider: TerminalProfileProvider): Disposable; + } + + export interface TerminalProfileProvider { + /** + * Provide terminal profile options for the requested terminal. + * @param token A cancellation token that indicates the result is no longer needed. */ - readonly icon?: string; + provideProfileOptions(token: CancellationToken): ProviderResult; } //#endregion @@ -881,66 +952,29 @@ declare module 'vscode' { } //#endregion - //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 - export interface TaskPresentationOptions { - /** - * Controls whether the task is executed in a specific terminal group using split panes. - */ - group?: string; + //#region Custom Tree View Drag and Drop https://github.com/microsoft/vscode/issues/32592 + export interface TreeViewOptions { + dragAndDropController?: DragAndDropController; } - //#endregion - - //#region Status bar item with ID and Name: https://github.com/microsoft/vscode/issues/74972 - - /** - * Options to configure the status bar item. - */ - export interface StatusBarItemOptions { - - /** - * A unique identifier of the status bar item. The identifier - * is for example used to allow a user to show or hide the - * status bar item in the UI. - */ - id: string; - - /** - * A human readable name of the status bar item. The name is - * for example used as a label in the UI to show or hide the - * status bar item. - */ - name: string; - - /** - * Accessibility information used when screen reader interacts with this status bar item. - */ - accessibilityInformation?: AccessibilityInformation; + export interface DragAndDropController extends Disposable { /** - * The alignment of the status bar item. - */ - alignment?: StatusBarAlignment; - - /** - * The priority of the status bar item. Higher value means the item should - * be shown more to the left. + * Extensions should fire `TreeDataProvider.onDidChangeTreeData` for any elements that need to be refreshed. + * + * @param source + * @param target */ - priority?: number; + onDrop(source: T[], target: T): Thenable; } + //#endregion - export namespace window { - + //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 + export interface TaskPresentationOptions { /** - * Creates a status bar [item](#StatusBarItem). - * - * @param options The options of the item. If not provided, some default values - * will be assumed. For example, the `StatusBarItemOptions.id` will be the id - * of the extension and the `StatusBarItemOptions.name` will be the extension name. - * @return A new status bar item. + * Controls whether the task is executed in a specific terminal group using split panes. */ - export function createStatusBarItem(options?: StatusBarItemOptions): StatusBarItem; + group?: string; } - //#endregion //#region Custom editor move https://github.com/microsoft/vscode/issues/86146 @@ -978,235 +1012,66 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, Notebooks (misc) - - export enum NotebookCellKind { - // todo@API rename/rethink as "Markup" cell - Markdown = 1, - Code = 2 - } - - export class NotebookCellMetadata { - /** - * Whether a code cell's editor is collapsed - */ - readonly inputCollapsed?: boolean; - - /** - * Whether a code cell's outputs are collapsed - */ - readonly outputCollapsed?: boolean; - - /** - * Additional attributes of a cell metadata. - */ - readonly [key: string]: any; - - /** - * Create a new notebook cell metadata. - * - * @param inputCollapsed Whether a code cell's editor is collapsed - * @param outputCollapsed Whether a code cell's outputs are collapsed - */ - constructor(inputCollapsed?: boolean, outputCollapsed?: boolean); - - /** - * Derived a new cell metadata from this metadata. - * - * @param change An object that describes a change to this NotebookCellMetadata. - * @return A new NotebookCellMetadata that reflects the given change. Will return `this` NotebookCellMetadata if the change - * is not changing anything. - */ - with(change: { inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, [key: string]: any }): NotebookCellMetadata; - } - - export interface NotebookCellExecutionSummary { - readonly executionOrder?: number; - readonly success?: boolean; - readonly startTime?: number; - readonly endTime?: number; - } - - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md - export interface NotebookCell { - readonly index: number; - readonly notebook: NotebookDocument; - readonly kind: NotebookCellKind; - readonly document: TextDocument; - readonly metadata: NotebookCellMetadata - readonly outputs: ReadonlyArray; - - // todo@API maybe just executionSummary or lastExecutionSummary? - readonly latestExecutionSummary: NotebookCellExecutionSummary | undefined; - } - - export class NotebookDocumentMetadata { - /** - * Whether the document is trusted, default to true - * When false, insecure outputs like HTML, JavaScript, SVG will not be rendered. - */ - readonly trusted: boolean; - - /** - * Additional attributes of the document metadata. - */ - readonly [key: string]: any; - - /** - * Create a new notebook document metadata - * @param trusted Whether the document metadata is trusted. - */ - constructor(trusted?: boolean); + //#region https://github.com/microsoft/vscode/issues/124970, Cell Execution State + /** + * The execution state of a notebook cell. + */ + export enum NotebookCellExecutionState { /** - * Derived a new document metadata from this metadata. - * - * @param change An object that describes a change to this NotebookDocumentMetadata. - * @return A new NotebookDocumentMetadata that reflects the given change. Will return `this` NotebookDocumentMetadata if the change - * is not changing anything. + * The cell is idle. */ - with(change: { trusted?: boolean | null, [key: string]: any }): NotebookDocumentMetadata - } - - export interface NotebookDocumentContentOptions { + Idle = 1, /** - * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + * Execution for the cell is pending. */ - transientOutputs?: boolean; - + Pending = 2, /** - * Controls if a cell metadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + * The cell is currently executing. */ - transientCellMetadata?: { [K in keyof NotebookCellMetadata]?: boolean }; - - /** - * Controls if a document metadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. - */ - transientDocumentMetadata?: { [K in keyof NotebookDocumentMetadata]?: boolean }; + Executing = 3, } /** - * Represents a notebook. Notebooks are composed of cells and metadata. + * An event describing a cell execution state change. */ - export interface NotebookDocument { - - /** - * The associated uri for this notebook. - * - * *Note* that most notebooks use the `file`-scheme, which means they are files on disk. However, **not** all notebooks are - * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. - * - * @see [FileSystemProvider](#FileSystemProvider) - * @see [TextDocumentContentProvider](#TextDocumentContentProvider) - */ - readonly uri: Uri; - - // todo@API should we really expose this? - // todo@API should this be called `notebookType` or `notebookKind` - readonly viewType: string; - + export interface NotebookCellExecutionStateChangeEvent { /** - * The version number of this notebook (it will strictly increase after each - * change, including undo/redo). + * The {@link NotebookCell cell} for which the execution state has changed. */ - readonly version: number; + readonly cell: NotebookCell; /** - * `true` if there are unpersisted changes. + * The new execution state of the cell. */ - readonly isDirty: boolean; + readonly state: NotebookCellExecutionState; + } - /** - * Is this notebook representing an untitled file which has not been saved yet. - */ - readonly isUntitled: boolean; + export namespace notebooks { /** - * `true` if the notebook has been closed. A closed notebook isn't synchronized anymore - * and won't be re-used when the same resource is opened again. + * An {@link Event} which fires when the execution state of a cell has changed. */ - readonly isClosed: boolean; + // todo@API this is an event that is fired for a property that cells don't have and that makes me wonder + // how a correct consumer works, e.g the consumer could have been late and missed an event? + export const onDidChangeNotebookCellExecutionState: Event; + } - /** - * The [metadata](#NotebookDocumentMetadata) for this notebook. - */ - readonly metadata: NotebookDocumentMetadata; + //#endregion - /** - * The number of cells in the notebook. - */ - readonly cellCount: number; + //#region https://github.com/microsoft/vscode/issues/106744, Notebook, deprecated & misc - /** - * Return the cell at the specified index. The index will be adjusted to the notebook. - * - * @param index - The index of the cell to retrieve. - * @return A [cell](#NotebookCell). - */ - cellAt(index: number): NotebookCell; + export interface NotebookCellOutput { + id: string; + } - /** - * Get the cells of this notebook. A subset can be retrieved by providing - * a range. The range will be adjuset to the notebook. - * - * @param range A notebook range. - * @returns The cells contained by the range or all cells. - */ - getCells(range?: NotebookRange): NotebookCell[]; + //#endregion - /** - * Save the document. The saving will be handled by the corresponding content provider - * - * @return A promise that will resolve to true when the document - * has been saved. If the file was not dirty or the save failed, - * will return false. - */ - save(): Thenable; - } + //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditor /** - * A notebook range represents on ordered pair of two cell indicies. - * It is guaranteed that start is less than or equal to end. + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. */ - export class NotebookRange { - - /** - * The zero-based start index of this range. - */ - readonly start: number; - - /** - * The exclusive end index of this range (zero-based). - */ - readonly end: number; - - /** - * `true` if `start` and `end` are equal. - */ - readonly isEmpty: boolean; - - /** - * Create a new notebook range. If `start` is not - * before or equal to `end`, the values will be swapped. - * - * @param start start index - * @param end end index. - */ - constructor(start: number, end: number); - - /** - * Derive a new range for this range. - * - * @param change An object that describes a change to this range. - * @return A range that reflects the given change. Will return `this` range if the change - * is not changing anything. - */ - with(change: { start?: number, end?: number }): NotebookRange; - } - export enum NotebookEditorRevealType { /** * The range will be revealed with as little scrolling as possible. @@ -1230,10 +1095,14 @@ declare module 'vscode' { AtTop = 3 } + /** + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. + */ export interface NotebookEditor { /** * The document associated with this notebook editor. */ + //todo@api rename to notebook? readonly document: NotebookDocument; /** @@ -1264,8 +1133,9 @@ declare module 'vscode' { export interface NotebookDocumentMetadataChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the document metadata have changed. + * The {@link NotebookDocument notebook document} for which the document metadata have changed. */ + //todo@API rename to notebook? readonly document: NotebookDocument; } @@ -1281,31 +1151,36 @@ declare module 'vscode' { export interface NotebookCellsChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the cells have changed. + * The {@link NotebookDocument notebook document} for which the cells have changed. */ + //todo@API rename to notebook? readonly document: NotebookDocument; readonly changes: ReadonlyArray; } export interface NotebookCellOutputsChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the cell outputs have changed. + * The {@link NotebookDocument notebook document} for which the cell outputs have changed. */ + //todo@API remove? use cell.notebook instead? readonly document: NotebookDocument; + // NotebookCellOutputsChangeEvent.cells vs NotebookCellMetadataChangeEvent.cell readonly cells: NotebookCell[]; } export interface NotebookCellMetadataChangeEvent { /** - * The [notebook document](#NotebookDocument) for which the cell metadata have changed. + * The {@link NotebookDocument notebook document} for which the cell metadata have changed. */ + //todo@API remove? use cell.notebook instead? readonly document: NotebookDocument; + // NotebookCellOutputsChangeEvent.cells vs NotebookCellMetadataChangeEvent.cell readonly cell: NotebookCell; } export interface NotebookEditorSelectionChangeEvent { /** - * The [notebook editor](#NotebookEditor) for which the selections have changed. + * The {@link NotebookEditor notebook editor} for which the selections have changed. */ readonly notebookEditor: NotebookEditor; readonly selections: ReadonlyArray @@ -1313,40 +1188,12 @@ declare module 'vscode' { export interface NotebookEditorVisibleRangesChangeEvent { /** - * The [notebook editor](#NotebookEditor) for which the visible ranges have changed. + * The {@link NotebookEditor notebook editor} for which the visible ranges have changed. */ readonly notebookEditor: NotebookEditor; readonly visibleRanges: ReadonlyArray; } - export interface NotebookCellExecutionStateChangeEvent { - /** - * The [notebook document](#NotebookDocument) for which the cell execution state has changed. - */ - readonly document: NotebookDocument; - readonly cell: NotebookCell; - readonly executionState: NotebookCellExecutionState; - } - - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md - export class NotebookCellData { - kind: NotebookCellKind; - // todo@API better names: value? text? - source: string; - // todo@API languageId (as in TextDocument) - language: string; - outputs?: NotebookCellOutput[]; - metadata?: NotebookCellMetadata; - // todo@API just executionSummary or lastExecutionSummary - latestExecutionSummary?: NotebookCellExecutionSummary; - constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, latestExecutionSummary?: NotebookCellExecutionSummary); - } - - export class NotebookData { - cells: NotebookCellData[]; - metadata: NotebookDocumentMetadata; - constructor(cells: NotebookCellData[], metadata?: NotebookDocumentMetadata); - } export interface NotebookDocumentShowOptions { viewColumn?: ViewColumn; @@ -1355,19 +1202,12 @@ declare module 'vscode' { selections?: NotebookRange[]; } - export namespace notebook { + export namespace notebooks { - export function openNotebookDocument(uri: Uri): Thenable; - export const onDidOpenNotebookDocument: Event; - export const onDidCloseNotebookDocument: Event; export const onDidSaveNotebookDocument: Event; - /** - * All currently known notebook documents. - */ - export const notebookDocuments: ReadonlyArray; export const onDidChangeNotebookDocumentMetadata: Event; export const onDidChangeNotebookCells: Event; @@ -1392,38 +1232,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookCellOutput - - // code specific mime types - // application/x.notebook.error-traceback - // application/x.notebook.stdout - // application/x.notebook.stderr - // application/x.notebook.stream - export class NotebookCellOutputItem { - - // todo@API - // add factory functions for common mime types - // static textplain(value:string): NotebookCellOutputItem; - // static errortrace(value:any): NotebookCellOutputItem; - - mime: string; - value: unknown; - metadata?: Record; - - constructor(mime: string, value: unknown, metadata?: Record); - } - - // @jrieken transient - export class NotebookCellOutput { - id: string; - outputs: NotebookCellOutputItem[]; - metadata?: Record; - constructor(outputs: NotebookCellOutputItem[], metadata?: Record); - constructor(outputs: NotebookCellOutputItem[], id: string, metadata?: Record); - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorEdit // todo@API add NotebookEdit-type which handles all these cases? @@ -1444,26 +1252,26 @@ declare module 'vscode' { export interface WorkspaceEdit { // todo@API add NotebookEdit-type which handles all these cases? - replaceNotebookMetadata(uri: Uri, value: NotebookDocumentMetadata): void; + replaceNotebookMetadata(uri: Uri, value: { [key: string]: any }): void; replaceNotebookCells(uri: Uri, range: NotebookRange, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; - replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; + replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: { [key: string]: any }, metadata?: WorkspaceEditEntryMetadata): void; } export interface NotebookEditorEdit { - replaceMetadata(value: NotebookDocumentMetadata): void; + replaceMetadata(value: { [key: string]: any }): void; replaceCells(start: number, end: number, cells: NotebookCellData[]): void; - replaceCellMetadata(index: number, metadata: NotebookCellMetadata): void; + replaceCellMetadata(index: number, metadata: { [key: string]: any }): void; } export interface NotebookEditor { /** * Perform an edit on the notebook associated with this notebook editor. * - * The given callback-function is invoked with an [edit-builder](#NotebookEditorEdit) which must + * The given callback-function is invoked with an {@link NotebookEditorEdit edit-builder} which must * be used to make edits. Note that the edit-builder is only valid while the * callback executes. * - * @param callback A function which can create edits using an [edit-builder](#NotebookEditorEdit). + * @param callback A function which can create edits using an {@link NotebookEditorEdit edit-builder}. * @return A promise that resolves with a value indicating if the edits could be applied. */ // @jrieken REMOVE maybe @@ -1472,220 +1280,60 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookSerializer - - /** - * The notebook serializer enables the editor to open notebook files. - * - * At its core the editor only knows a [notebook data structure](#NotebookData) but not - * how that data structure is written to a file, nor how it is read from a file. The - * notebook serializer bridges this gap by deserializing bytes into notebook data and - * vice versa. - */ - export interface NotebookSerializer { - - /** - * Deserialize contents of a notebook file into the notebook data structure. - * - * @param content Contents of a notebook file. - * @param token A cancellation token. - * @return Notebook data or a thenable that resolves to such. - */ - deserializeNotebook(content: Uint8Array, token: CancellationToken): NotebookData | Thenable; - - /** - * Serialize notebook data into file contents. - * - * @param data A notebook data structure. - * @param token A cancellation token. - * @returns An array of bytes or a thenable that resolves to such. - */ - serializeNotebook(data: NotebookData, token: CancellationToken): Uint8Array | Thenable; - } - - export namespace notebook { + //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorDecorationType - // todo@API remove output when notebook marks that as transient, same for metadata - /** - * Register a [notebook serializer](#NotebookSerializer). - * - * @param notebookType A notebook. - * @param serializer A notebook serialzier. - * @param options Optional context options that define what parts of a notebook should be persisted - * @return A [disposable](#Disposable) that unregisters this serializer when being disposed. - */ - export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions): Disposable; + export interface NotebookEditor { + setDecorations(decorationType: NotebookEditorDecorationType, range: NotebookRange): void; } - //#endregion - - //#region https://github.com/microsoft/vscode/issues/119949 - - - export interface NotebookFilter { - readonly viewType?: string; - readonly scheme?: string; - readonly pattern?: GlobPattern; + export interface NotebookDecorationRenderOptions { + backgroundColor?: string | ThemeColor; + borderColor?: string | ThemeColor; + top: ThemableDecorationAttachmentRenderOptions; } - export type NotebookSelector = NotebookFilter | string | ReadonlyArray; - - export interface NotebookExecuteHandler { - /** - * @param cells The notebook cells to execute. - * @param notebook The notebook for which the execute handler is being called. - * @param controller The controller that the handler is attached to - */ - (this: NotebookController, cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController): void | Thenable + export interface NotebookEditorDecorationType { + readonly key: string; + dispose(): void; } - export interface NotebookInterruptHandler { - /** - * @param notebook The notebook for which the interrupt handler is being called. - */ - (this: NotebookController, notebook: NotebookDocument): void | Thenable; + export namespace notebooks { + export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; } - export interface NotebookController { - - /** - * The identifier of this notebook controller. - */ - readonly id: string; - - /** - * The notebook view type this controller is for. - */ - readonly viewType: string; - - /** - * An array of language identifiers that are supported by this - * controller. Any language identifier from [`getLanguages`](#languages.getLanguages) - * is possible. When falsy all languages are supported. - * - * Samples: - * ```js - * // support JavaScript and TypeScript - * myController.supportedLanguages = ['javascript', 'typescript'] - * - * // support all languages - * myController.supportedLanguages = undefined; // falsy - * myController.supportedLanguages = []; // falsy - * ``` - */ - supportedLanguages?: string[]; - - /** - * The human-readable label of this notebook controller. - */ - label: string; - - /** - * The human-readable description which is rendered less prominent. - */ - description?: string; - - /** - * The human-readable detail which is rendered less prominent. - */ - detail?: string; - - /** - * Whether this controller supports execution order so that the - * editor can render placeholders for them. - */ - // todo@API rename to supportsExecutionOrder, usesExecutionOrder - hasExecutionOrder?: boolean; - - /** - * The execute handler is invoked when the run gestures in the UI are selected, e.g Run Cell, Run All, - * Run Selection etc. - */ - executeHandler: NotebookExecuteHandler; - - /** - * The interrupt handler is invoked the interrupt all execution. This is contrary to cancellation (available via - * [`NotebookCellExecutionTask#token`](NotebookCellExecutionTask#token)) and should only be used when - * execution-level cancellation is supported - */ - interruptHandler?: NotebookInterruptHandler - - /** - * Dispose and free associated resources. - */ - dispose(): void; - - /** - * A kernel can apply to one or many notebook documents but a notebook has only one active - * kernel. This event fires whenever a notebook has been associated to a kernel or when - * that association has been removed. - */ - readonly onDidChangeNotebookAssociation: Event<{ notebook: NotebookDocument, selected: boolean }>; - - /** - * Create a cell execution task. - * - * This should be used in response to the [execution handler](#NotebookController.executeHandler) - * being calleed or when cell execution has been started else, e.g when a cell was already - * executing or when cell execution was triggered from another source. - * - * @param cell The notebook cell for which to create the execution. - * @returns A notebook cell execution. - */ - createNotebookCellExecutionTask(cell: NotebookCell): NotebookCellExecutionTask; - - // todo@API find a better name than "preloads" - // todo@API allow add, not remove - // ipc - readonly preloads: NotebookKernelPreload[]; - - /** - * An event that fires when a renderer (see `preloads`) has send a message to the controller. - */ - readonly onDidReceiveMessage: Event<{ editor: NotebookEditor, message: any }>; - - /** - * Send a message to the renderer of notebook editors. - * - * Note that only editors showing documents that are bound to this controller - * are receiving the message. - * - * @param message The message to send. - * @param editor A specific editor to send the message to. When `undefined` all applicable editors are receiving the message. - * @returns A promise that resolves to a boolean indicating if the message has been send or not. - */ - postMessage(message: any, editor?: NotebookEditor): Thenable; + //#endregion - //todo@API validate this works - asWebviewUri(localResource: Uri): Uri; + //#region https://github.com/microsoft/vscode/issues/106744, NotebookConcatTextDocument + export namespace notebooks { /** - * A controller can set affinities for specific notebook documents. This allows a controller - * to be more important for some notebooks. + * Create a document that is the concatenation of all notebook cells. By default all code-cells are included + * but a selector can be provided to narrow to down the set of cells. * - * @param notebook The notebook for which a priority is set. - * @param affinity A controller affinity + * @param notebook + * @param selector */ - updateNotebookAffinity(notebook: NotebookDocument, affinity: NotebookControllerAffinity): void; + // todo@API really needed? we didn't find a user here + export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; } - export enum NotebookControllerAffinity { - Default = 1, - Preferred = 2 - } + export interface NotebookConcatTextDocument { + readonly uri: Uri; + readonly isClosed: boolean; + dispose(): void; + readonly onDidChange: Event; + readonly version: number; + getText(): string; + getText(range: Range): string; - export namespace notebook { + offsetAt(position: Position): number; + positionAt(offset: number): Position; + validateRange(range: Range): Range; + validatePosition(position: Position): Position; - /** - * Creates a new notebook controller. - * - * @param id Extension-unique identifier of the controller - * @param viewType A notebook type for which this controller is for. - * @param label The label of the controller - * @param handler - * @param preloads - */ - export function createNotebookController(id: string, viewType: string, label: string, handler?: NotebookExecuteHandler, preloads?: NotebookKernelPreload[]): NotebookController; + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; + contains(uri: Uri): boolean; } //#endregion @@ -1727,7 +1375,7 @@ declare module 'vscode' { readonly onDidChangeNotebookContentOptions?: Event; /** - * Content providers should always use [file system providers](#FileSystemProvider) to + * Content providers should always use {@link FileSystemProvider file system providers} to * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. */ openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext, token: CancellationToken): NotebookData | Thenable; @@ -1742,232 +1390,164 @@ declare module 'vscode' { backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, token: CancellationToken): Thenable; } - export interface NotebookDocumentContentOptions { - /** - * Not ready for production or development use yet. - */ - viewOptions?: { - displayName: string; - filenamePattern: (GlobPattern | { include: GlobPattern; exclude: GlobPattern; })[]; - exclusive?: boolean; - }; - } - - export namespace notebook { + export namespace workspace { // TODO@api use NotebookDocumentFilter instead of just notebookType:string? // TODO@API options duplicates the more powerful variant on NotebookContentProvider - export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions): Disposable; - } - - //#endregion - - //#region https://github.com/microsoft/vscode/issues/106744, NotebookKernel - - // todo@API make class? - export interface NotebookKernelPreload { - provides?: string | string[]; - uri: Uri; - } - - export interface NotebookCellExecuteStartContext { - /** - * The time that execution began, in milliseconds in the Unix epoch. Used to drive the clock - * that shows for how long a cell has been running. If not given, the clock won't be shown. - */ - startTime?: number; - } - - export interface NotebookCellExecuteEndContext { - /** - * If true, a green check is shown on the cell status bar. - * If false, a red X is shown. - */ - success?: boolean; - - /** - * The time that execution finished, in milliseconds in the Unix epoch. - */ - endTime?: number; - } - - /** - * A NotebookCellExecutionTask is how the kernel modifies a notebook cell as it is executing. When - * [`createNotebookCellExecutionTask`](#notebook.createNotebookCellExecutionTask) is called, the cell - * enters the Pending state. When `start()` is called on the execution task, it enters the Executing state. When - * `end()` is called, it enters the Idle state. While in the Executing state, cell outputs can be - * modified with the methods on the run task. - * - * All outputs methods operate on this NotebookCellExecutionTask's cell by default. They optionally take - * a cellIndex parameter that allows them to modify the outputs of other cells. `appendOutputItems` and - * `replaceOutputItems` operate on the output with the given ID, which can be an output on any cell. They - * all resolve once the output edit has been applied. - */ - export interface NotebookCellExecutionTask { - readonly document: NotebookDocument; - readonly cell: NotebookCell; - - start(context?: NotebookCellExecuteStartContext): void; - executionOrder: number | undefined; - end(result?: NotebookCellExecuteEndContext): void; - readonly token: CancellationToken; - - clearOutput(cellIndex?: number): Thenable; - appendOutput(out: NotebookCellOutput | NotebookCellOutput[], cellIndex?: number): Thenable; - replaceOutput(out: NotebookCellOutput | NotebookCellOutput[], cellIndex?: number): Thenable; - appendOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], outputId: string): Thenable; - replaceOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], outputId: string): Thenable; - } - - export enum NotebookCellExecutionState { - Idle = 1, - Pending = 2, - Executing = 3, - } - - export namespace notebook { - /** @deprecated use NotebookController */ - export function createNotebookCellExecutionTask(uri: Uri, index: number, kernelId: string): NotebookCellExecutionTask | undefined; - - export const onDidChangeCellExecutionState: Event; + export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions): Disposable; } //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorDecorationType - - export interface NotebookEditor { - setDecorations(decorationType: NotebookEditorDecorationType, range: NotebookRange): void; - } - - export interface NotebookDecorationRenderOptions { - backgroundColor?: string | ThemeColor; - borderColor?: string | ThemeColor; - top: ThemableDecorationAttachmentRenderOptions; - } + //#region https://github.com/microsoft/vscode/issues/106744, LiveShare - export interface NotebookEditorDecorationType { - readonly key: string; - dispose(): void; + export interface NotebookRegistrationData { + displayName: string; + filenamePattern: (GlobPattern | { include: GlobPattern; exclude: GlobPattern; })[]; + exclusive?: boolean; } - export namespace notebook { - export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; + export namespace workspace { + // SPECIAL overload with NotebookRegistrationData + export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions, registrationData?: NotebookRegistrationData): Disposable; + // SPECIAL overload with NotebookRegistrationData + export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions, registration?: NotebookRegistrationData): Disposable; } //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookCellStatusBarItem - - /** - * Represents the alignment of status bar items. - */ - export enum NotebookCellStatusBarAlignment { + //#region https://github.com/microsoft/vscode/issues/39441 + export interface CompletionItem { /** - * Aligned to the left side. + * Will be merged into CompletionItem#label */ - Left = 1, + label2?: CompletionItemLabel; + } + export interface CompletionItemLabel { /** - * Aligned to the right side. + * The function or variable. Rendered leftmost. */ - Right = 2 - } - - export class NotebookCellStatusBarItem { - text: string; - alignment: NotebookCellStatusBarAlignment; - command?: string | Command; - tooltip?: string; - priority?: number; - accessibilityInformation?: AccessibilityInformation; - - constructor(text: string, alignment: NotebookCellStatusBarAlignment, command?: string | Command, tooltip?: string, priority?: number, accessibilityInformation?: AccessibilityInformation); - } + name: string; - interface NotebookCellStatusBarItemProvider { /** - * Implement and fire this event to signal that statusbar items have changed. The provide method will be called again. + * The parameters without the return type. Render after `name`. */ - onDidChangeCellStatusBarItems?: Event; + parameters?: string; /** - * The provider will be called when the cell scrolls into view, when its content, outputs, language, or metadata change, and when it changes execution state. + * The fully qualified name, like package name or file path. Rendered after `signature`. */ - provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): ProviderResult; - } + qualifier?: string; - export namespace notebook { - export function registerNotebookCellStatusBarItemProvider(selector: NotebookSelector, provider: NotebookCellStatusBarItemProvider): Disposable; + /** + * The return-type of a function or type of a property/variable. Rendered rightmost. + */ + type?: string; } //#endregion - //#region https://github.com/microsoft/vscode/issues/106744, NotebookConcatTextDocument + //#region @https://github.com/microsoft/vscode/issues/123601, notebook messaging - export namespace notebook { + export interface NotebookRendererMessage { /** - * Create a document that is the concatenation of all notebook cells. By default all code-cells are included - * but a selector can be provided to narrow to down the set of cells. - * - * @param notebook - * @param selector + * Editor that sent the message. */ - // todo@API really needed? we didn't find a user here - export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; - } + editor: NotebookEditor; - export interface NotebookConcatTextDocument { - readonly uri: Uri; - readonly isClosed: boolean; - dispose(): void; - readonly onDidChange: Event; - readonly version: number; - getText(): string; - getText(range: Range): string; + /** + * Message sent from the webview. + */ + message: T; + } - offsetAt(position: Position): number; - positionAt(offset: number): Position; - validateRange(range: Range): Range; - validatePosition(position: Position): Position; + /** + * Renderer messaging is used to communicate with a single renderer. It's + * returned from {@link notebooks.createRendererMessaging}. + */ + export interface NotebookRendererMessaging { + /** + * Events that fires when a message is received from a renderer. + */ + onDidReceiveMessage: Event>; - locationAt(positionOrRange: Position | Range): Location; - positionAt(location: Location): Position; - contains(uri: Uri): boolean; + /** + * Sends a message to the renderer. + * @param editor Editor to target with the message + * @param message Message to send + */ + postMessage(editor: NotebookEditor, message: TSend): void; } - //#endregion - - //#region https://github.com/microsoft/vscode/issues/39441 + /** + * Represents a script that is loaded into the notebook renderer before rendering output. This allows + * to provide and share functionality for notebook markup and notebook output renderers. + */ + export class NotebookRendererScript { - export interface CompletionItem { /** - * Will be merged into CompletionItem#label + * APIs that the preload provides to the renderer. These are matched + * against the `dependencies` and `optionalDependencies` arrays in the + * notebook renderer contribution point. */ - label2?: CompletionItemLabel; - } + provides: string[]; - export interface CompletionItemLabel { /** - * The function or variable. Rendered leftmost. + * URI for the file to preload */ - name: string; + uri: Uri; /** - * The parameters without the return type. Render after `name`. + * @param uri URI for the file to preload + * @param provides Value for the `provides` property */ - parameters?: string; + constructor(uri: Uri, provides?: string | string[]); + } + + export interface NotebookController { + + // todo@API allow add, not remove + readonly rendererScripts: NotebookRendererScript[]; /** - * The fully qualified name, like package name or file path. Rendered after `signature`. + * An event that fires when a {@link NotebookController.rendererScripts renderer script} has send a message to + * the controller. */ - qualifier?: string; + readonly onDidReceiveMessage: Event<{ editor: NotebookEditor, message: any }>; /** - * The return-type of a function or type of a property/variable. Rendered rightmost. + * Send a message to the renderer of notebook editors. + * + * Note that only editors showing documents that are bound to this controller + * are receiving the message. + * + * @param message The message to send. + * @param editor A specific editor to send the message to. When `undefined` all applicable editors are receiving the message. + * @returns A promise that resolves to a boolean indicating if the message has been send or not. */ - type?: string; + postMessage(message: any, editor?: NotebookEditor): Thenable; + + //todo@API validate this works + asWebviewUri(localResource: Uri): Uri; + } + + export namespace notebooks { + + export function createNotebookController(id: string, viewType: string, label: string, handler?: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[]): NotebookController; + + /** + * Creates a new messaging instance used to communicate with a specific + * renderer. The renderer only has access to messaging if `requiresMessaging` + * is set to `always` or `optional` in its `notebookRenderer ` contribution. + * + * @see https://github.com/microsoft/vscode/issues/123601 + * @param rendererId The renderer ID to communicate with + */ + // todo@API can ANY extension talk to renderer or is there a check that the calling extension + // declared the renderer in its package.json? + export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; } //#endregion @@ -1993,7 +1573,7 @@ declare module 'vscode' { id?: string; /** - * The icon path or [ThemeIcon](#ThemeIcon) for the timeline item. + * The icon path or {@link ThemeIcon} for the timeline item. */ iconPath?: Uri | { light: Uri; dark: Uri; } | ThemeIcon; @@ -2008,7 +1588,7 @@ declare module 'vscode' { detail?: string; /** - * The [command](#Command) that should be executed when the timeline item is selected. + * The {@link Command} that should be executed when the timeline item is selected. */ command?: Command; @@ -2046,7 +1626,7 @@ declare module 'vscode' { export interface TimelineChangeEvent { /** - * The [uri](#Uri) of the resource for which the timeline changed. + * The {@link Uri} of the resource for which the timeline changed. */ uri: Uri; @@ -2066,7 +1646,7 @@ declare module 'vscode' { }; /** - * An array of [timeline items](#TimelineItem). + * An array of {@link TimelineItem timeline items}. */ readonly items: readonly TimelineItem[]; } @@ -2102,12 +1682,12 @@ declare module 'vscode' { readonly label: string; /** - * Provide [timeline items](#TimelineItem) for a [Uri](#Uri). + * Provide {@link TimelineItem timeline items} for a {@link Uri}. * - * @param uri The [uri](#Uri) of the file to provide the timeline for. + * @param uri The {@link Uri} of the file to provide the timeline for. * @param options A set of options to determine how results should be returned. * @param token A cancellation token. - * @return The [timeline result](#TimelineResult) or a thenable that resolves to such. The lack of a result + * @return The {@link TimelineResult timeline result} or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideTimeline(uri: Uri, options: TimelineOptions, token: CancellationToken): ProviderResult; @@ -2123,7 +1703,7 @@ declare module 'vscode' { * * @param scheme A scheme or schemes that defines which documents this provider is applicable to. Can be `*` to target all documents. * @param provider A timeline provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTimelineProvider(scheme: string | string[], provider: TimelineProvider): Disposable; } @@ -2152,49 +1732,49 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/16221 - // todo@API rename to InlayHint + // todo@API Split between Inlay- and OverlayHints (InlayHint are for a position, OverlayHints for a non-empty range) // todo@API add "mini-markdown" for links and styles - // todo@API remove description - // (done:) add InlayHintKind with type, argument, etc + // (done) remove description + // (done) rename to InlayHint + // (done) add InlayHintKind with type, argument, etc export namespace languages { /** - * Register a inline hints provider. + * Register a inlay hints provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. - * @param provider An inline hints provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * @param provider An inlay hints provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. */ - export function registerInlineHintsProvider(selector: DocumentSelector, provider: InlineHintsProvider): Disposable; + export function registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable; } - export enum InlineHintKind { + export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2, } /** - * Inline hint information. + * Inlay hint information. */ - export class InlineHint { + export class InlayHint { /** * The text of the hint. */ text: string; /** - * The range of the hint. + * The position of this hint. */ - range: Range; - - kind?: InlineHintKind; - - // todo@API remove this - description?: string | MarkdownString; + position: Position; + /** + * The kind of this hint. + */ + kind?: InlayHintKind; /** * Whitespace before the hint. */ @@ -2205,29 +1785,29 @@ declare module 'vscode' { whitespaceAfter?: boolean; // todo@API make range first argument - constructor(text: string, range: Range, kind?: InlineHintKind); + constructor(text: string, position: Position, kind?: InlayHintKind); } /** - * The inline hints provider interface defines the contract between extensions and - * the inline hints feature. + * The inlay hints provider interface defines the contract between extensions and + * the inlay hints feature. */ - export interface InlineHintsProvider { + export interface InlayHintsProvider { /** - * An optional event to signal that inline hints have changed. - * @see [EventEmitter](#EventEmitter) + * An optional event to signal that inlay hints have changed. + * @see {@link EventEmitter} */ - onDidChangeInlineHints?: Event; + onDidChangeInlayHints?: Event; /** + * * @param model The document in which the command was invoked. - * @param range The range for which line hints should be computed. + * @param range The range for which inlay hints should be computed. * @param token A cancellation token. - * - * @return A list of arguments labels or a thenable that resolves to such. + * @return A list of inlay hints or a thenable that resolves to such. */ - provideInlineHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + provideInlayHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; } //#endregion @@ -2255,7 +1835,7 @@ declare module 'vscode' { export interface TextDocument { /** - * The [notebook](#NotebookDocument) that contains this document as a notebook cell or `undefined` when + * The {@link NotebookDocument notebook} that contains this document as a notebook cell or `undefined` when * the document is not contained by a notebook (this should be the more frequent case). */ notebook: NotebookDocument | undefined; @@ -2540,7 +2120,7 @@ declare module 'vscode' { /** * URI this TestItem is associated with. May be a file or directory. */ - uri: Uri; + uri?: Uri; /** * Display name describing the test item. @@ -2563,7 +2143,7 @@ declare module 'vscode' { /** * URI this TestItem is associated with. May be a file or directory. */ - readonly uri: Uri; + readonly uri?: Uri; /** * A mapping of children by ID to the associated TestItem instances. @@ -2795,7 +2375,7 @@ declare module 'vscode' { /** * URI this TestItem is associated with. May be a file or file. */ - readonly uri: Uri; + readonly uri?: Uri; /** * Display name describing the test case. @@ -3004,12 +2584,76 @@ declare module 'vscode' { //#endregion + //#region @joaomoreno https://github.com/microsoft/vscode/issues/124263 + // This API change only affects behavior and documentation, not API surface. + + namespace env { + + /** + * Resolves a uri to form that is accessible externally. + * + * #### `http:` or `https:` scheme + * + * Resolves an *external* uri, such as a `http:` or `https:` link, from where the extension is running to a + * uri to the same resource on the client machine. + * + * This is a no-op if the extension is running on the client machine. + * + * If the extension is running remotely, this function automatically establishes a port forwarding tunnel + * from the local machine to `target` on the remote and returns a local uri to the tunnel. The lifetime of + * the port forwarding tunnel is managed by VS Code and the tunnel can be closed by the user. + * + * *Note* that uris passed through `openExternal` are automatically resolved and you should not call `asExternalUri` on them. + * + * #### `vscode.env.uriScheme` + * + * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered {@link UriHandler} + * to trigger. + * + * Extensions should not make any assumptions about the resulting uri and should not alter it in anyway. + * Rather, extensions can e.g. use this uri in an authentication flow, by adding the uri as callback query + * argument to the server to authenticate to. + * + * *Note* that if the server decides to add additional query parameters to the uri (e.g. a token or secret), it + * will appear in the uri that is passed to the {@link UriHandler}. + * + * **Example** of an authentication flow: + * ```typescript + * vscode.window.registerUriHandler({ + * handleUri(uri: vscode.Uri): vscode.ProviderResult { + * if (uri.path === '/did-authenticate') { + * console.log(uri.toString()); + * } + * } + * }); + * + * const callableUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://my.extension/did-authenticate`)); + * await vscode.env.openExternal(callableUri); + * ``` + * + * *Note* that extensions should not cache the result of `asExternalUri` as the resolved uri may become invalid due to + * a system or user action โ€”ย for example, in remote cases, a user may close a port forwarding tunnel that was opened by + * `asExternalUri`. + * + * #### Any other scheme + * + * Any other scheme will be handled as if the provided URI is a workspace URI. In that case, the method will return + * a URI which, when handled, will make VS Code open the workspace. + * + * @return A uri that can be used on the client machine. + */ + export function asExternalUri(target: Uri): Thenable; + } + + //#endregion + //#region https://github.com/Microsoft/vscode/issues/15178 // TODO@API must be a class export interface OpenEditorInfo { name: string; resource: Uri; + isActive: boolean; } export namespace window { @@ -3027,47 +2671,24 @@ declare module 'vscode' { */ export interface WorkspaceTrustRequestOptions { /** - * When true, a modal dialog will be used to request workspace trust. - * When false, a badge will be displayed on the settings gear activity bar item. + * Custom message describing the user action that requires workspace + * trust. If omitted, a generic message will be displayed in the workspace + * trust request dialog. */ - readonly modal: boolean; + readonly message?: string; } export namespace workspace { /** * Prompt the user to chose whether to trust the current workspace * @param options Optional object describing the properties of the - * workspace trust request. Defaults to { modal: false } - * When using a non-modal request, the promise will return immediately. - * Any time trust is not given, it is recommended to use the - * `onDidGrantWorkspaceTrust` event to listen for trust changes. + * workspace trust request. */ export function requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable; } //#endregion - //#region https://github.com/microsoft/vscode/issues/115807 - - export interface Webview { - /** - * @param message A json serializable message to send to the webview. - * - * For older versions of vscode, if an `ArrayBuffer` is included in `message`, - * it will not be serialized properly and will not be received by the webview. - * Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently - * serialized and will also not be recreated as a typed array inside the webview. - * - * However if your extension targets vscode 1.56+ in the `engines` field of its - * `package.json` any `ArrayBuffer` values that appear in `message` will be more - * efficiently transferred to the webview and will also be recreated inside of - * the webview. - */ - postMessage(message: any): Thenable; - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/115616 @alexr00 export enum PortAutoForwardAction { Notify = 1, @@ -3077,9 +2698,23 @@ declare module 'vscode' { Ignore = 5 } - export interface PortAttributes { + export class PortAttributes { + /** + * The port number associated with this this set of attributes. + */ port: number; - autoForwardAction: PortAutoForwardAction + + /** + * The action to be taken when this port is detected for auto forwarding. + */ + autoForwardAction: PortAutoForwardAction; + + /** + * Creates a new PortAttributes object + * @param port the port number + * @param autoForwardAction the action to take when this port is detected + */ + constructor(port: number, autoForwardAction: PortAutoForwardAction); } export interface PortAttributesProvider { @@ -3108,7 +2743,7 @@ declare module 'vscode' { } //#endregion - // region https://github.com/microsoft/vscode/issues/119904 @eamodio + //#region https://github.com/microsoft/vscode/issues/119904 @eamodio export interface SourceControlInputBox { @@ -3119,4 +2754,148 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima + + export namespace languages { + /** + * Registers an inline completion provider. + */ + export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable; + } + + export interface InlineCompletionItemProvider { + /** + * Provides inline completion items for the given position and document. + * If inline completions are enabled, this method will be called whenever the user stopped typing. + * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion. + * Use `context.triggerKind` to distinguish between these scenarios. + */ + provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult | T[]>; + } + + export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + } + + /** + * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1, + } + + export class InlineCompletionList { + items: T[]; + + constructor(items: T[]); + } + + export class InlineCompletionItem { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + text: string; + + /** + * The range to replace. + * Must begin and end on the same line. + * + * Prefer replacements over insertions to avoid cache invalidation. + * Instead of reporting a completion that extends a word, + * the whole word should be replaced with the extended word. + */ + range?: Range; + + /** + * An optional {@link Command} that is executed *after* inserting this completion. + */ + command?: Command; + + constructor(text: string, range?: Range, command?: Command); + } + + + /** + * Be aware that this API will not ever be finalized. + */ + export namespace window { + export function getInlineCompletionItemController(provider: InlineCompletionItemProvider): InlineCompletionController; + } + + /** + * Be aware that this API will not ever be finalized. + */ + export interface InlineCompletionController { + /** + * Is fired when an inline completion item is shown to the user. + */ + // eslint-disable-next-line vscode-dts-event-naming + readonly onDidShowCompletionItem: Event>; + } + + /** + * Be aware that this API will not ever be finalized. + */ + export interface InlineCompletionItemDidShowEvent { + completionItem: T; + } + + //#endregion + + //#region FileSystemProvider stat readonly - https://github.com/microsoft/vscode/issues/73122 + + export enum FilePermission { + /** + * The file is readonly. + * + * *Note:* All `FileStat` from a `FileSystemProvider` that is registered with + * the option `isReadonly: true` will be implicitly handled as if `FilePermission.Readonly` + * is set. As a consequence, it is not possible to have a readonly file system provider + * registered where some `FileStat` are not readonly. + */ + Readonly = 1 + } + + /** + * The `FileStat`-type represents metadata about a file + */ + export interface FileStat { + + /** + * The permissions of the file, e.g. whether the file is readonly. + * + * *Note:* This value might be a bitmask, e.g. `FilePermission.Readonly | FilePermission.Other`. + */ + permissions?: FilePermission; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/87110 @eamodio + + export interface Memento { + + /** + * The stored keys. + */ + readonly keys: readonly string[]; + } + + //#endregion } diff --git a/lib/vscode/src/vs/workbench/api/browser/extensionHost.contribution.ts b/lib/vscode/src/vs/workbench/api/browser/extensionHost.contribution.ts index 524697c3426b..427e98ba553e 100644 --- a/lib/vscode/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/lib/vscode/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -65,6 +65,7 @@ import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadNotebookKernels'; import './mainThreadNotebookDocumentsAndEditors'; +import './mainThreadNotebookRenderers'; import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadAuthentication.ts index f2cc04cd7268..19e704dff861 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -40,6 +40,8 @@ export class MainThreadAuthenticationProvider extends Disposable { const quickPick = this.quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>(); quickPick.canSelectMany = true; + quickPick.customButton = true; + quickPick.customLabel = nls.localize('manageTrustedExtensions.cancel', 'Cancel'); const usages = readAccountUsages(this.storageService, this.id, accountName); const items = allowedExtensions.map(extension => { const usage = usages.find(usage => extension.id === usage.extensionId); @@ -68,6 +70,10 @@ export class MainThreadAuthenticationProvider extends Disposable { quickPick.dispose(); }); + quickPick.onDidCustom(() => { + quickPick.hide(); + }); + quickPick.show(); } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 00e0592719eb..413c53b81553 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { multibyteAwareBtoa } from 'vs/base/browser/dom'; -import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; @@ -16,7 +16,7 @@ import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resour import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; +import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; @@ -35,9 +35,10 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; const enum CustomEditorModelType { Custom, @@ -50,6 +51,8 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc private readonly _editorProviders = new Map(); + private readonly _editorRenameBackups = new Map(); + constructor( context: extHostProtocol.IExtHostContext, private readonly mainThreadWebview: MainThreadWebviews, @@ -60,7 +63,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -89,6 +92,9 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc }, resolveWebview: () => { throw new Error('not implemented'); } })); + + // Working copy operations + this._register(workingCopyFileService.onWillRunWorkingCopyFileOperation(async e => this.onWillRunWorkingCopyFileOperation(e))); } override dispose() { @@ -137,9 +143,18 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc webviewInput.webview.options = options; webviewInput.webview.extension = extension; + // If there's an old resource this was a move and we must resolve the backup at the same time as the webview + // This is because the backup must be ready upon model creation, and the input resolve method comes after + let backupId = webviewInput.backupId; + if (webviewInput.oldResource && !webviewInput.backupId) { + const backup = this._editorRenameBackups.get(webviewInput.oldResource.toString()); + backupId = backup?.backupId; + this._editorRenameBackups.delete(webviewInput.oldResource.toString()); + } + let modelRef: IReference; try { - modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation); + modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId }, cancellation); } catch (error) { onUnexpectedError(error); webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); @@ -252,6 +267,31 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc } return model; } + + //#region Working Copy + private async onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent) { + if (e.operation !== FileOperation.MOVE) { + return; + } + e.waitUntil((async () => { + const models = []; + for (const file of e.files) { + if (file.source) { + models.push(...(await this._customEditorService.models.getAllModels(file.source))); + } + } + for (const model of models) { + if (model instanceof MainThreadCustomEditorModel && model.isDirty()) { + const workingCopy = await model.backup(CancellationToken.None); + if (workingCopy.meta) { + // This cast is safe because we do an instanceof check above and a custom document backup data is always returned + this._editorRenameBackups.set(model.editorResource.toString(), workingCopy.meta as CustomDocumentBackupData); + } + } + } + })()); + } + //#endregion } namespace HotExitState { @@ -276,9 +316,7 @@ namespace HotExitState { } -class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { - - #isDisposed = false; +class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustomEditorModel { private _fromBackup: boolean = false; private _hotExitState: HotExitState.State = HotExitState.Allowed; @@ -288,13 +326,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private _savePoint: number = -1; private readonly _edits: Array = []; private _isDirtyFromContentChange = false; - private _inOrphaned = false; private _ongoingSave?: CancelablePromise; - private readonly _onDidChangeOrphaned = this._register(new Emitter()); - public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; - // TODO@mjbvz consider to enable a `typeId` that is specific for custom // editors. Using a distinct `typeId` allows the working copy to have // any resource (including file based resources) even if other working @@ -322,7 +356,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod untitledDocumentData = editors[0].untitledDocumentData; } const { editable } = await proxy.$createCustomDocument(resource, viewType, options.backupId, untitledDocumentData, cancellation); - return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, getEditors); + return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, !!untitledDocumentData, getEditors); } constructor( @@ -331,16 +365,17 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private readonly _editorResource: URI, fromBackup: boolean, private readonly _editable: boolean, + startDirty: boolean, private readonly _getEditors: () => CustomEditorInput[], @IFileDialogService private readonly _fileDialogService: IFileDialogService, - @IFileService private readonly _fileService: IFileService, + @IFileService fileService: IFileService, @ILabelService private readonly _labelService: ILabelService, @IUndoRedoService private readonly _undoService: IUndoRedoService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IWorkingCopyService workingCopyService: IWorkingCopyService, - @IPathService private readonly _pathService: IPathService + @IPathService private readonly _pathService: IPathService, ) { - super(); + super(MainThreadCustomEditorModel.toWorkingCopyResource(_viewType, _editorResource), fileService); this._fromBackup = fromBackup; @@ -348,7 +383,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod this._register(workingCopyService.registerWorkingCopy(this)); } - this._register(_fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + // Normally means we're re-opening an untitled file + if (startDirty) { + this._isDirtyFromContentChange = true; + } } get editorResource() { @@ -356,8 +394,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } override dispose() { - this.#isDisposed = true; - if (this._editable) { this._undoService.removeElements(this._editorResource); } @@ -369,11 +405,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod //#region IWorkingCopy - public get resource() { - // Make sure each custom editor has a unique resource for backup and edits - return MainThreadCustomEditorModel.toWorkingCopyResource(this._viewType, this._editorResource); - } - + // Make sure each custom editor has a unique resource for backup and edits private static toWorkingCopyResource(viewType: string, resource: URI) { const authority = viewType.replace(/[^a-z0-9\-_]/gi, '-'); const path = `/${multibyteAwareBtoa(resource.with({ query: null, fragment: null }).toString(true))}`; @@ -403,10 +435,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return this._fromBackup; } - public isOrphaned(): boolean { - return this._inOrphaned; - } - private isUntitled() { return this._editorResource.scheme === Schemas.untitled; } @@ -417,66 +445,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; - //#endregion - - private async onDidFilesChange(e: FileChangesEvent): Promise { - let fileEventImpactsModel = false; - let newInOrphanModeGuess: boolean | undefined; - - // If we are currently orphaned, we check if the model file was added back - if (this._inOrphaned) { - const modelFileAdded = e.contains(this.editorResource, FileChangeType.ADDED); - if (modelFileAdded) { - newInOrphanModeGuess = false; - fileEventImpactsModel = true; - } - } - - // Otherwise we check if the model file was deleted - else { - const modelFileDeleted = e.contains(this.editorResource, FileChangeType.DELETED); - if (modelFileDeleted) { - newInOrphanModeGuess = true; - fileEventImpactsModel = true; - } - } - - if (fileEventImpactsModel && this._inOrphaned !== newInOrphanModeGuess) { - let newInOrphanModeValidated: boolean = false; - if (newInOrphanModeGuess) { - // We have received reports of users seeing delete events even though the file still - // exists (network shares issue: https://github.com/microsoft/vscode/issues/13665). - // Since we do not want to mark the model as orphaned, we have to check if the - // file is really gone and not just a faulty file event. - await timeout(100); - - if (this.#isDisposed) { - newInOrphanModeValidated = true; - } else { - const exists = await this._fileService.exists(this.editorResource); - newInOrphanModeValidated = !exists; - } - } + readonly onDidChangeReadonly = Event.None; - if (this._inOrphaned !== newInOrphanModeValidated && !this.#isDisposed) { - this.setOrphaned(newInOrphanModeValidated); - } - } - } - - private setOrphaned(orphaned: boolean): void { - if (this._inOrphaned !== orphaned) { - this._inOrphaned = orphaned; - this._onDidChangeOrphaned.fire(); - } - } - - public isEditable(): boolean { - return this._editable; - } + //#endregion - public isOnReadonlyFileSystem(): boolean { - return this._fileService.hasCapability(this.editorResource, FileSystemProviderCapabilities.Readonly); + public isReadonly(): boolean { + return !this._editable; } public get viewType() { @@ -569,7 +543,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } } - public async revert(_options?: IRevertOptions) { + public async revert(options?: IRevertOptions) { if (!this._editable) { return; } @@ -578,7 +552,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return; } - this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None); + if (!options?.soft) { + this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None); + } + this.change(() => { this._isDirtyFromContentChange = false; this._fromBackup = false; @@ -650,7 +627,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return true; } else { // Since the editor is readonly, just copy the file over - await this._fileService.copy(resource, targetResource, false /* overwrite */); + await this.fileService.copy(resource, targetResource, false /* overwrite */); return true; } } @@ -698,6 +675,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod pendingState.operation.cancel(); }); + let errorMessage = ''; try { const backupId = await pendingState.operation; // Make sure state has not changed in the meantime @@ -716,12 +694,15 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod if (this._hotExitState === pendingState) { this._hotExitState = HotExitState.NotAllowed; } + if (e.message) { + errorMessage = e.message; + } } if (this._hotExitState === HotExitState.Allowed) { return backupData; } - throw new Error('Cannot back up in this state'); + throw new Error(`Cannot back up in this state: ${errorMessage}`); } } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadDebugService.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadDebugService.ts index 287bcd08305e..60c3a74c0170 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -329,7 +329,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb type: session.configuration.type, name: session.name, folderUri: session.root ? session.root.uri : undefined, - configuration: session.configuration + configuration: session.configuration, + parent: session.parentSession?.getId(), }; } } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadDocuments.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadDocuments.ts index 5b0102492cae..f49040e3ad1b 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -20,6 +20,7 @@ import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/commo import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { Emitter } from 'vs/base/common/event'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { ResourceMap } from 'vs/base/common/map'; export class BoundModelReferenceCollection { @@ -105,52 +106,39 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen private _onIsCaughtUpWithContentChanges = this._register(new Emitter()); public readonly onIsCaughtUpWithContentChanges = this._onIsCaughtUpWithContentChanges.event; - private readonly _modelService: IModelService; - private readonly _textModelResolverService: ITextModelService; - private readonly _textFileService: ITextFileService; - private readonly _fileService: IFileService; - private readonly _environmentService: IWorkbenchEnvironmentService; - private readonly _uriIdentityService: IUriIdentityService; - - private _modelTrackers: { [modelUrl: string]: ModelTracker; }; private readonly _proxy: ExtHostDocumentsShape; - private readonly _modelIsSynced = new Set(); + private readonly _modelTrackers = new ResourceMap(); + private readonly _modelIsSynced = new ResourceMap(); private readonly _modelReferenceCollection: BoundModelReferenceCollection; constructor( documentsAndEditors: MainThreadDocumentsAndEditors, extHostContext: IExtHostContext, - @IModelService modelService: IModelService, - @ITextFileService textFileService: ITextFileService, - @IFileService fileService: IFileService, - @ITextModelService textModelResolverService: ITextModelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IUriIdentityService uriIdentityService: IUriIdentityService, + @IModelService private readonly _modelService: IModelService, + @ITextFileService private readonly _textFileService: ITextFileService, + @IFileService private readonly _fileService: IFileService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IPathService private readonly _pathService: IPathService ) { super(); - this._modelService = modelService; - this._textModelResolverService = textModelResolverService; - this._textFileService = textFileService; - this._fileService = fileService; - this._environmentService = environmentService; - this._uriIdentityService = uriIdentityService; - this._modelReferenceCollection = this._register(new BoundModelReferenceCollection(uriIdentityService.extUri)); + this._modelReferenceCollection = this._register(new BoundModelReferenceCollection(_uriIdentityService.extUri)); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); this._register(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); this._register(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); - this._register(modelService.onModelModeChanged(this._onModelModeChanged, this)); + this._register(_modelService.onModelModeChanged(this._onModelModeChanged, this)); - this._register(textFileService.files.onDidSave(e => { + this._register(_textFileService.files.onDidSave(e => { if (this._shouldHandleFileEvent(e.model.resource)) { this._proxy.$acceptModelSaved(e.model.resource); } })); - this._register(textFileService.files.onDidChangeDirty(m => { + this._register(_textFileService.files.onDidChangeDirty(m => { if (this._shouldHandleFileEvent(m.resource)) { this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty()); } @@ -167,22 +155,18 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen } } })); - - this._modelTrackers = Object.create(null); } public override dispose(): void { - Object.keys(this._modelTrackers).forEach((modelUrl) => { - this._modelTrackers[modelUrl].dispose(); - }); - this._modelTrackers = Object.create(null); + dispose(this._modelTrackers.values()); + this._modelTrackers.clear(); super.dispose(); } public isCaughtUpWithContentChanges(resource: URI): boolean { - const modelUrl = resource.toString(); - if (this._modelTrackers[modelUrl]) { - return this._modelTrackers[modelUrl].isCaughtUpWithContentChanges(); + const tracker = this._modelTrackers.get(resource); + if (tracker) { + return tracker.isCaughtUpWithContentChanges(); } return true; } @@ -198,28 +182,25 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen // don't synchronize too large models return; } - const modelUrl = model.uri; - this._modelIsSynced.add(modelUrl.toString()); - this._modelTrackers[modelUrl.toString()] = new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService); + this._modelIsSynced.set(model.uri, undefined); + this._modelTrackers.set(model.uri, new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService)); } private _onModelModeChanged(event: { model: ITextModel; oldModeId: string; }): void { let { model } = event; - const modelUrl = model.uri; - if (!this._modelIsSynced.has(modelUrl.toString())) { + if (!this._modelIsSynced.has(model.uri)) { return; } this._proxy.$acceptModelModeChanged(model.uri, model.getLanguageIdentifier().language); } private _onModelRemoved(modelUrl: URI): void { - const strModelUrl = modelUrl.toString(); - if (!this._modelIsSynced.has(strModelUrl)) { + if (!this._modelIsSynced.has(modelUrl)) { return; } - this._modelIsSynced.delete(strModelUrl); - this._modelTrackers[strModelUrl].dispose(); - delete this._modelTrackers[strModelUrl]; + this._modelIsSynced.delete(modelUrl); + this._modelTrackers.get(modelUrl)!.dispose(); + this._modelTrackers.delete(modelUrl); } // --- from extension host process @@ -252,7 +233,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}`)); } else if (!extUri.isEqual(documentUri, canonicalUri)) { return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`)); - } else if (!this._modelIsSynced.has(canonicalUri.toString())) { + } else if (!this._modelIsSynced.has(canonicalUri)) { return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`)); } else { return canonicalUri; @@ -291,7 +272,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen }).then(model => { const resource = model.resource; - if (!this._modelIsSynced.has(resource.toString())) { + if (!this._modelIsSynced.has(resource)) { throw new Error(`expected URI ${resource.toString()} to have come to LIFE`); } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadEditor.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadEditor.ts index 23f062a36bc0..813c5cb92d5c 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -414,7 +414,7 @@ export class MainThreadTextEditor { if (!this._codeEditor) { return; } - this._codeEditor.setDecorations(key, ranges); + this._codeEditor.setDecorations('exthost-api', key, ranges); } public setDecorationsFast(key: string, _ranges: number[]): void { diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index 2e6aaffa5e46..c9d970717d5d 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -7,8 +7,9 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle' import { URI } from 'vs/base/common/uri'; import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { Verbosity } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, Verbosity } from 'vs/workbench/common/editor'; import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export interface ITabInfo { name: string; @@ -27,11 +28,12 @@ export class MainThreadEditorTabs { constructor( extHostContext: IExtHostContext, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IEditorService editorService: IEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); - this._editorGroupsService.groups.forEach(this._subscribeToGroup, this); + this._editorGroupsService.whenReady.then(() => this._editorGroupsService.groups.forEach(this._subscribeToGroup, this)); this._dispoables.add(_editorGroupsService.onDidAddGroup(this._subscribeToGroup, this)); this._dispoables.add(_editorGroupsService.onDidRemoveGroup(e => { const subscription = this._groups.get(e); @@ -41,6 +43,7 @@ export class MainThreadEditorTabs { this._pushEditorTabs(); } })); + this._dispoables.add(editorService.onDidActiveEditorChange(this._pushEditorTabs, this)); this._pushEditorTabs(); } @@ -69,7 +72,8 @@ export class MainThreadEditorTabs { tabs.push({ group: group.id, name: editor.getTitle(Verbosity.SHORT) ?? '', - resource: editor.resource + resource: EditorResourceAccessor.getOriginalUri(editor) ?? editor.resource, + isActive: (this._editorGroupsService.activeGroup === group) && group.isActive(editor) }); } } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadEditors.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadEditors.ts index 5587adddb267..c18de551a26d 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -26,6 +26,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { revive } from 'vs/base/common/marshalling'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { @@ -249,10 +250,10 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(editor.insertSnippet(template, ranges, opts)); } - $registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { + $registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: IDecorationRenderOptions): void { key = `${this._instanceId}-${key}`; this._registeredDecorationTypes[key] = true; - this._codeEditorService.registerDecorationType(key, options); + this._codeEditorService.registerDecorationType(`exthost-api-${extensionId}`, key, options); } $removeTextEditorDecorationType(key: string): void { diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 1a5a495c68eb..a3ab550d7eb9 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -21,6 +21,7 @@ import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensio import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { @@ -35,6 +36,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha @IHostService private readonly _hostService: IHostService, @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @ITimerService private readonly _timerService: ITimerService, + @ICommandService private readonly _commandService: ICommandService, @IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService, ) { this._extensionHostKind = extHostContext.extensionHostKind; @@ -105,15 +107,36 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha }); } else { const enablementState = this._extensionEnablementService.getEnablementState(missingInstalledDependency); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('disabledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), - actions: { - primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true, - () => this._extensionEnablementService.setEnablement([missingInstalledDependency], enablementState === EnablementState.DisabledGlobally ? EnablementState.EnabledGlobally : EnablementState.EnabledWorkspace) - .then(() => this._hostService.reload(), e => this._notificationService.error(e)))] - } - }); + if (enablementState === EnablementState.DisabledByVirtualWorkspace) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('notSupportedInWorkspace', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is not supported in the current workspace", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + }); + } else if (enablementState === EnablementState.DisabledByTrustRequirement) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('restrictedMode', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is not supported in Restricted Mode", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + actions: { + primary: [new Action('manageWorkspaceTrust', localize('manageWorkspaceTrust', "Manage Workspace Trust"), '', true, + () => this._commandService.executeCommand('workbench.trust.manage'))] + } + }); + } else if (this._extensionEnablementService.canChangeEnablement(missingInstalledDependency)) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('disabledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + actions: { + primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true, + () => this._extensionEnablementService.setEnablement([missingInstalledDependency], enablementState === EnablementState.DisabledGlobally ? EnablementState.EnabledGlobally : EnablementState.EnabledWorkspace) + .then(() => this._hostService.reload(), e => this._notificationService.error(e)))] + } + }); + } else { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('disabledDepNoAction', "Cannot activate the '{0}' extension because it depends on the '{1}' extension which is disabled.", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name), + }); + } } } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadFileSystem.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadFileSystem.ts index 286f1e540da4..0b8545ea02a7 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadFileSystem.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadFileSystem.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileStat, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files'; +import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileStat, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability, FilePermission } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -65,6 +65,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { ctime: stat.ctime, mtime: stat.mtime, size: stat.size, + permissions: MainThreadFileSystem._asFilePermission(stat), type: MainThreadFileSystem._asFileType(stat) }; }).catch(MainThreadFileSystem._handleError); @@ -95,6 +96,13 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { return res; } + private static _asFilePermission(stat: IFileStat): FilePermission | undefined { + if (stat.readonly) { + return FilePermission.Readonly; + } + return undefined; + } + $readFile(uri: UriComponents): Promise { return this._fileService.readFile(URI.revive(uri)).then(file => file.value).catch(MainThreadFileSystem._handleError); } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index fd83c6e81a06..16a59acaf5a0 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto, IdentifiableInlineCompletions, IdentifiableInlineCompletion } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -499,6 +499,21 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations.set(handle, modes.CompletionProviderRegistry.register(selector, provider)); } + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void { + const provider: modes.InlineCompletionsProvider = { + provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise => { + return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); + }, + handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion): Promise => { + return this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx); + }, + freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { + this._proxy.$freeInlineCompletionsList(handle, completions.pid); + } + }; + this._registrations.set(handle, modes.InlineCompletionsProviderRegistry.register(selector, provider)); + } + // --- parameter hints $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void { @@ -524,10 +539,10 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- inline hints - $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { - const provider = { - provideInlineHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { - const result = await this._proxy.$provideInlineHints(handle, model.uri, range, token); + $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { + provideInlayHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { + const result = await this._proxy.$provideInlayHints(handle, model.uri, range, token); return result?.hints; } }; @@ -535,13 +550,13 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha if (typeof eventHandle === 'number') { const emitter = new Emitter(); this._registrations.set(eventHandle, emitter); - provider.onDidChangeInlineHints = emitter.event; + provider.onDidChangeInlayHints = emitter.event; } - this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider)); + this._registrations.set(handle, modes.InlayHintsProviderRegistry.register(selector, provider)); } - $emitInlineHintsEvent(eventHandle: number, event?: any): void { + $emitInlayHintsEvent(eventHandle: number, event?: any): void { const obj = this._registrations.get(eventHandle); if (obj instanceof Emitter) { obj.fire(event); diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebook.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebook.ts index acc83786481b..b95e973ad7f0 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -6,13 +6,11 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { IRelativePattern } from 'vs/base/common/glob'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; -import { INotebookCellStatusBarItemProvider, INotebookExclusiveDocumentFilter, NotebookDataDto, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookDataDto, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookContentProvider, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainContext, MainThreadNotebookShape, NotebookExtensionDescription } from '../common/extHost.protocol'; @@ -43,13 +41,8 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { dispose(this._notebookSerializer.values()); } - async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: { - transientOutputs: boolean; - transientCellMetadata: TransientCellMetadata; - transientDocumentMetadata: TransientDocumentMetadata; - viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; }; - }): Promise { - let contentOptions = { transientOutputs: options.transientOutputs, transientCellMetadata: options.transientCellMetadata, transientDocumentMetadata: options.transientDocumentMetadata }; + async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, data: INotebookContributionData | undefined): Promise { + let contentOptions = { ...options }; const controller: INotebookContentProvider = { get options() { @@ -60,7 +53,6 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { contentOptions.transientDocumentMetadata = newOptions.transientDocumentMetadata; contentOptions.transientOutputs = newOptions.transientOutputs; }, - viewOptions: options.viewOptions, open: async (uri: URI, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken) => { const data = await this._proxy.$openNotebook(viewType, uri, backupId, untitledDocumentData, token); return { @@ -79,7 +71,11 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } }; - const disposable = this._notebookService.registerNotebookController(viewType, extension, controller); + const disposable = new DisposableStore(); + disposable.add(this._notebookService.registerNotebookController(viewType, extension, controller)); + if (data) { + disposable.add(this._notebookService.registerContributedNotebookType(viewType, data)); + } this._notebookProviders.set(viewType, { controller, disposable }); } @@ -104,7 +100,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } - $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions): void { + $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, data: INotebookContributionData | undefined): void { const registration = this._notebookService.registerNotebookSerializer(viewType, extension, { options, dataToNotebook: (data: VSBuffer): Promise => { @@ -114,7 +110,12 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { return this._proxy.$notebookToData(handle, data, CancellationToken.None); } }); - this._notebookSerializer.set(handle, registration); + const disposables = new DisposableStore(); + disposables.add(registration); + if (data) { + disposables.add(this._notebookService.registerContributedNotebookType(viewType, data)); + } + this._notebookSerializer.set(handle, disposables); } $unregisterNotebookSerializer(handle: number): void { @@ -129,7 +130,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } - async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, selector: NotebookSelector): Promise { + async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise { const that = this; const provider: INotebookCellStatusBarItemProvider = { async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken) { @@ -143,7 +144,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } }; }, - selector: selector + viewType }; if (typeof eventHandle === 'number') { diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index d2bf4a13f273..4fbe1ac207ff 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -9,13 +9,15 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainThreadNotebookDocumentsShape } from '../common/extHost.protocol'; import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Schemas } from 'vs/base/common/network'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { @@ -47,7 +49,6 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS this._disposables.dispose(); this._modelReferenceCollection.dispose(); dispose(this._documentEventListenersMapping.values()); - } private _handleNotebooksAdded(notebooks: readonly NotebookTextModel[]): void { @@ -114,18 +115,59 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS language: cell.language, cellKind: cell.cellKind, outputs: cell.outputs, - metadata: cell.metadata + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, }; } - async $tryOpenDocument(uriComponents: UriComponents): Promise { + async $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise { + + const info = this._notebookService.getContributedNotebookType(options.viewType); + if (!info) { + throw new Error('UNKNOWN view type: ' + options.viewType); + } + + // find a free URI for the untitled case + const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; + let uri: URI; + for (let counter = 1; ; counter++) { + let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: options.viewType }); + if (!this._notebookService.getNotebookTextModel(candidate)) { + uri = candidate; + break; + } + } + + const ref = await this._notebookEditorModelResolverService.resolve(uri, options.viewType); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + + // untitled notebooks are dirty by default + this._proxy.$acceptDirtyStateChanged(uri, true); + + // apply content changes... slightly HACKY -> this triggers a change event + if (options.content) { + ref.object.notebook.reset( + options.content.cells, + options.content.metadata, + ref.object.notebook.transientOptions + ); + } + return uri; + } + + async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri, undefined); this._modelReferenceCollection.add(uri, ref); return uri; } - async $trySaveDocument(uriComponents: UriComponents) { + async $trySaveNotebook(uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri); diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index c0f0aff2d510..be97d15f0960 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -30,7 +30,7 @@ interface INotebookAndEditorDelta { } class NotebookAndEditorState { - static compute(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): INotebookAndEditorDelta { + static delta(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): INotebookAndEditorDelta { if (!before) { return { addedDocuments: [...after.documents], @@ -107,7 +107,7 @@ export class MainThreadNotebooksAndEditors { extHostContext.set(MainContext.MainThreadNotebookDocuments, this._mainThreadNotebooks); extHostContext.set(MainContext.MainThreadNotebookEditors, this._mainThreadEditors); - this._notebookService.onDidCreateNotebookDocument(() => this._updateState(), this, this._disposables); + this._notebookService.onWillAddNotebookDocument(() => this._updateState(), this, this._disposables); this._notebookService.onDidRemoveNotebookDocument(() => this._updateState(), this, this._disposables); this._editorService.onDidActiveEditorChange(() => this._updateState(), this, this._disposables); this._editorService.onDidVisibleEditorsChange(() => this._updateState(), this, this._disposables); @@ -170,7 +170,7 @@ export class MainThreadNotebooksAndEditors { } const newState = new NotebookAndEditorState(new Set(this._notebookService.listNotebookDocuments()), editors, activeEditor, visibleEditorsMap); - this._onDelta(NotebookAndEditorState.compute(this._currentState, newState)); + this._onDelta(NotebookAndEditorState.delta(this._currentState, newState)); this._currentState = newState; } @@ -234,7 +234,8 @@ export class MainThreadNotebooksAndEditors { language: cell.language, cellKind: cell.cellKind, outputs: cell.outputs, - metadata: cell.metadata + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, })) }; } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts index a8ca5a5ac46a..704b1c392b12 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { getNotebookEditorFromEditorPane, INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookDocumentShowOptions, INotebookEditorViewColumnInfo, MainThreadNotebookEditorsShape, NotebookEditorRevealType } from '../common/extHost.protocol'; import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; @@ -124,7 +124,7 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape async $tryShowNotebookDocument(resource: UriComponents, viewType: string, options: INotebookDocumentShowOptions): Promise { - const editorOptions = new NotebookEditorOptions({ + const editorOptions: INotebookEditorOptions = { cellSelections: options.selections, preserveFocus: options.preserveFocus, pinned: options.pinned, @@ -132,8 +132,8 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape // preserve pre 1.38 behaviour to not make group active when preserveFocus: true // but make sure to restore the editor to fix https://github.com/microsoft/vscode/issues/79633 activation: options.preserveFocus ? EditorActivation.RESTORE : undefined, - override: EditorOverride.DISABLED, - }); + override: EditorOverride.DISABLED + }; const input = NotebookEditorInput.create(this._instantiationService, URI.revive(resource), viewType); const editorPane = await this._editorService.openEditor(input, editorOptions, options.position); diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 308e0964ae04..73bc0ed650e6 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -44,7 +44,7 @@ abstract class MainThreadKernel implements INotebookKernel { constructor(data: INotebookKernelDto2, private _modeService: IModeService) { this.id = data.id; - this.viewType = data.viewType; + this.viewType = data.notebookType; this.extension = data.extensionId; this.implementsInterrupt = data.supportsInterrupt ?? false; @@ -52,7 +52,7 @@ abstract class MainThreadKernel implements INotebookKernel { this.description = data.description; this.detail = data.detail; this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : _modeService.getRegisteredModes(); - this.implementsExecutionOrder = data.hasExecutionOrder ?? false; + this.implementsExecutionOrder = data.supportsExecutionOrder ?? false; this.localResourceRoot = URI.revive(data.extensionLocation); this.preloads = data.preloads?.map(u => ({ uri: URI.revive(u.uri), provides: u.provides })) ?? []; } @@ -77,8 +77,8 @@ abstract class MainThreadKernel implements INotebookKernel { this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : this._modeService.getRegisteredModes(); event.supportedLanguages = true; } - if (data.hasExecutionOrder !== undefined) { - this.implementsExecutionOrder = data.hasExecutionOrder; + if (data.supportsExecutionOrder !== undefined) { + this.implementsExecutionOrder = data.supportsExecutionOrder; event.hasExecutionOrder = true; } this._onDidChange.fire(event); @@ -122,9 +122,6 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape private _onEditorAdd(editor: INotebookEditor) { const ipcListener = editor.onDidReceiveMessage(e => { - if (e.forRenderer) { - return; - } if (!editor.hasModel()) { return; } @@ -134,7 +131,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape } for (let [handle, candidate] of this._kernels) { if (candidate[0] === selected) { - this._proxy.$acceptRendererMessage(handle, editor.getId(), e.message); + this._proxy.$acceptKernelMessageFromRenderer(handle, editor.getId(), e.message); break; } } @@ -164,11 +161,11 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape } if (editorId === undefined) { // all editors - editor.postMessage(undefined, message); + editor.postMessage(message); didSend = true; } else if (editor.getId() === editorId) { // selected editors - editor.postMessage(undefined, message); + editor.postMessage(message); didSend = true; break; } @@ -188,16 +185,16 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape await that._proxy.$cancelCells(handle, uri, handles); } }(data, this._modeService); - const registration = this._notebookKernelService.registerKernel(kernel); const listener = this._notebookKernelService.onDidChangeNotebookKernelBinding(e => { if (e.oldKernel === kernel.id) { - this._proxy.$acceptSelection(handle, e.notebook, false); + this._proxy.$acceptNotebookAssociation(handle, e.notebook, false); } else if (e.newKernel === kernel.id) { - this._proxy.$acceptSelection(handle, e.notebook, true); + this._proxy.$acceptNotebookAssociation(handle, e.notebook, true); } }); + const registration = this._notebookKernelService.registerKernel(kernel); this._kernels.set(handle, [kernel, combinedDisposable(listener, registration)]); } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts new file mode 100644 index 000000000000..5adf89ce7a73 --- /dev/null +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtHostContext, ExtHostNotebookRenderersShape, IExtHostContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; + +@extHostNamedCustomer(MainContext.MainThreadNotebookRenderers) +export class MainThreadNotebookRenderers extends Disposable implements MainThreadNotebookRenderersShape { + private readonly proxy: ExtHostNotebookRenderersShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookRendererMessagingService private readonly messaging: INotebookRendererMessagingService, + ) { + super(); + this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookRenderers); + this._register(messaging.onShouldPostMessage(e => { + this.proxy.$postRendererMessage(e.editorId, e.rendererId, e.message); + })); + } + + $postMessage(editorId: string, rendererId: string, message: unknown): void { + this.messaging.fireDidReceiveMessage(editorId, rendererId, message); + } +} diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 3ba387186db1..69f8d2cf92a7 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -27,7 +27,7 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this.entries.clear(); } - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { + $setEntry(entryId: number, id: string, name: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { // if there are icons in the text use the tooltip for the aria label let ariaLabel: string; let role: string | undefined = undefined; @@ -37,23 +37,23 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { } else { ariaLabel = getCodiconAriaLabel(text); } - const entry: IStatusbarEntry = { text, tooltip, command, color, backgroundColor, ariaLabel, role }; + const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role }; if (typeof priority === 'undefined') { priority = 0; } // Reset existing entry if alignment or priority changed - let existingEntry = this.entries.get(id); + let existingEntry = this.entries.get(entryId); if (existingEntry && (existingEntry.alignment !== alignment || existingEntry.priority !== priority)) { dispose(existingEntry.accessor); - this.entries.delete(id); + this.entries.delete(entryId); existingEntry = undefined; } // Create new entry if not existing if (!existingEntry) { - this.entries.set(id, { accessor: this.statusbarService.addEntry(entry, statusId, statusName, alignment, priority), alignment, priority }); + this.entries.set(entryId, { accessor: this.statusbarService.addEntry(entry, id, alignment, priority), alignment, priority }); } // Otherwise update diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadStorage.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadStorage.ts index 0206ff96b173..8fd15e86f9f8 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadStorage.ts @@ -67,6 +67,7 @@ export class MainThreadStorage implements MainThreadStorageShape { try { jsonValue = JSON.stringify(value); // Extension state is synced separately through extensions + // NOTE@coder: Wait for the actual storage write. await this._storageService.store(key, jsonValue, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE, StorageTarget.MACHINE); } catch (err) { return Promise.reject(err); diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadTask.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadTask.ts index c88061cf34d4..8944c4daf423 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadTask.ts @@ -702,9 +702,6 @@ export class MainThreadTask implements MainThreadTaskShape { }); }); }, - getDefaultShellAndArgs: (): Promise<{ shell: string, args: string[] | string | undefined }> => { - return Promise.resolve(this._proxy.$getDefaultShellAndArgs()); - }, findExecutable: (command: string, cwd?: string, paths?: string[]): Promise => { return this._proxy.$findExecutable(command, cwd, paths); } diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 428f24124f6e..7b52f5c75d11 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -3,22 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { StopWatch } from 'vs/base/common/stopwatch'; +import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, TerminalLaunchConfig, ITerminalDimensionsDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; +import { StopWatch } from 'vs/base/common/stopwatch'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; -import { ExtHostContext, ExtHostTerminalServiceShape, IExtHostContext, ITerminalDimensionsDto, MainContext, MainThreadTerminalServiceShape, TerminalIdentifier, TerminalLaunchConfig } from 'vs/workbench/api/common/extHost.protocol'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITerminalExternalLinkProvider, ITerminalInstance, ITerminalInstanceService, ITerminalLink, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { IAvailableProfilesRequest as IAvailableProfilesRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -30,11 +31,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape * This comes in play only when dealing with terminals created on the extension host side */ private _extHostTerminalIds = new Map(); - private _remoteAuthority: string | null; private readonly _toDispose = new DisposableStore(); private readonly _terminalProcessProxies = new Map(); + private readonly _profileProviders = new Map(); private _dataEventTracker: TerminalDataEventTracker | undefined; - private _extHostKind: ExtensionHostKind; /** * A single shared terminal link provider for the exthost. When an ext registers a link * provider, this is registered with the terminal on the renderer side and all links are @@ -43,17 +43,19 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape */ private _linkProvider: IDisposable | undefined; + private _os: OperatingSystem = OS; + constructor( - extHostContext: IExtHostContext, + private readonly _extHostContext: IExtHostContext, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService, - @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, @ILogService private readonly _logService: ILogService, + @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService ) { - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._remoteAuthority = extHostContext.remoteAuthority; + this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); // ITerminalService listeners this._toDispose.add(_terminalService.onInstanceCreated((instance) => { @@ -61,8 +63,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._onInstanceDimensionsChanged(instance); })); - this._extHostKind = extHostContext.extensionHostKind; - this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); this._toDispose.add(_terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance))); this._toDispose.add(_terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance))); @@ -70,12 +70,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e))); this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); - this._toDispose.add(_terminalService.onRequestAvailableProfiles(e => this._onRequestAvailableProfiles(e))); - - // ITerminalInstanceService listeners - if (terminalInstanceService.onRequestDefaultShellAndArgs) { - this._toDispose.add(terminalInstanceService.onRequestDefaultShellAndArgs(e => this._onRequestDefaultShellAndArgs(e))); - } // Set initial ext host state this._terminalService.terminalInstances.forEach(t => { @@ -94,7 +88,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$initEnvironmentVariableCollections(serializedCollections); } - this._terminalService.extHostReady(extHostContext.remoteAuthority!); // TODO@Tyriar: remove null assertion + remoteAgentService.getEnvironment().then(async env => { + this._os = env?.os || OS; + this._updateDefaultProfile(); + }); + this._terminalService.onDidChangeAvailableProfiles(() => this._updateDefaultProfile()); } public dispose(): void { @@ -102,6 +100,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._linkProvider?.dispose(); } + private async _updateDefaultProfile() { + const remoteAuthority = withNullAsUndefined(this._extHostContext.remoteAuthority); + const defaultProfile = this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority, os: this._os }); + const defaultAutomationProfile = this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority, os: this._os, allowAutomationShell: true }); + this._proxy.$acceptDefaultProfile(...await Promise.all([defaultProfile, defaultAutomationProfile])); + } + private _getTerminalId(id: TerminalIdentifier): number | undefined { if (typeof id === 'number') { return id; @@ -135,9 +140,19 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape : undefined, extHostTerminalId: extHostTerminalId, isFeatureTerminal: launchConfig.isFeatureTerminal, - isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal + isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, + useShellEnvironment: launchConfig.useShellEnvironment }; - const terminal = this._terminalService.createTerminal(shellLaunchConfig); + let terminal: ITerminalInstance | undefined; + if (launchConfig.isSplitTerminal) { + const activeInstance = this._terminalService.getActiveInstance(); + if (activeInstance) { + terminal = withNullAsUndefined(this._terminalService.splitInstance(activeInstance, shellLaunchConfig)); + } + } + if (!terminal) { + terminal = this._terminalService.createTerminal(shellLaunchConfig); + } this._extHostTerminalIds.set(extHostTerminalId, terminal.instanceId); } @@ -196,6 +211,18 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._terminalService.registerProcessSupport(isSupported); } + public $registerProfileProvider(id: string): void { + // Proxy profile provider requests through the extension host + this._profileProviders.set(id, this._terminalService.registerTerminalProfileProvider(id, { + createContributedTerminalProfile: async (isSplitTerminal) => this._proxy.$createContributedProfileTerminal(id, isSplitTerminal) + })); + } + + public $unregisterProfileProvider(id: string): void { + this._profileProviders.get(id)?.dispose(); + this._profileProviders.delete(id); + } + private _onActiveTerminalChanged(terminalId: number | null): void { this._proxy.$acceptActiveTerminalChanged(terminalId); } @@ -265,7 +292,15 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $sendProcessTitle(terminalId: number, title: string): void { - this._terminalProcessProxies.get(terminalId)?.emitTitle(title); + // Since title events can only come from vscode.Pseudoterminals right now, these are routed + // directly to the instance as API source events such that they will replace the initial + // `name` property provided for the Pseudoterminal. If we support showing both Api and + // Process titles at the same time we may want to pass this through as a Process source + // event. + const instance = this._terminalService.getInstanceFromId(terminalId); + if (instance) { + instance.setTitle(title, TitleEventSource.Api); + } } public $sendProcessData(terminalId: number, data: string): void { @@ -308,28 +343,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._getTerminalProcess(terminalId)?.emitLatency(sum / COUNT); } - private _isPrimaryExtHost(): boolean { - // The "primary" ext host is the remote ext host if there is one, otherwise the local - const conn = this._remoteAgentService.getConnection(); - if (conn) { - return this._remoteAuthority === conn.remoteAuthority; - } - return this._extHostKind !== ExtensionHostKind.LocalWebWorker; - } - - private async _onRequestAvailableProfiles(req: IAvailableProfilesRequest): Promise { - if (this._isPrimaryExtHost()) { - req.callback(await this._proxy.$getAvailableProfiles(req.configuredProfilesOnly)); - } - } - - private async _onRequestDefaultShellAndArgs(req: IDefaultShellAndArgsRequest): Promise { - if (this._isPrimaryExtHost()) { - const res = await this._proxy.$getDefaultShellAndArgs(req.useAutomationShell); - req.callback(res.shell, res.args); - } - } - private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy | undefined { const terminal = this._terminalProcessProxies.get(terminalId); if (!terminal) { diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadTreeViews.ts index b70ff475447f..c131a19a431b 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -31,18 +31,20 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): Promise { + async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean, canDragAndDrop: boolean }): Promise { this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options); this.extensionService.whenInstalledExtensionsRegistered().then(() => { const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); this._dataProviders.set(treeViewId, dataProvider); + const dndController = options.canDragAndDrop ? new TreeViewDragAndDropController(treeViewId, this._proxy) : undefined; const viewer = this.getTreeView(treeViewId); if (viewer) { // Order is important here. The internal tree isn't created until the dataProvider is set. // Set all other properties first! viewer.showCollapseAllAction = !!options.showCollapseAll; viewer.canSelectMany = !!options.canSelectMany; + viewer.dragAndDropController = dndController; viewer.dataProvider = dataProvider; this.registerListeners(treeViewId, viewer); this._proxy.$setVisible(treeViewId, viewer.visible); @@ -161,6 +163,16 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie type TreeItemHandle = string; +class TreeViewDragAndDropController implements ITreeViewDragAndDropController { + + constructor(private readonly treeViewId: string, + private readonly _proxy: ExtHostTreeViewsShape) { } + + onDrop(treeItem: ITreeItem[], targetTreeItem: ITreeItem): Promise { + return this._proxy.$onDrop(this.treeViewId, treeItem.map(item => item.handle), targetTreeItem.handle); + } +} + class TreeViewDataProvider implements ITreeViewDataProvider { private readonly itemsMap: Map = new Map(); diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 668e0fae130e..6fc537aa2a9d 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -41,7 +41,8 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun } private processFindingEnabled(): boolean { - return (!!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) && (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS); + return (!!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING) || this.tunnelService.hasTunnelProvider) + && (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS); } async $setRemoteTunnelService(processId: number): Promise { diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadWebviews.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadWebviews.ts index 5f5d3ac3b01b..d3e93066b572 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -14,8 +14,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { serializeMessage } from 'vs/workbench/api/common/extHostWebview'; -import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; +import { serializeWebviewMessage, deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { @@ -74,7 +73,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); disposables.add(webview.onMessage((message) => { - const serialized = serializeMessage(message.message, options); + const serialized = serializeWebviewMessage(message.message, options); this._proxy.$onMessage(handle, serialized.message, ...serialized.buffers); })); diff --git a/lib/vscode/src/vs/workbench/api/browser/mainThreadWindow.ts b/lib/vscode/src/vs/workbench/api/browser/mainThreadWindow.ts index 7f10aa334c6b..5dc01ce8c762 100644 --- a/lib/vscode/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/lib/vscode/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -60,8 +60,7 @@ export class MainThreadWindow implements MainThreadWindowShape { } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { - const uri = URI.revive(uriComponents); - const result = await this.openerService.resolveExternalUri(uri, options); + const result = await this.openerService.resolveExternalUri(URI.revive(uriComponents), options); return result.resolved; } } diff --git a/lib/vscode/src/vs/workbench/api/common/apiCommands.ts b/lib/vscode/src/vs/workbench/api/common/apiCommands.ts index da918ae31e89..65c0cab492b3 100644 --- a/lib/vscode/src/vs/workbench/api/common/apiCommands.ts +++ b/lib/vscode/src/vs/workbench/api/common/apiCommands.ts @@ -7,8 +7,6 @@ import { URI } from 'vs/base/common/uri'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; -import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IViewDescriptorService, IViewsService, ViewVisibilityState } from 'vs/workbench/common/views'; @@ -30,100 +28,6 @@ function adjustHandler(handler: (executor: ICommandsExecutor, ...args: any[]) => }; } -interface INewWindowAPICommandOptions { - reuseWindow?: boolean; - /** - * If set, defines the remoteAuthority of the new window. `null` will open a local window. - * If not set, defaults to remoteAuthority of the current window. - */ - remoteAuthority?: string | null; -} - -export class NewWindowAPICommand { - public static readonly ID = 'vscode.newWindow'; - public static execute(executor: ICommandsExecutor, options?: INewWindowAPICommandOptions): Promise { - const commandOptions: IOpenEmptyWindowOptions = { - forceReuseWindow: options && options.reuseWindow, - remoteAuthority: options && options.remoteAuthority - }; - - return executor.executeCommand('_files.newWindow', commandOptions); - } -} -CommandsRegistry.registerCommand({ - id: NewWindowAPICommand.ID, - handler: adjustHandler(NewWindowAPICommand.execute), - description: { - description: 'Opens an new window', - args: [ - ] - } -}); - -CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, uri: URI) { - const workspacesService = accessor.get(IWorkspacesService); - return workspacesService.removeRecentlyOpened([uri]); -}); - -export class RemoveFromRecentlyOpenedAPICommand { - public static readonly ID = 'vscode.removeFromRecentlyOpened'; - public static execute(executor: ICommandsExecutor, path: string | URI): Promise { - if (typeof path === 'string') { - path = path.match(/^[^:/?#]+:\/\//) ? URI.parse(path) : URI.file(path); - } else { - path = URI.revive(path); // called from extension host - } - return executor.executeCommand('_workbench.removeFromRecentlyOpened', path); - } -} -CommandsRegistry.registerCommand(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute)); - -export interface OpenIssueReporterArgs { - readonly extensionId: string; - readonly issueTitle?: string; - readonly issueBody?: string; -} - -export class OpenIssueReporter { - public static readonly ID = 'vscode.openIssueReporter'; - - public static execute(executor: ICommandsExecutor, args: string | OpenIssueReporterArgs): Promise { - const commandArgs = typeof args === 'string' - ? { extensionId: args } - : args; - return executor.executeCommand('workbench.action.openIssueReporter', commandArgs); - } -} - -interface RecentEntry { - uri: URI; - type: 'workspace' | 'folder' | 'file'; - label?: string; - remoteAuthority?: string; -} - -CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async function (accessor: ServicesAccessor, recentEntry: RecentEntry) { - const workspacesService = accessor.get(IWorkspacesService); - let recent: IRecent | undefined = undefined; - const uri = recentEntry.uri; - const label = recentEntry.label; - const remoteAuthority = recentEntry.remoteAuthority; - if (recentEntry.type === 'workspace') { - const workspace = await workspacesService.getWorkspaceIdentifier(uri); - recent = { workspace, label, remoteAuthority }; - } else if (recentEntry.type === 'folder') { - recent = { folderUri: uri, label, remoteAuthority }; - } else { - recent = { fileUri: uri, label, remoteAuthority }; - } - return workspacesService.addRecentlyOpened([recent]); -}); - -CommandsRegistry.registerCommand('_workbench.getRecentlyOpened', async function (accessor: ServicesAccessor) { - const workspacesService = accessor.get(IWorkspacesService); - return workspacesService.getRecentlyOpened(); -}); - CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (accessor: ServicesAccessor, level: number) { const logService = accessor.get(ILogService); const environmentService = accessor.get(IEnvironmentService); diff --git a/lib/vscode/src/vs/workbench/api/common/extHost.api.impl.ts b/lib/vscode/src/vs/workbench/api/common/extHost.api.impl.ts index 14757bf88e99..e4f4400b6c0a 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -29,7 +28,7 @@ import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocu import { Extension, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; -import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostLanguageFeatures, InlineCompletionController } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; import { ExtHostMessageService } from 'vs/workbench/api/common/extHostMessageService'; import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; @@ -82,11 +81,13 @@ import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSyste import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener'; import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; -import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; -import { RemoteTrustOption } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; +import { Schemas } from 'vs/base/common/network'; +import { matchesScheme } from 'vs/platform/opener/common/opener'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -114,6 +115,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); const extHostSecretState = accessor.get(IExtHostSecretState); + const extHostEditorTabs = accessor.get(IExtHostEditorTabs); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -126,6 +128,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); rpcProtocol.set(ExtHostContext.ExtHostTelemetry, extHostTelemetry); + rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, extHostEditorTabs); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -138,16 +141,16 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService)); // manually create and register addressable instances - const extHostEditorTabs = rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, new ExtHostEditorTabs()); const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostLogService, extensionStoragePaths)); - const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook)); + const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostLogService)); + const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); - const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); + const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); @@ -160,7 +163,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, { remote: initData.remote }, extHostWorkspace, extHostLogService, extHostApiDeprecation)); const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); @@ -216,7 +219,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I })(); const authentication: typeof vscode.authentication = { - getSession(providerId: string, scopes: string[], options?: vscode.AuthenticationGetSessionOptions) { + getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, get onDidChangeSessions(): Event { @@ -295,7 +298,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get uriScheme() { return initData.environment.appUriScheme; }, get clipboard(): vscode.Clipboard { return extHostClipboard.value; }, get shell() { - return extHostTerminalService.getDefaultShell(false, configProvider); + return extHostTerminalService.getDefaultShell(false); }, get isTelemetryEnabled() { return extHostTelemetry.getTelemetryEnabled(); @@ -313,12 +316,26 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I allowContributedOpeners: options?.allowContributedOpeners, }); }, - asExternalUri(uri: URI) { + async asExternalUri(uri: URI) { if (uri.scheme === initData.environment.appUriScheme) { return extHostUrls.createAppUri(uri); } - return extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.authority }); + const isHttp = matchesScheme(uri, Schemas.http) || matchesScheme(uri, Schemas.https); + + if (!isHttp) { + checkProposedApiEnabled(extension); // https://github.com/microsoft/vscode/issues/124263 + } + + try { + return await extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.authority }); + } catch (err) { + if (isHttp) { + return uri; + } + + throw err; + } }, get remoteName() { return getRemoteName(initData.remote.authority); @@ -479,6 +496,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerCompletionItemProvider(extension, checkSelector(selector), provider, triggerCharacters); }, + registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider); + }, registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentLinkProvider(extension, checkSelector(selector), provider); }, @@ -501,9 +522,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostLanguages.tokenAtPosition(doc, pos); }, - registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + registerInlayHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlayHintsProvider): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider); + return extHostLanguageFeatures.registerInlayHintsProvider(extension, selector, provider); } }; @@ -529,7 +550,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostEditors.showTextDocument(document, columnOrOptions, preserveFocus); }, createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { - return extHostEditors.createTextEditorDecorationType(options); + return extHostEditors.createTextEditorDecorationType(extension, options); }, onDidChangeActiveTextEditor(listener, thisArg?, disposables?) { return extHostEditors.onDidChangeActiveTextEditor(listener, thisArg, disposables); @@ -596,25 +617,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showSaveDialog(options) { return extHostDialogs.showSaveDialog(options); }, - createStatusBarItem(alignmentOrOptions?: vscode.StatusBarAlignment | vscode.StatusBarItemOptions, priority?: number): vscode.StatusBarItem { - let id: string; - let name: string; + createStatusBarItem(alignmentOrId?: vscode.StatusBarAlignment | string, priorityOrAlignment?: number | vscode.StatusBarAlignment, priorityArg?: number): vscode.StatusBarItem { + let id: string | undefined; let alignment: number | undefined; - let accessibilityInformation: vscode.AccessibilityInformation | undefined = undefined; + let priority: number | undefined; - if (alignmentOrOptions && typeof alignmentOrOptions !== 'number') { - id = alignmentOrOptions.id; - name = alignmentOrOptions.name; - alignment = alignmentOrOptions.alignment; - priority = alignmentOrOptions.priority; - accessibilityInformation = alignmentOrOptions.accessibilityInformation; + if (typeof alignmentOrId === 'string') { + id = alignmentOrId; + alignment = priorityOrAlignment; + priority = priorityArg; } else { - id = extension.identifier.value; - name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); - alignment = alignmentOrOptions; + alignment = alignmentOrId; + priority = priorityOrAlignment; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); + return extHostStatusBar.createStatusBarEntry(extension, id, alignment, priority); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); @@ -643,18 +660,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); } - if (nameOrOptions.message) { - checkProposedApiEnabled(extension); - } - if (nameOrOptions.icon) { + if (nameOrOptions.iconPath) { checkProposedApiEnabled(extension); } return extHostTerminalService.createTerminalFromOptions(nameOrOptions); } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, - registerTerminalLinkProvider(handler: vscode.TerminalLinkProvider): vscode.Disposable { - return extHostTerminalService.registerLinkProvider(handler); + registerTerminalLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable { + return extHostTerminalService.registerLinkProvider(provider); + }, + registerTerminalProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable { + return extHostTerminalService.registerProfileProvider(id, provider); }, registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension); @@ -732,6 +749,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeOpenEditors() { checkProposedApiEnabled(extension); return extHostEditorTabs.onDidChangeTabs; + }, + getInlineCompletionItemController(provider: vscode.InlineCompletionItemProvider): vscode.InlineCompletionController { + checkProposedApiEnabled(extension); + return InlineCompletionController.get(provider); } }; @@ -843,6 +864,34 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onWillSaveTextDocument: (listener, thisArgs?, disposables?) => { return extHostDocumentSaveParticipant.getOnWillSaveTextDocumentEvent(extension)(listener, thisArgs, disposables); }, + get notebookDocuments(): vscode.NotebookDocument[] { + return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); + }, + async openNotebookDocument(uriOrType?: URI | string, content?: vscode.NotebookData) { + let uri: URI; + if (URI.isUri(uriOrType)) { + uri = uriOrType; + await extHostNotebook.openNotebookDocument(uriOrType); + } else if (typeof uriOrType === 'string') { + uri = URI.revive(await extHostNotebook.createNotebookDocument({ viewType: uriOrType, content })); + } else { + throw new Error('Invalid arguments'); + } + return extHostNotebook.getNotebookDocument(uri).apiNotebook; + }, + get onDidOpenNotebookDocument(): Event { + return extHostNotebook.onDidOpenNotebookDocument; + }, + get onDidCloseNotebookDocument(): Event { + return extHostNotebook.onDidCloseNotebookDocument; + }, + registerNotebookSerializer(viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) { + return extHostNotebook.registerNotebookSerializer(extension, viewType, serializer, options, extension.enableProposedApi ? registration : undefined); + }, + registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) => { + checkProposedApiEnabled(extension); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options, extension.enableProposedApi ? registration : undefined); + }, onDidChangeConfiguration: (listener: (_: any) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { return configProvider.onDidChangeConfiguration(listener, thisArgs, disposables); }, @@ -860,7 +909,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTask.registerTaskProvider(extension, type, provider); }, registerFileSystemProvider(scheme, provider, options) { - return extHostFileSystem.registerFileSystemProvider(extension.identifier, scheme, provider, options); + return extHostFileSystem.registerFileSystemProvider(extension.identifier, scheme, provider, options, extension.enableProposedApi); }, get fs() { return extHostConsumerFileSystem.value; @@ -1000,10 +1049,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I stopDebugging(session?: vscode.DebugSession) { return extHostDebugService.stopDebugging(session); }, - addBreakpoints(breakpoints: vscode.Breakpoint[]) { + addBreakpoints(breakpoints: readonly vscode.Breakpoint[]) { return extHostDebugService.addBreakpoints(breakpoints); }, - removeBreakpoints(breakpoints: vscode.Breakpoint[]) { + removeBreakpoints(breakpoints: readonly vscode.Breakpoint[]) { return extHostDebugService.removeBreakpoints(breakpoints); }, asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri { @@ -1039,43 +1088,25 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: notebook - const notebook: typeof vscode.notebook = { - openNotebookDocument: (uriComponents) => { - checkProposedApiEnabled(extension); - return extHostNotebook.openNotebookDocument(uriComponents); + const notebooks: typeof vscode.notebooks = { + createNotebookController(id: string, notebookType: string, label: string, handler?, rendererScripts?: vscode.NotebookRendererScript[]) { + return extHostNotebookKernels.createNotebookController(extension, id, notebookType, label, handler, extension.enableProposedApi ? rendererScripts : undefined); }, - get onDidOpenNotebookDocument(): Event { - checkProposedApiEnabled(extension); - return extHostNotebook.onDidOpenNotebookDocument; - }, - get onDidCloseNotebookDocument(): Event { - checkProposedApiEnabled(extension); - return extHostNotebook.onDidCloseNotebookDocument; + registerNotebookCellStatusBarItemProvider: (notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) => { + return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, notebookType, provider); }, get onDidSaveNotebookDocument(): Event { checkProposedApiEnabled(extension); return extHostNotebook.onDidSaveNotebookDocument; }, - get notebookDocuments(): vscode.NotebookDocument[] { - checkProposedApiEnabled(extension); - return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); - }, - registerNotebookSerializer(viewType, serializer, options) { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookSerializer(extension, viewType, serializer, options); - }, - registerNotebookContentProvider: (viewType, provider, options) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); - }, - registerNotebookCellStatusBarItemProvider: (selector: vscode.NotebookSelector, provider: vscode.NotebookCellStatusBarItemProvider) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, selector, provider); - }, createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { checkProposedApiEnabled(extension); return extHostNotebook.createNotebookEditorDecorationType(options); }, + createRendererMessaging(rendererId) { + checkProposedApiEnabled(extension); + return extHostNotebookRenderers.createRendererMessaging(rendererId); + }, onDidChangeNotebookDocumentMetadata(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookDocumentMetadata(listener, thisArgs, disposables); @@ -1084,7 +1115,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); }, - onDidChangeCellExecutionState(listener, thisArgs?, disposables?) { + onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, @@ -1100,14 +1131,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); }, - createNotebookCellExecutionTask(uri: vscode.Uri, index: number, kernelId: string): vscode.NotebookCellExecutionTask | undefined { - checkProposedApiEnabled(extension); - return extHostNotebook.createNotebookCellExecution(uri, index, kernelId); - }, - createNotebookController(id, viewType, label, executeHandler, preloads) { - checkProposedApiEnabled(extension); - return extHostNotebookKernels.createNotebookController(extension, id, viewType, label, executeHandler, preloads); - } }; return { @@ -1120,7 +1143,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I env, extensions, languages, - notebook, + notebooks, scm, tasks, test, @@ -1173,6 +1196,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlineValueText: extHostTypes.InlineValueText, InlineValueVariableLookup: extHostTypes.InlineValueVariableLookup, InlineValueEvaluatableExpression: extHostTypes.InlineValueEvaluatableExpression, + InlineCompletionTriggerKind: extHostTypes.InlineCompletionTriggerKind, EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, @@ -1181,9 +1205,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FileDecoration: extHostTypes.FileDecoration, FileSystemError: extHostTypes.FileSystemError, FileType: files.FileType, + FilePermission: files.FilePermission, FoldingRange: extHostTypes.FoldingRange, FoldingRangeKind: extHostTypes.FoldingRangeKind, FunctionBreakpoint: extHostTypes.FunctionBreakpoint, + InlineCompletionItem: extHostTypes.InlineSuggestion, + InlineCompletionList: extHostTypes.InlineSuggestions, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, Location: extHostTypes.Location, @@ -1236,10 +1263,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, // proposed api types - InlineHint: extHostTypes.InlineHint, - InlineHintKind: extHostTypes.InlineHintKind, + InlayHint: extHostTypes.InlayHint, + InlayHintKind: extHostTypes.InlayHintKind, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, - RemoteTrustOption: RemoteTrustOption, ResolvedAuthority: extHostTypes.ResolvedAuthority, SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, ExtensionRuntime: extHostTypes.ExtensionRuntime, @@ -1247,16 +1273,16 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NotebookRange: extHostTypes.NotebookRange, NotebookCellKind: extHostTypes.NotebookCellKind, NotebookCellExecutionState: extHostTypes.NotebookCellExecutionState, - NotebookDocumentMetadata: extHostTypes.NotebookDocumentMetadata, - NotebookCellMetadata: extHostTypes.NotebookCellMetadata, NotebookCellData: extHostTypes.NotebookCellData, NotebookData: extHostTypes.NotebookData, + NotebookRendererScript: extHostTypes.NotebookRendererScript, NotebookCellStatusBarAlignment: extHostTypes.NotebookCellStatusBarAlignment, NotebookEditorRevealType: extHostTypes.NotebookEditorRevealType, NotebookCellOutput: extHostTypes.NotebookCellOutput, NotebookCellOutputItem: extHostTypes.NotebookCellOutputItem, NotebookCellStatusBarItem: extHostTypes.NotebookCellStatusBarItem, NotebookControllerAffinity: extHostTypes.NotebookControllerAffinity, + PortAttributes: extHostTypes.PortAttributes, LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestItemStatus: extHostTypes.TestItemStatus, TestResultState: extHostTypes.TestResultState, diff --git a/lib/vscode/src/vs/workbench/api/common/extHost.common.services.ts b/lib/vscode/src/vs/workbench/api/common/extHost.common.services.ts index bf894f0cfb4b..1ff423101e9d 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHost.common.services.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHost.common.services.ts @@ -23,6 +23,7 @@ import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbe import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; +import { ExtHostEditorTabs, IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); @@ -43,3 +44,4 @@ registerSingleton(IExtHostWindow, ExtHostWindow); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostSecretState, ExtHostSecretState); registerSingleton(IExtHostTelemetry, ExtHostTelemetry); +registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs); diff --git a/lib/vscode/src/vs/workbench/api/common/extHost.protocol.ts b/lib/vscode/src/vs/workbench/api/common/extHost.protocol.ts index f1a42cd27f37..a6bd90d90b8a 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHost.protocol.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHost.protocol.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as performance from 'vs/base/common/performance'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; @@ -11,7 +10,10 @@ import { SerializedError } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import * as performance from 'vs/base/common/performance'; import Severity from 'vs/base/common/severity'; +import { Dto } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { RenderLineNumbersType, TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; @@ -22,8 +24,9 @@ import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model' import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import * as modes from 'vs/editor/common/modes'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationData, IConfigurationChange, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as files from 'vs/platform/files/common/files'; @@ -32,39 +35,34 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IMarkerData } from 'vs/platform/markers/common/markers'; import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; import * as quickInput from 'vs/platform/quickinput/common/quickInput'; -import { RemoteAuthorityResolverErrorCode, ResolverResult, TunnelDescription, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar'; +import { IRemoteConnectionData, RemoteAuthorityResolverErrorCode, ResolverResult, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; +import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; +import { DebugConfigurationProviderTriggerKind, TestResultState } from 'vs/workbench/api/common/extHostTypes'; import * as tasks from 'vs/workbench/api/common/shared/tasks'; +import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; +import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; +import { CellKind, ICellEditOperation, IImmediateCellEditOperation, IMainCellDto, INotebookCellStatusBarItem, INotebookContributionData, INotebookDecorationRenderOptions, IOutputDto, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentMetadata, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; -import { ActivationKind, MissingExtensionDependency, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { ExtensionRunTestsRequest, InternalTestItem, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ActivationKind, ExtensionHostKind, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import * as search from 'vs/workbench/services/search/common/search'; -import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; -import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; -import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, ProvidedPortAttributes } from 'vs/platform/remote/common/tunnel'; -import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; -import { revive } from 'vs/base/common/marshalling'; -import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, TransientCellMetadata, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto, TransientOptions, IImmediateCellEditOperation, INotebookCellStatusBarItem, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { Dto } from 'vs/base/common/types'; -import { DebugConfigurationProviderTriggerKind, TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; -import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage, ITestItem, ITestRunTask, ExtensionRunTestsRequest } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; -import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; -import { ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; -import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; -import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; +import * as search from 'vs/workbench/services/search/common/search'; +import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -76,8 +74,6 @@ export interface IEnvironment { extensionTestsLocationURI?: URI; globalStorageHome: URI; workspaceStorageHome: URI; - webviewResourceRoot: string; - webviewCspSource: string; useHostProxy?: boolean; } @@ -171,7 +167,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $unregisterAuthenticationProvider(id: string): void; $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; - $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; + $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; $removeSession(providerId: string, sessionId: string): Promise; } @@ -274,7 +270,7 @@ export interface MainThreadBulkEditsShape extends IDisposable { export interface MainThreadTextEditorsShape extends IDisposable { $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise; - $registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void; + $registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: editorCommon.IDecorationRenderOptions): void; $removeTextEditorDecorationType(key: string): void; $tryShowEditor(id: string, position: EditorGroupColumn): Promise; $tryHideEditor(id: string): Promise; @@ -289,7 +285,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean; }): Promise; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean, canDragAndDrop: boolean; }): Promise; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem; }): Promise; $reveal(treeViewId: string, itemInfo: { item: ITreeItem, parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise; $setMessage(treeViewId: string, message: string): void; @@ -371,6 +367,14 @@ export interface ISignatureHelpProviderMetadataDto { readonly retriggerCharacters: readonly string[]; } +export interface IdentifiableInlineCompletions extends modes.InlineCompletions { + pid: number; +} + +export interface IdentifiableInlineCompletion extends modes.InlineCompletion { + idx: number; +} + export interface MainThreadLanguageFeaturesShape extends IDisposable { $unregister(handle: number): void; $registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], label: string): void; @@ -397,9 +401,10 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; - $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; - $emitInlineHintsEvent(eventHandle: number, event?: any): void; + $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitInlayHintsEvent(eventHandle: number, event?: any): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; @@ -458,7 +463,7 @@ export interface TerminalLaunchConfig { shellArgs?: string[] | string; cwd?: string | UriComponents; env?: ITerminalEnvironment; - icon?: string; + icon?: URI | { light: URI; dark: URI } | ThemeIcon; initialText?: string; waitOnExit?: boolean; strictEnv?: boolean; @@ -466,6 +471,8 @@ export interface TerminalLaunchConfig { isExtensionCustomPtyTerminal?: boolean; isFeatureTerminal?: boolean; isExtensionOwnedTerminal?: boolean; + useShellEnvironment?: boolean; + isSplitTerminal?: boolean; } export interface MainThreadTerminalServiceShape extends IDisposable { @@ -479,6 +486,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $startLinkProvider(): void; $stopLinkProvider(): void; $registerProcessSupport(isSupported: boolean): void; + $registerProfileProvider(id: string): void; + $unregisterProfileProvider(id: string): void; $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process @@ -623,7 +632,8 @@ export interface MainThreadEditorTabsShape extends IDisposable { export interface IEditorTabDto { group: number; name: string; - resource: UriComponents + resource: UriComponents; + isActive: boolean; } export interface IExtHostEditorTabsShape { @@ -647,7 +657,6 @@ export interface WebviewExtensionDescription { export interface NotebookExtensionDescription { readonly id: ExtensionIdentifier; readonly location: UriComponents; - readonly description?: string; } export enum WebviewEditorCapabilities { @@ -708,7 +717,7 @@ export interface WebviewMessageArrayBufferReference { export interface MainThreadWebviewsShape extends IDisposable { $setHtml(handle: WebviewHandle, value: string): void; $setOptions(handle: WebviewHandle, options: IWebviewOptions): void; - $postMessage(handle: WebviewHandle, value: any, ...buffers: VSBuffer[]): Promise + $postMessage(handle: WebviewHandle, value: string, ...buffers: VSBuffer[]): Promise } export interface MainThreadWebviewPanelsShape extends IDisposable { @@ -818,11 +827,6 @@ export interface ExtHostWebviewViewsShape { $disposeWebviewView(webviewHandle: WebviewHandle): void; } -export enum CellKind { - Markdown = 1, - Code = 2 -} - export enum CellOutputKind { Text = 1, Error = 2, @@ -873,19 +877,14 @@ export interface INotebookCellStatusBarListDto { } export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: { - transientOutputs: boolean; - transientCellMetadata: TransientCellMetadata; - transientDocumentMetadata: TransientDocumentMetadata; - viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; }; - }): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, registration: INotebookContributionData | undefined): Promise; $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata; }): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions): void; + $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, registration: INotebookContributionData | undefined): void; $unregisterNotebookSerializer(handle: number): void; - $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, selector: NotebookSelector): Promise; + $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise; $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise; $emitCellStatusBarEvent(eventHandle: number): void; } @@ -900,14 +899,15 @@ export interface MainThreadNotebookEditorsShape extends IDisposable { } export interface MainThreadNotebookDocumentsShape extends IDisposable { - $tryOpenDocument(uriComponents: UriComponents): Promise; - $trySaveDocument(uri: UriComponents): Promise; + $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise; + $tryOpenNotebook(uriComponents: UriComponents): Promise; + $trySaveNotebook(uri: UriComponents): Promise; $applyEdits(resource: UriComponents, edits: IImmediateCellEditOperation[], computeUndoRedo?: boolean): Promise; } export interface INotebookKernelDto2 { id: string; - viewType: string; + notebookType: string; extensionId: ExtensionIdentifier; extensionLocation: UriComponents; label: string; @@ -915,7 +915,7 @@ export interface INotebookKernelDto2 { description?: string; supportedLanguages?: string[]; supportsInterrupt?: boolean; - hasExecutionOrder?: boolean; + supportsExecutionOrder?: boolean; preloads?: { uri: UriComponents; provides: string[] }[]; } @@ -927,6 +927,10 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $updateNotebookPriority(handle: number, uri: UriComponents, value: number | undefined): void; } +export interface MainThreadNotebookRenderersShape extends IDisposable { + $postMessage(editorId: string, rendererId: string, message: unknown): void; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1229,6 +1233,7 @@ export interface ExtHostDocumentsAndEditorsShape { export interface ExtHostTreeViewsShape { $getChildren(treeViewId: string, treeItemHandle?: string): Promise; + $onDrop(treeViewId: string, treeItemHandle: string[], newParentTreeItemHandle: string): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; @@ -1305,6 +1310,7 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; + $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; $extensionTestsExecute(): Promise; $extensionTestsExit(code: number): Promise; @@ -1443,17 +1449,16 @@ export interface ISignatureHelpContextDto { readonly activeSignatureHelp?: ISignatureHelpDto; } -export interface IInlineHintDto { +export interface IInlayHintDto { text: string; - range: IRange; - kind: modes.InlineHintKind; + position: IPosition; + kind: modes.InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; - hoverMessage?: string; } -export interface IInlineHintsDto { - hints: IInlineHintDto[] +export interface IInlayHintsDto { + hints: IInlayHintDto[] } export interface ILocationDto { @@ -1646,9 +1651,12 @@ export interface ExtHostLanguageFeaturesShape { $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; + $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise; + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void; + $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; - $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise + $provideInlayHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveDocumentLink(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseDocumentLinks(handle: number, id: number): void; @@ -1679,11 +1687,6 @@ export interface ExtHostTelemetryShape { $onDidChangeTelemetryEnabled(enabled: boolean): void; } -export interface IShellAndArgsDto { - shell: string; - args: string[] | string | undefined; -} - export interface ITerminalLinkDto { /** The ID of the link to enable activation and disposal. */ id: number; @@ -1717,11 +1720,11 @@ export interface ExtHostTerminalServiceShape { $acceptProcessRequestInitialCwd(id: number): void; $acceptProcessRequestCwd(id: number): void; $acceptProcessRequestLatency(id: number): number; - $getAvailableProfiles(configuredProfilesOnly: boolean): Promise; - $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; $provideLinks(id: number, line: string): Promise; $activateLink(id: number, linkId: number): void; $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void; + $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void; + $createContributedProfileTerminal(id: string, isSplitTerminal: boolean): Promise; } export interface ExtHostSCMShape { @@ -1740,7 +1743,6 @@ export interface ExtHostTaskShape { $onDidEndTaskProcess(value: tasks.TaskProcessEndedDTO): void; $OnDidEndTask(execution: tasks.TaskExecutionDTO): void; $resolveVariables(workspaceFolder: UriComponents, toResolve: { process?: { name: string; cwd?: string; }, variables: string[]; }): Promise<{ process?: string; variables: { [key: string]: string; }; }>; - $getDefaultShellAndArgs(): Thenable<{ shell: string, args: string[] | string | undefined; }>; $jsonTasksSupported(): Thenable; $findExecutable(command: string, cwd?: string, paths?: string[]): Promise; } @@ -1799,6 +1801,7 @@ export interface IDebugSessionFullDto { id: DebugSessionUUID; type: string; name: string; + parent: DebugSessionUUID | undefined; folderUri: UriComponents | undefined; configuration: IConfig; } @@ -1934,6 +1937,10 @@ export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditors $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise; } +export interface ExtHostNotebookRenderersShape { + $postRendererMessage(editorId: string, rendererId: string, message: unknown): void; +} + export interface ExtHostNotebookDocumentsAndEditorsShape { $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; } @@ -1953,10 +1960,10 @@ export interface ExtHostNotebookEditorsShape { } export interface ExtHostNotebookKernelsShape { - $acceptSelection(handle: number, uri: UriComponents, value: boolean): void; + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void; $executeCells(handle: number, uri: UriComponents, handles: number[]): Promise; $cancelCells(handle: number, uri: UriComponents, handles: number[]): Promise; - $acceptRendererMessage(handle: number, editorId: string, message: any): void; + $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void; } export interface ExtHostStorageShape { @@ -2087,6 +2094,7 @@ export const MainContext = { MainThreadNotebookDocuments: createMainId('MainThreadNotebookDocumentsShape'), MainThreadNotebookEditors: createMainId('MainThreadNotebookEditorsShape'), MainThreadNotebookKernels: createMainId('MainThreadNotebookKernels'), + MainThreadNotebookRenderers: createMainId('MainThreadNotebookRenderers'), MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline'), @@ -2134,6 +2142,7 @@ export const ExtHostContext = { ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), ExtHostNotebookKernels: createMainId('ExtHostNotebookKernels'), + ExtHostNotebookRenderers: createMainId('ExtHostNotebookRenderers'), ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/lib/vscode/src/vs/workbench/api/common/extHostApiCommands.ts b/lib/vscode/src/vs/workbench/api/common/extHostApiCommands.ts index f088f4ded9db..4fe87e99ca2a 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostApiCommands.ts @@ -4,17 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { IRawColorInfo, IWorkspaceEditDto, ICallHierarchyItemDto, IIncomingCallDto, IOutgoingCallDto } from 'vs/workbench/api/common/extHost.protocol'; import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; -import { ICommandsExecutor, RemoveFromRecentlyOpenedAPICommand, OpenIssueReporter, OpenIssueReporterArgs } from './apiCommands'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; @@ -326,10 +323,10 @@ const newCommands: ApiCommand[] = [ ), // --- inline hints new ApiCommand( - 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', + 'vscode.executeInlayHintProvider', '_executeInlayHintProvider', 'Execute inline hints provider', [ApiCommandArgument.Uri, ApiCommandArgument.Range], - new ApiCommandResult('A promise that resolves to an array of InlineHint objects', result => { - return result.map(typeConverters.InlineHint.to); + new ApiCommandResult('A promise that resolves to an array of Inlay objects', result => { + return result.map(typeConverters.InlayHint.to); }) ), // --- notebooks @@ -420,63 +417,8 @@ export class ExtHostApiCommands { static register(commands: ExtHostCommands) { newCommands.forEach(commands.registerApiCommand, commands); - return new ExtHostApiCommands(commands).registerCommands(); } - private _commands: ExtHostCommands; - private readonly _disposables = new DisposableStore(); - - private constructor(commands: ExtHostCommands) { - this._commands = commands; - } - - registerCommands() { - - - - - - // ----------------------------------------------------------------- - // The following commands are registered on both sides separately. - // - // We are trying to maintain backwards compatibility for cases where - // API commands are encoded as markdown links, for example. - // ----------------------------------------------------------------- - - type ICommandHandler = (...args: any[]) => any; - const adjustHandler = (handler: (executor: ICommandsExecutor, ...args: any[]) => any): ICommandHandler => { - return (...args: any[]) => { - return handler(this._commands, ...args); - }; - }; - - this._register(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute), { - description: 'Removes an entry with the given path from the recently opened list.', - args: [ - { name: 'path', description: 'Path to remove from recently opened.', constraint: (value: any) => typeof value === 'string' } - ] - }); - - this._register(OpenIssueReporter.ID, adjustHandler(OpenIssueReporter.execute), { - description: 'Opens the issue reporter with the provided extension id as the selected source', - args: [ - { name: 'extensionId', description: 'extensionId to report an issue on', constraint: (value: unknown) => typeof value === 'string' || (typeof value === 'object' && typeof (value as OpenIssueReporterArgs).extensionId === 'string') } - ] - }); - } - - // --- command impl - - /** - * @deprecated use the ApiCommand instead - */ - private _register(id: string, handler: (...args: any[]) => any, description?: ICommandHandlerDescription): void { - const disposable = this._commands.registerCommand(false, id, handler, this, description); - this._disposables.add(disposable); - } - - - } function tryMapWith(f: (x: T) => R) { diff --git a/lib/vscode/src/vs/workbench/api/common/extHostAuthentication.ts b/lib/vscode/src/vs/workbench/api/common/extHostAuthentication.ts index be7640ce1d9a..c134a357403f 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostAuthentication.ts @@ -48,11 +48,11 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return Object.freeze(this._providers.slice()); } - async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; - async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); const inFlightRequests = this._inFlightRequests.get(extensionId) || []; - const sortedScopes = scopes.sort().join(' '); + const sortedScopes = [...scopes].sort().join(' '); let inFlightRequest: GetSessionsRequest | undefined = inFlightRequests.find(request => request.scopes === sortedScopes); if (inFlightRequest) { @@ -81,7 +81,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } } - private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { + private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { await this._proxy.$ensureProvider(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostCodeInsets.ts b/lib/vscode/src/vs/workbench/api/common/extHostCodeInsets.ts index 7db034cd81f9..fd7e9e715e53 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -8,10 +8,9 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -61,16 +60,15 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { const webview = new class implements vscode.Webview { - private readonly _uuid = generateUuid(); private _html: string = ''; private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - return asWebviewUri(that._initData, this._uuid, resource); + return asWebviewUri(resource, that._initData.remote); } get cspSource(): string { - return that._initData.webviewCspSource; + return webviewGenericCspSource; } set options(value: vscode.WebviewOptions) { diff --git a/lib/vscode/src/vs/workbench/api/common/extHostCommands.ts b/lib/vscode/src/vs/workbench/api/common/extHostCommands.ts index 6d641f81fd22..14cb48998240 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostCommands.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostCommands.ts @@ -166,12 +166,12 @@ export class ExtHostCommands implements ExtHostCommandsShape { const toArgs = cloneAndChange(args, function (value) { if (value instanceof extHostTypes.Position) { return extHostTypeConverter.Position.from(value); - } - if (value instanceof extHostTypes.Range) { + } else if (value instanceof extHostTypes.Range) { return extHostTypeConverter.Range.from(value); - } - if (value instanceof extHostTypes.Location) { + } else if (value instanceof extHostTypes.Location) { return extHostTypeConverter.location.from(value); + } else if (extHostTypes.NotebookRange.isNotebookRange(value)) { + return extHostTypeConverter.NotebookRange.from(value); } if (!Array.isArray(value)) { return value; diff --git a/lib/vscode/src/vs/workbench/api/common/extHostDebugService.ts b/lib/vscode/src/vs/workbench/api/common/extHostDebugService.ts index 67477c58c5ca..406992259f6b 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostDebugService.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostDebugService.ts @@ -31,6 +31,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { withNullAsUndefined } from 'vs/base/common/types'; import * as process from 'vs/base/common/process'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -47,8 +48,8 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { onDidChangeBreakpoints: Event; breakpoints: vscode.Breakpoint[]; - addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; - removeBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; + addBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; + removeBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise; stopDebugging(session?: vscode.DebugSession): Promise; registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTriggerKind): vscode.Disposable; @@ -109,6 +110,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E @IExtHostExtensionService private _extensionService: IExtHostExtensionService, @IExtHostDocumentsAndEditors private _editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration protected _configurationService: IExtHostConfiguration, + @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs ) { this._configProviderHandleCounter = 0; this._configProviders = []; @@ -843,7 +845,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E let ds = this._debugSessions.get(dto.id); if (!ds) { const folder = await this.getFolder(dto.folderUri); - ds = new ExtHostDebugSession(this._debugServiceProxy, dto.id, dto.type, dto.name, folder, dto.configuration); + const parent = dto.parent ? this._debugSessions.get(dto.parent) : undefined; + ds = new ExtHostDebugSession(this._debugServiceProxy, dto.id, dto.type, dto.name, folder, dto.configuration, parent); this._debugSessions.set(ds.id, ds); this._debugServiceProxy.$sessionCached(ds.id); } @@ -870,7 +873,8 @@ export class ExtHostDebugSession implements vscode.DebugSession { private _type: string, private _name: string, private _workspaceFolder: vscode.WorkspaceFolder | undefined, - private _configuration: vscode.DebugConfiguration) { + private _configuration: vscode.DebugConfiguration, + private _parentSession: vscode.DebugSession | undefined) { } public get id(): string { @@ -884,12 +888,15 @@ export class ExtHostDebugSession implements vscode.DebugSession { public get name(): string { return this._name; } - public set name(name: string) { this._name = name; this._debugServiceProxy.$setDebugSessionName(this._id, name); } + public get parentSession(): vscode.DebugSession | undefined { + return this._parentSession; + } + _acceptNameChanged(name: string) { this._name = name; } @@ -930,7 +937,21 @@ export class ExtHostDebugConsole { export class ExtHostVariableResolverService extends AbstractVariableResolverService { - constructor(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors | undefined, configurationService: ExtHostConfigProvider, workspaceService?: IExtHostWorkspace) { + constructor(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors | undefined, configurationService: ExtHostConfigProvider, editorTabs: IExtHostEditorTabs, workspaceService?: IExtHostWorkspace) { + function getActiveUri(): URI | undefined { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return activeEditor.document.uri; + } + const tabs = editorTabs.tabs.filter(tab => tab.isActive); + if (tabs.length > 0) { + return tabs[0].resource; + } + } + return undefined; + } + super({ getFolderUri: (folderName: string): URI | undefined => { const found = folders.filter(f => f.name === folderName); @@ -952,19 +973,17 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ return process.env['VSCODE_EXEC_PATH']; }, getFilePath: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return path.normalize(activeEditor.document.uri.fsPath); - } + const activeUri = getActiveUri(); + if (activeUri) { + return path.normalize(activeUri.fsPath); } return undefined; }, getWorkspaceFolderPathForFile: (): string | undefined => { - if (editorService && workspaceService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - const ws = workspaceService.getWorkspaceFolder(activeEditor.document.uri); + if (workspaceService) { + const activeUri = getActiveUri(); + if (activeUri) { + const ws = workspaceService.getWorkspaceFolder(activeUri); if (ws) { return path.normalize(ws.uri.fsPath); } @@ -1076,12 +1095,13 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, - @IExtHostConfiguration configurationService: IExtHostConfiguration + @IExtHostConfiguration configurationService: IExtHostConfiguration, + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService); + super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); } protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService); + return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs); } } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostDiagnostics.ts b/lib/vscode/src/vs/workbench/api/common/extHostDiagnostics.ts index 1114e309d858..39f05e7db201 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -217,7 +217,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private readonly _collections = new Map(); private readonly _onDidChangeDiagnostics = new Emitter(); - static _debouncer(last: (vscode.Uri | string)[] | undefined, current: (vscode.Uri | string)[]): (vscode.Uri | string)[] { + static _debouncer(last: vscode.Uri[] | undefined, current: vscode.Uri[]): vscode.Uri[] { if (!last) { return current; } else { @@ -225,24 +225,12 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } } - static _mapper(last: (vscode.Uri | string)[]): { uris: vscode.Uri[] } { - const uris: vscode.Uri[] = []; - const map = new Set(); + static _mapper(last: vscode.Uri[]): { uris: readonly vscode.Uri[] } { + const map = new ResourceMap(); for (const uri of last) { - if (typeof uri === 'string') { - if (!map.has(uri)) { - map.add(uri); - uris.push(URI.parse(uri)); - } - } else { - if (!map.has(uri.toString())) { - map.add(uri.toString()); - uris.push(uri); - } - } + map.set(uri, uri); } - Object.freeze(uris); - return { uris }; + return { uris: Object.freeze(Array.from(map.values())) }; } readonly onDidChangeDiagnostics: Event = Event.map(Event.debounce(this._onDidChangeDiagnostics.event, ExtHostDiagnostics._debouncer, 50), ExtHostDiagnostics._mapper); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostEditorTabs.ts b/lib/vscode/src/vs/workbench/api/common/extHostEditorTabs.ts index 2d0f7b4c697f..1662754fd85e 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -7,15 +7,25 @@ import type * as vscode from 'vscode'; import { IEditorTabDto, IExtHostEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; - +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IEditorTab { name: string; group: number; resource: vscode.Uri + isActive: boolean +} + +export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { + readonly _serviceBrand: undefined; + tabs: readonly IEditorTab[]; + onDidChangeTabs: Event; } -export class ExtHostEditorTabs implements IExtHostEditorTabsShape { +export const IExtHostEditorTabs = createDecorator('IExtHostEditorTabs'); + +export class ExtHostEditorTabs implements IExtHostEditorTabs { + readonly _serviceBrand: undefined; private readonly _onDidChangeTabs = new Emitter(); readonly onDidChangeTabs: Event = this._onDidChangeTabs.event; @@ -31,7 +41,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabsShape { return { name: dto.name, group: dto.group, - resource: URI.revive(dto.resource) + resource: URI.revive(dto.resource), + isActive: dto.isActive }; }); this._onDidChangeTabs.fire(); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostExtensionService.ts b/lib/vscode/src/vs/workbench/api/common/extHostExtensionService.ts index d7c4c6a4e6f4..abb761d9dbee 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostExtensionService.ts @@ -7,10 +7,10 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; -import { Barrier, timeout } from 'vs/base/common/async'; +import { asPromise, Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, IInitData, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape, IResolveAuthorityResult } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; @@ -292,19 +292,19 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme try { if (typeof extension.module.deactivate === 'function') { result = Promise.resolve(extension.module.deactivate()).then(undefined, (err) => { - // TODO: Do something with err if this is not the shutdown case + this._logService.error(err); return Promise.resolve(undefined); }); } } catch (err) { - // TODO: Do something with err if this is not the shutdown case + this._logService.error(err); } // clean up subscriptions try { dispose(extension.subscriptions); } catch (err) { - // TODO: Do something with err if this is not the shutdown case + this._logService.error(err); } return result; @@ -631,7 +631,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // -- called by main thread - public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + private async _activateAndGetResolver(remoteAuthority: string): Promise<{ authorityPrefix: string; resolver: vscode.RemoteAuthorityResolver | undefined; }> { const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { throw new Error(`Not an authority that can be resolved!`); @@ -641,7 +641,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme await this._almostReadyToRunExtensions.wait(); await this._activateByEvent(`onResolveRemoteAuthority:${authorityPrefix}`, false); - const resolver = this._resolvers[authorityPrefix]; + return { authorityPrefix, resolver: this._resolvers[authorityPrefix] }; + } + + public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + + const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); if (!resolver) { return { type: 'error', @@ -668,7 +673,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }; const options: ResolvedOptions = { extensionHostEnv: result.extensionHostEnv, - trust: result.trust + isTrusted: result.isTrusted }; return { @@ -695,6 +700,28 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } + public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { + + const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); + if (!resolver) { + throw new Error(`Cannot get canonical URI because no remote extension is installed to resolve ${authorityPrefix}`); + } + + const uri = URI.revive(uriComponents); + + if (typeof resolver.getCanonicalURI === 'undefined') { + // resolver cannot compute canonical URI + return uri; + } + + const result = await asPromise(() => resolver.getCanonicalURI!(uri)); + if (!result) { + return uri; + } + + return result; + } + public $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise { this._registry.keepOnly(enabledExtensionIds); return this._startExtensionHost(); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostFileSystem.ts b/lib/vscode/src/vs/workbench/api/common/extHostFileSystem.ts index 462f181ba5b5..c3522c3f7900 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostFileSystem.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostFileSystem.ts @@ -115,6 +115,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { private readonly _fsProvider = new Map(); private readonly _registeredSchemes = new Set(); private readonly _watches = new Map(); + private readonly _enableProposedApi = new Map(); private _linkProviderRegistration?: IDisposable; private _handlePool: number = 0; @@ -133,7 +134,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { } } - registerFileSystemProvider(extension: ExtensionIdentifier, scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) { + registerFileSystemProvider(extension: ExtensionIdentifier, scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}, enableProposedApi?: boolean) { if (this._registeredSchemes.has(scheme)) { throw new Error(`a provider for the scheme '${scheme}' is already registered`); @@ -146,6 +147,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._linkProvider.add(scheme); this._registeredSchemes.add(scheme); this._fsProvider.set(handle, provider); + this._enableProposedApi.set(handle, enableProposedApi ?? false); let capabilities = files.FileSystemProviderCapabilities.FileReadWrite; if (options.isCaseSensitive) { @@ -200,17 +202,22 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._linkProvider.delete(scheme); this._registeredSchemes.delete(scheme); this._fsProvider.delete(handle); + this._enableProposedApi.delete(handle); this._proxy.$unregisterProvider(handle); }); } - private static _asIStat(stat: vscode.FileStat): files.IStat { - const { type, ctime, mtime, size } = stat; - return { type, ctime, mtime, size }; + private static _asIStat(stat: vscode.FileStat, enableProposedApi: boolean): files.IStat { + const { type, ctime, mtime, size, permissions } = stat; + if (enableProposedApi) { + return { type, ctime, mtime, size, permissions }; + } else { + return { type, ctime, mtime, size }; + } } $stat(handle: number, resource: UriComponents): Promise { - return Promise.resolve(this._getFsProvider(handle).stat(URI.revive(resource))).then(ExtHostFileSystem._asIStat); + return Promise.resolve(this._getFsProvider(handle).stat(URI.revive(resource))).then(stat => ExtHostFileSystem._asIStat(stat, this._enableProposedApi.get(handle) ?? false)); } $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]> { diff --git a/lib/vscode/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/lib/vscode/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 7fc9363feb79..05ab94c722c5 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -19,7 +19,7 @@ import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; import { isFalsyOrEmpty, isNonEmptyArray, coalesce, asArray } from 'vs/base/common/arrays'; -import { isObject } from 'vs/base/common/types'; +import { isArray, isObject } from 'vs/base/common/types'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -33,6 +33,7 @@ import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostAp import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; import { CancellationError } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; // --- adapter @@ -1038,6 +1039,99 @@ class SuggestAdapter { } } +class InlineCompletionAdapter { + private readonly _cache = new Cache('InlineCompletionItem'); + private readonly _disposables = new Map(); + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineCompletionItemProvider, + private readonly _commands: CommandsConverter, + ) { } + + public async provideInlineCompletions(resource: URI, position: IPosition, context: vscode.InlineCompletionContext, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + const pos = typeConvert.Position.to(position); + + const result = await asPromise(() => this._provider.provideInlineCompletionItems(doc, pos, context, token)); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + const normalizedResult: vscode.InlineCompletionList = isArray(result) ? { items: result } : result; + + const pid = this._cache.add(normalizedResult.items); + let disposableStore: DisposableStore | undefined = undefined; + + return { + pid, + items: normalizedResult.items.map((item, idx) => { + let command: modes.Command | undefined = undefined; + if (item.command) { + if (!disposableStore) { + disposableStore = new DisposableStore(); + this._disposables.set(pid, disposableStore); + } + command = this._commands.toInternal(item.command, disposableStore); + } + + return ({ + text: item.text, + range: item.range ? typeConvert.Range.from(item.range) : undefined, + command, + idx: idx, + }); + }), + }; + } + + public disposeCompletions(pid: number) { + this._cache.delete(pid); + const d = this._disposables.get(pid); + if (d) { + d.clear(); + } + this._disposables.delete(pid); + } + + public handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._cache.get(pid, idx); + if (completionItem) { + InlineCompletionController.get(this._provider).fireOnDidShowCompletionItem({ + completionItem + }); + } + } +} + +export class InlineCompletionController implements vscode.InlineCompletionController { + private static readonly map = new WeakMap, InlineCompletionController>(); + + public static get(provider: vscode.InlineCompletionItemProvider): InlineCompletionController { + let existing = InlineCompletionController.map.get(provider); + if (!existing) { + existing = new InlineCompletionController(); + InlineCompletionController.map.set(provider, existing); + } + return existing; + } + + private readonly _onDidShowCompletionItemEmitter = new Emitter>(); + public readonly onDidShowCompletionItem: vscode.Event> = this._onDidShowCompletionItemEmitter.event; + + public fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent): void { + this._onDidShowCompletionItemEmitter.fire(event); + } +} + class SignatureHelpAdapter { private readonly _cache = new Cache('SignatureHelp'); @@ -1082,16 +1176,16 @@ class SignatureHelpAdapter { } } -class InlineHintsAdapter { +class InlayHintsAdapter { constructor( private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.InlineHintsProvider, + private readonly _provider: vscode.InlayHintsProvider, ) { } - provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise { + provideInlayHints(resource: URI, range: IRange, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); - return asPromise(() => this._provider.provideInlineHints(doc, typeConvert.Range.to(range), token)).then(value => { - return value ? { hints: value.map(typeConvert.InlineHint.from) } : undefined; + return asPromise(() => this._provider.provideInlayHints(doc, typeConvert.Range.to(range), token)).then(value => { + return value ? { hints: value.map(typeConvert.InlayHint.from) } : undefined; }); } } @@ -1355,7 +1449,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter - | LinkedEditingRangeAdapter | InlineHintsAdapter; + | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter; class AdapterData { constructor( @@ -1809,6 +1903,28 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id), undefined); } + // --- ghost test + + registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { + const handle = this._addNewAdapter(new InlineCompletionAdapter(this._documents, provider, this._commands.converter), extension); + this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineCompletionAdapter, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined); + } + + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void { + this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + adapter.handleDidShowCompletionItem(pid, idx); + }, undefined); + } + + $freeInlineCompletionsList(handle: number, pid: number): void { + this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.disposeCompletions(pid); }, undefined); + } + // --- parameter hints registerSignatureHelpProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, metadataOrTriggerChars: string[] | vscode.SignatureHelpProviderMetadata): vscode.Disposable { @@ -1831,23 +1947,23 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- inline hints - registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + registerInlayHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlayHintsProvider): vscode.Disposable { - const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined; - const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension); + const eventHandle = typeof provider.onDidChangeInlayHints === 'function' ? this._nextHandle() : undefined; + const handle = this._addNewAdapter(new InlayHintsAdapter(this._documents, provider), extension); - this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); + this._proxy.$registerInlayHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); let result = this._createDisposable(handle); if (eventHandle !== undefined) { - const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle)); + const subscription = provider.onDidChangeInlayHints!(_ => this._proxy.$emitInlayHintsEvent(eventHandle)); result = Disposable.from(result, subscription); } return result; } - $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { - return this._withAdapter(handle, InlineHintsAdapter, adapter => adapter.provideInlineHints(URI.revive(resource), range, token), undefined); + $provideInlayHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, InlayHintsAdapter, adapter => adapter.provideInlayHints(URI.revive(resource), range, token), undefined); } // --- links diff --git a/lib/vscode/src/vs/workbench/api/common/extHostMemento.ts b/lib/vscode/src/vs/workbench/api/common/extHostMemento.ts index 31c1676e6963..e4f029c0b126 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostMemento.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostMemento.ts @@ -56,6 +56,11 @@ export class ExtensionMemento implements vscode.Memento { }, 0); } + get keys(): readonly string[] { + // Filter out `undefined` values, as they can stick around in the `_value` until the `onDidChangeStorage` event runs + return Object.entries(this._value ?? {}).filter(([, value]) => value !== undefined).map(([key]) => key); + } + get whenReady(): Promise { return this._init; } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostNotebook.ts b/lib/vscode/src/vs/workbench/api/common/extHostNotebook.ts index 116b80d14c0e..e220a0d6f00e 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostNotebook.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostNotebook.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; import { hash } from 'vs/base/common/hash'; import { IdGenerator } from 'vs/base/common/idGenerator'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; @@ -25,7 +24,7 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { CellEditType, IImmediateCellEditOperation, INotebookExclusiveDocumentFilter, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, NullablePartialNotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExclusiveDocumentFilter, INotebookContributionData, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'; import { ExtHostNotebookEditor } from './extHostNotebookEditor'; @@ -103,8 +102,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; - private _activeExecutions = new ResourceMap(); - private _statusBarCache = new Cache('NotebookCellStatusBarCache'); constructor( @@ -155,13 +152,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return [...this._documents.values()]; } - lookupNotebookDocument(uri: URI): ExtHostNotebookDocument | undefined { - return this._documents.get(uri); - } - - private _getNotebookDocument(uri: URI): ExtHostNotebookDocument { + getNotebookDocument(uri: URI, relaxed: true): ExtHostNotebookDocument | undefined; + getNotebookDocument(uri: URI): ExtHostNotebookDocument; + getNotebookDocument(uri: URI, relaxed?: true): ExtHostNotebookDocument | undefined { const result = this._documents.get(uri); - if (!result) { + if (!result && !relaxed) { throw new Error(`NO notebook document for '${uri}'`); } return result; @@ -179,13 +174,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, - options?: vscode.NotebookDocumentContentOptions & { - viewOptions?: { - displayName: string; - filenamePattern: (vscode.GlobPattern | { include: vscode.GlobPattern; exclude: vscode.GlobPattern })[]; - exclusive?: boolean; - }; - } + options?: vscode.NotebookDocumentContentOptions, + registration?: vscode.NotebookRegistrationData ): vscode.Disposable { if (isFalsyOrWhitespace(viewType)) { throw new Error(`viewType cannot be empty or just whitespace`); @@ -196,7 +186,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._notebookContentProviders.set(viewType, { extension, provider }); - let listener: IDisposable | undefined; if (provider.onDidChangeNotebookContentOptions) { listener = provider.onDidChangeNotebookContentOptions(() => { @@ -205,21 +194,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - const viewOptionsFilenamePattern = options?.viewOptions?.filenamePattern - .map(pattern => typeConverters.NotebookExclusiveDocumentPattern.from(pattern)) - .filter(pattern => pattern !== undefined) as (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; - - if (options?.viewOptions?.filenamePattern && !viewOptionsFilenamePattern) { - console.warn(`Notebook content provider view options file name pattern is invalid ${options?.viewOptions?.filenamePattern}`); - } - - const internalOptions = typeConverters.NotebookDocumentContentOptions.from(options); - this._notebookProxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, { - transientOutputs: internalOptions.transientOutputs, - transientCellMetadata: internalOptions.transientCellMetadata, - transientDocumentMetadata: internalOptions.transientDocumentMetadata, - viewOptions: options?.viewOptions && viewOptionsFilenamePattern ? { displayName: options.viewOptions.displayName, filenamePattern: viewOptionsFilenamePattern, exclusive: options.viewOptions.exclusive || false } : undefined - }); + this._notebookProxy.$registerNotebookProvider( + { id: extension.identifier, location: extension.extensionLocation }, + viewType, + typeConverters.NotebookDocumentContentOptions.from(options), + ExtHostNotebookController._convertNotebookRegistrationData(extension, registration) + ); return new extHostTypes.Disposable(() => { listener?.dispose(); @@ -228,13 +208,33 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - registerNotebookCellStatusBarItemProvider(extension: IExtensionDescription, selector: vscode.NotebookSelector, provider: vscode.NotebookCellStatusBarItemProvider) { + private static _convertNotebookRegistrationData(extension: IExtensionDescription, registration: vscode.NotebookRegistrationData | undefined): INotebookContributionData | undefined { + if (!registration) { + return; + } + const viewOptionsFilenamePattern = registration.filenamePattern + .map(pattern => typeConverters.NotebookExclusiveDocumentPattern.from(pattern)) + .filter(pattern => pattern !== undefined) as (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; + if (registration.filenamePattern && !viewOptionsFilenamePattern) { + console.warn(`Notebook content provider view options file name pattern is invalid ${registration.filenamePattern}`); + return undefined; + } + return { + extension: extension.identifier, + providerDisplayName: extension.displayName || extension.name, + displayName: registration.displayName, + filenamePattern: viewOptionsFilenamePattern, + exclusive: registration.exclusive || false + }; + } + + registerNotebookCellStatusBarItemProvider(extension: IExtensionDescription, notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) { const handle = ExtHostNotebookController._notebookStatusBarItemProviderHandlePool++; const eventHandle = typeof provider.onDidChangeCellStatusBarItems === 'function' ? ExtHostNotebookController._notebookStatusBarItemProviderHandlePool++ : undefined; this._notebookStatusBarItemProviders.set(handle, provider); - this._notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, selector); + this._notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, notebookType); let subscription: vscode.Disposable | undefined; if (eventHandle !== undefined) { @@ -254,12 +254,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return new NotebookEditorDecorationType(this._notebookEditorsProxy, options).value; } + async createNotebookDocument(options: { viewType: string, content?: vscode.NotebookData }): Promise { + const canonicalUri = await this._notebookDocumentsProxy.$tryCreateNotebook({ + viewType: options.viewType, + content: options.content && typeConverters.NotebookData.from(options.content) + }); + return URI.revive(canonicalUri); + } + async openNotebookDocument(uri: URI): Promise { const cached = this._documents.get(uri); if (cached) { return cached.apiNotebook; } - const canonicalUri = await this._notebookDocumentsProxy.$tryOpenDocument(uri); + const canonicalUri = await this._notebookDocumentsProxy.$tryOpenNotebook(uri); const document = this._documents.get(URI.revive(canonicalUri)); return assertIsDefined(document?.apiNotebook); } @@ -285,7 +293,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }; } - const editorId = await this._notebookEditorsProxy.$tryShowNotebookDocument(notebookOrUri.uri, notebookOrUri.viewType, resolvedOptions); + const editorId = await this._notebookEditorsProxy.$tryShowNotebookDocument(notebookOrUri.uri, notebookOrUri.notebookType, resolvedOptions); const editor = editorId && this._editors.get(editorId)?.apiEditor; if (editor) { @@ -293,9 +301,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } if (editorId) { - throw new Error(`Could NOT open editor for "${notebookOrUri.toString()}" because another editor opened in the meantime.`); + throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}" because another editor opened in the meantime.`); } else { - throw new Error(`Could NOT open editor for "${notebookOrUri.toString()}".`); + throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}".`); } } @@ -319,7 +327,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { const disposables = new DisposableStore(); const cacheId = this._statusBarCache.add([disposables]); - const items = (result && result.map(item => typeConverters.NotebookStatusBarItem.from(item, this._commandsConverter, disposables))) ?? undefined; + const resultArr = Array.isArray(result) ? result : [result]; + const items = resultArr.map(item => typeConverters.NotebookStatusBarItem.from(item, this._commandsConverter, disposables)); return { cacheId, items @@ -335,18 +344,18 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _handlePool = 0; private readonly _notebookSerializer = new Map(); - registerNotebookSerializer(extension: IExtensionDescription, viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions): vscode.Disposable { + registerNotebookSerializer(extension: IExtensionDescription, viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData): vscode.Disposable { if (isFalsyOrWhitespace(viewType)) { throw new Error(`viewType cannot be empty or just whitespace`); } const handle = this._handlePool++; this._notebookSerializer.set(handle, serializer); - const internalOptions = typeConverters.NotebookDocumentContentOptions.from(options); this._notebookProxy.$registerNotebookSerializer( handle, - { id: extension.identifier, location: extension.extensionLocation, description: extension.description }, + { id: extension.identifier, location: extension.extensionLocation }, viewType, - internalOptions + typeConverters.NotebookDocumentContentOptions.from(options), + ExtHostNotebookController._convertNotebookRegistrationData(extension, registration) ); return toDisposable(() => { this._notebookProxy.$unregisterNotebookSerializer(handle); @@ -359,10 +368,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { throw new Error('NO serializer found'); } const data = await serializer.deserializeNotebook(bytes.buffer, token); - return { - metadata: typeConverters.NotebookDocumentMetadata.from(data.metadata), - cells: data.cells.map(typeConverters.NotebookCellData.from), - }; + return typeConverters.NotebookData.from(data); } async $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise { @@ -370,38 +376,30 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (!serializer) { throw new Error('NO serializer found'); } - const bytes = await serializer.serializeNotebook({ - metadata: typeConverters.NotebookDocumentMetadata.to(data.metadata), - cells: data.cells.map(typeConverters.NotebookCellData.to) - }, token); + const bytes = await serializer.serializeNotebook(typeConverters.NotebookData.to(data), token); return VSBuffer.wrap(bytes); } - cancelOneNotebookCellExecution(cell: ExtHostCell): void { - const execution = this._activeExecutions.get(cell.uri); - execution?.cancel(); - } - // --- open, save, saveAs, backup async $openNotebook(viewType: string, uri: UriComponents, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken): Promise { const { provider } = this._getProviderData(viewType); const data = await provider.openNotebook(URI.revive(uri), { backupId, untitledDocumentData: untitledDocumentData?.buffer }, token); return { - metadata: typeConverters.NotebookDocumentMetadata.from(data.metadata), + metadata: data.metadata ?? Object.create(null), cells: data.cells.map(typeConverters.NotebookCellData.from), }; } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); const { provider } = this._getProviderData(viewType); await provider.saveNotebook(document.apiNotebook, token); return true; } async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); const { provider } = this._getProviderData(viewType); await provider.saveNotebookAs(URI.revive(target), document.apiNotebook, token); return true; @@ -410,7 +408,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _backupIdPool: number = 0; async $backupNotebook(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); const provider = this._getProviderData(viewType); const storagePath = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); @@ -423,17 +421,17 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } $acceptModelChanged(uri: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); document.acceptModelChanged(event, isDirty); } $acceptDirtyStateChanged(uri: UriComponents, isDirty: boolean): void { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); document.acceptModelChanged({ rawEvents: [], versionId: document.apiNotebook.version }, isDirty); } $acceptModelSaved(uri: UriComponents): void { - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); this._onDidSaveNotebookDocument.fire(document.apiNotebook); } @@ -480,7 +478,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { $acceptDocumentPropertiesChanged(uri: UriComponents, data: INotebookDocumentPropertiesChangeData): void { this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uri.path, data); - const document = this._getNotebookDocument(URI.revive(uri)); + const document = this.getNotebookDocument(URI.revive(uri)); document.acceptDocumentPropertiesChanged(data); if (data.metadata) { this._onDidChangeNotebookDocumentMetadata.fire({ document: document.apiNotebook }); @@ -559,7 +557,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } }, viewType, - modelData.metadata ? typeConverters.NotebookDocumentMetadata.to(modelData.metadata) : new extHostTypes.NotebookDocumentMetadata(), + modelData.metadata ?? Object.create({}), uri, ); @@ -639,213 +637,4 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor?.apiEditor); } } - createNotebookCellExecution(docUri: vscode.Uri, index: number, kernelId: string): vscode.NotebookCellExecutionTask | undefined { - const document = this.lookupNotebookDocument(docUri); - if (!document) { - throw new Error(`Invalid uri: ${docUri} `); - } - - const cell = document.getCellFromIndex(index); - if (!cell) { - throw new Error(`Invalid cell index: ${docUri}, ${index} `); - } - - // TODO@roblou also validate kernelId, once kernel has moved from editor to document - if (this._activeExecutions.has(cell.uri)) { - throw new Error(`duplicate execution for ${cell.uri}`); - } - - const execution = new NotebookCellExecutionTask(docUri, document, cell, this._notebookDocumentsProxy); - this._activeExecutions.set(cell.uri, execution); - const listener = execution.onDidChangeState(() => { - if (execution.state === NotebookCellExecutionTaskState.Resolved) { - execution.dispose(); - listener.dispose(); - this._activeExecutions.delete(cell.uri); - } - }); - - return execution.asApiObject(); - } -} - -enum NotebookCellExecutionTaskState { - Init, - Started, - Resolved -} - -class NotebookCellExecutionTask extends Disposable { - private _onDidChangeState = new Emitter(); - readonly onDidChangeState = this._onDidChangeState.event; - - private _state = NotebookCellExecutionTaskState.Init; - get state(): NotebookCellExecutionTaskState { return this._state; } - - private readonly _tokenSource = this._register(new CancellationTokenSource()); - - private readonly _collector: TimeoutBasedCollector; - - private _executionOrder: number | undefined; - - constructor( - private readonly _uri: vscode.Uri, - private readonly _document: ExtHostNotebookDocument, - private readonly _cell: ExtHostCell, - private readonly _proxy: MainThreadNotebookDocumentsShape) { - super(); - - this._collector = new TimeoutBasedCollector(10, edits => this.applyEdits(edits)); - - this._executionOrder = _cell.internalMetadata.executionOrder; - this.mixinMetadata({ - runState: extHostTypes.NotebookCellExecutionState.Pending, - executionOrder: null - }); - } - - cancel(): void { - this._tokenSource.cancel(); - } - - private async applyEditSoon(edit: IImmediateCellEditOperation): Promise { - await this._collector.addItem(edit); - } - - private async applyEdits(edits: IImmediateCellEditOperation[]): Promise { - return this._proxy.$applyEdits(this._uri, edits, false); - } - - private verifyStateForOutput() { - if (this._state === NotebookCellExecutionTaskState.Init) { - throw new Error('Must call start before modifying cell output'); - } - - if (this._state === NotebookCellExecutionTaskState.Resolved) { - throw new Error('Cannot modify cell output after calling resolve'); - } - } - - private mixinMetadata(mixinMetadata: NullablePartialNotebookCellMetadata) { - const edit: IImmediateCellEditOperation = { editType: CellEditType.PartialMetadata, handle: this._cell.handle, metadata: mixinMetadata }; - this.applyEdits([edit]); - } - - private cellIndexToHandle(cellIndex: number | undefined): number | undefined { - const cell = typeof cellIndex === 'number' ? this._document.getCellFromIndex(cellIndex) : this._cell; - if (!cell) { - return; - } - - return cell.handle; - } - - asApiObject(): vscode.NotebookCellExecutionTask { - const that = this; - return Object.freeze({ - get document() { return that._document.apiNotebook; }, - get cell() { return that._cell.apiCell; }, - - get executionOrder() { return that._executionOrder; }, - set executionOrder(v: number | undefined) { - that._executionOrder = v; - that.mixinMetadata({ - executionOrder: v - }); - }, - - start(context?: vscode.NotebookCellExecuteStartContext): void { - if (that._state === NotebookCellExecutionTaskState.Resolved || that._state === NotebookCellExecutionTaskState.Started) { - throw new Error('Cannot call start again'); - } - - that._state = NotebookCellExecutionTaskState.Started; - that._onDidChangeState.fire(); - - that.mixinMetadata({ - runState: extHostTypes.NotebookCellExecutionState.Executing, - runStartTime: context?.startTime ?? null - }); - }, - - end(result?: vscode.NotebookCellExecuteEndContext): void { - if (that._state === NotebookCellExecutionTaskState.Resolved) { - throw new Error('Cannot call resolve twice'); - } - - that._state = NotebookCellExecutionTaskState.Resolved; - that._onDidChangeState.fire(); - - that.mixinMetadata({ - runState: extHostTypes.NotebookCellExecutionState.Idle, - lastRunSuccess: result?.success ?? null, - runEndTime: result?.endTime ?? null, - }); - }, - - clearOutput(cellIndex?: number): Thenable { - that.verifyStateForOutput(); - return this.replaceOutput([], cellIndex); - }, - - async appendOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cellIndex?: number): Promise { - that.verifyStateForOutput(); - const handle = that.cellIndexToHandle(cellIndex); - if (typeof handle !== 'number') { - return; - } - - outputs = Array.isArray(outputs) ? outputs : [outputs]; - return that.applyEditSoon({ editType: CellEditType.Output, handle, append: true, outputs: outputs.map(typeConverters.NotebookCellOutput.from) }); - }, - - async replaceOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cellIndex?: number): Promise { - that.verifyStateForOutput(); - const handle = that.cellIndexToHandle(cellIndex); - if (typeof handle !== 'number') { - return; - } - - outputs = Array.isArray(outputs) ? outputs : [outputs]; - return that.applyEditSoon({ editType: CellEditType.Output, handle, outputs: outputs.map(typeConverters.NotebookCellOutput.from) }); - }, - - async appendOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], outputId: string): Promise { - that.verifyStateForOutput(); - items = Array.isArray(items) ? items : [items]; - return that.applyEditSoon({ editType: CellEditType.OutputItems, append: true, items: items.map(typeConverters.NotebookCellOutputItem.from), outputId }); - }, - - async replaceOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], outputId: string): Promise { - that.verifyStateForOutput(); - items = Array.isArray(items) ? items : [items]; - return that.applyEditSoon({ editType: CellEditType.OutputItems, items: items.map(typeConverters.NotebookCellOutputItem.from), outputId }); - }, - - token: that._tokenSource.token - }); - } -} - -class TimeoutBasedCollector { - private batch: T[] = []; - private waitPromise: Promise | undefined; - - constructor( - private readonly delay: number, - private readonly callback: (items: T[]) => Promise) { } - - addItem(item: T): Promise { - this.batch.push(item); - if (!this.waitPromise) { - this.waitPromise = timeout(this.delay).then(() => { - this.waitPromise = undefined; - const batch = this.batch; - this.batch = []; - return this.callback(batch); - }); - } - - return this.waitPromise; - } } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostNotebookDocument.ts b/lib/vscode/src/vs/workbench/api/common/extHostNotebookDocument.ts index 4e46c5819461..55c2669a3d73 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -6,12 +6,12 @@ import { Schemas } from 'vs/base/common/network'; import { deepFreeze, equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; -import { CellKind, INotebookDocumentPropertiesChangeData, MainThreadNotebookDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { INotebookDocumentPropertiesChangeData, MainThreadNotebookDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IMainCellDto, IOutputDto, IOutputItemDto, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IMainCellDto, IOutputDto, IOutputItemDto, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; class RawContentChangeEvent { @@ -44,19 +44,19 @@ export class ExtHostCell { }; } - private _outputs: extHostTypes.NotebookCellOutput[]; - private _metadata: extHostTypes.NotebookCellMetadata; + private _outputs: vscode.NotebookCellOutput[]; + private _metadata: NotebookCellMetadata; private _previousResult: vscode.NotebookCellExecutionSummary | undefined; - private _internalMetadata: NotebookCellMetadata; + private _internalMetadata: NotebookCellInternalMetadata; readonly handle: number; readonly uri: URI; readonly cellKind: CellKind; - private _cell: vscode.NotebookCell | undefined; + private _apiCell: vscode.NotebookCell | undefined; constructor( - private readonly _notebook: ExtHostNotebookDocument, + readonly notebook: ExtHostNotebookDocument, private readonly _extHostDocument: ExtHostDocumentsAndEditors, private readonly _cellData: IMainCellDto, ) { @@ -64,33 +64,33 @@ export class ExtHostCell { this.uri = URI.revive(_cellData.uri); this.cellKind = _cellData.cellKind; this._outputs = _cellData.outputs.map(extHostTypeConverters.NotebookCellOutput.to); - this._internalMetadata = _cellData.metadata ?? {}; - this._metadata = extHostTypeConverters.NotebookCellMetadata.to(this._internalMetadata); - this._previousResult = extHostTypeConverters.NotebookCellPreviousExecutionResult.to(this._internalMetadata); + this._internalMetadata = _cellData.internalMetadata ?? {}; + this._metadata = _cellData.metadata ?? {}; + this._previousResult = extHostTypeConverters.NotebookCellExecutionSummary.to(_cellData.internalMetadata ?? {}); } - get internalMetadata(): NotebookCellMetadata { + get internalMetadata(): NotebookCellInternalMetadata { return this._internalMetadata; } get apiCell(): vscode.NotebookCell { - if (!this._cell) { + if (!this._apiCell) { const that = this; const data = this._extHostDocument.getDocument(this.uri); if (!data) { throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`); } - this._cell = Object.freeze({ - get index() { return that._notebook.getCellIndex(that); }, - notebook: that._notebook.apiNotebook, + this._apiCell = Object.freeze({ + get index() { return that.notebook.getCellIndex(that); }, + notebook: that.notebook.apiNotebook, kind: extHostTypeConverters.NotebookCellKind.to(this._cellData.cellKind), document: data.document, get outputs() { return that._outputs.slice(0); }, get metadata() { return that._metadata; }, - get latestExecutionSummary() { return that._previousResult; } + get executionSummary() { return that._previousResult; } }); } - return this._cell; + return this._apiCell; } setOutputs(newOutputs: IOutputDto[]): void { @@ -102,16 +102,19 @@ export class ExtHostCell { const output = this._outputs.find(op => op.id === outputId); if (output) { if (!append) { - output.outputs.length = 0; + output.items.length = 0; } - output.outputs.push(...newItems); + output.items.push(...newItems); } } setMetadata(newMetadata: NotebookCellMetadata): void { - this._internalMetadata = newMetadata; - this._metadata = extHostTypeConverters.NotebookCellMetadata.to(newMetadata); - this._previousResult = extHostTypeConverters.NotebookCellPreviousExecutionResult.to(newMetadata); + this._metadata = newMetadata; + } + + setInternalMetadata(newInternalMetadata: NotebookCellInternalMetadata): void { + this._internalMetadata = newInternalMetadata; + this._previousResult = extHostTypeConverters.NotebookCellExecutionSummary.to(newInternalMetadata); } } @@ -141,8 +144,8 @@ export class ExtHostNotebookDocument { private readonly _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private readonly _textDocuments: ExtHostDocuments, private readonly _emitter: INotebookEventEmitter, - private readonly _viewType: string, - private _metadata: extHostTypes.NotebookDocumentMetadata, + private readonly _notebookType: string, + private _metadata: Record, readonly uri: URI, ) { } @@ -156,7 +159,7 @@ export class ExtHostNotebookDocument { this._notebook = { get uri() { return that.uri; }, get version() { return that._versionId; }, - get viewType() { return that._viewType; }, + get notebookType() { return that._notebookType; }, get isDirty() { return that._isDirty; }, get isUntitled() { return that.uri.scheme === Schemas.untitled; }, get isClosed() { return that._disposed; }, @@ -190,7 +193,7 @@ export class ExtHostNotebookDocument { acceptDocumentPropertiesChanged(data: INotebookDocumentPropertiesChangeData) { if (data.metadata) { - this._metadata = this._metadata.with(data.metadata); + this._metadata = { ...this._metadata, ...data.metadata }; } } @@ -213,11 +216,14 @@ export class ExtHostNotebookDocument { this._changeCellLanguage(rawEvent.index, rawEvent.language); } else if (rawEvent.kind === NotebookCellsChangeType.ChangeCellMetadata) { this._changeCellMetadata(rawEvent.index, rawEvent.metadata); + } else if (rawEvent.kind === NotebookCellsChangeType.ChangeCellInternalMetadata) { + this._changeCellInternalMetadata(rawEvent.index, rawEvent.internalMetadata); } } } private _validateIndex(index: number): number { + index = index | 0; if (index < 0) { return 0; } else if (index >= this._cells.length) { @@ -228,13 +234,15 @@ export class ExtHostNotebookDocument { } private _validateRange(range: vscode.NotebookRange): vscode.NotebookRange { - if (range.start < 0) { - range = range.with({ start: 0 }); + let start = range.start | 0; + let end = range.end | 0; + if (start < 0) { + start = 0; } - if (range.end > this._cells.length) { - range = range.with({ end: this._cells.length }); + if (end > this._cells.length) { + end = this._cells.length; } - return range; + return range.with({ start, end }); } private _getCells(range: vscode.NotebookRange): ExtHostCell[] { @@ -250,7 +258,7 @@ export class ExtHostNotebookDocument { if (this._disposed) { return Promise.reject(new Error('Notebook has been closed')); } - return this._proxy.$trySaveDocument(this.uri); + return this._proxy.$trySaveNotebook(this.uri); } private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { @@ -275,7 +283,7 @@ export class ExtHostNotebookDocument { const changeEvent = new RawContentChangeEvent(splice[0], splice[1], [], newCells); const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); - for (let cell of deletedItems) { + for (const cell of deletedItems) { removedCellDocuments.push(cell.uri); changeEvent.deletedItems.push(cell.apiCell); } @@ -331,7 +339,6 @@ export class ExtHostNotebookDocument { private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { const cell = this._cells[index]; - const originalInternalMetadata = cell.internalMetadata; const originalExtMetadata = cell.apiCell.metadata; cell.setMetadata(newMetadata); const newExtMetadata = cell.apiCell.metadata; @@ -339,13 +346,24 @@ export class ExtHostNotebookDocument { if (!equals(originalExtMetadata, newExtMetadata)) { this._emitter.emitCellMetadataChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell })); } + } + + private _changeCellInternalMetadata(index: number, newInternalMetadata: NotebookCellInternalMetadata): void { + const cell = this._cells[index]; + + const originalInternalMetadata = cell.internalMetadata; + cell.setInternalMetadata(newInternalMetadata); - if (originalInternalMetadata.runState !== newMetadata.runState) { - const executionState = newMetadata.runState ?? extHostTypes.NotebookCellExecutionState.Idle; - this._emitter.emitCellExecutionStateChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell, executionState })); + if (originalInternalMetadata.runState !== newInternalMetadata.runState) { + const executionState = newInternalMetadata.runState ?? extHostTypes.NotebookCellExecutionState.Idle; + this._emitter.emitCellExecutionStateChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell, state: executionState })); } } + getCellFromApiCell(apiCell: vscode.NotebookCell): ExtHostCell | undefined { + return this._cells.find(cell => cell.apiCell === apiCell); + } + getCellFromIndex(index: number): ExtHostCell | undefined { return this._cells[index]; } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostNotebookEditor.ts b/lib/vscode/src/vs/workbench/api/common/extHostNotebookEditor.ts index 30f8c6d52e43..a3b7fabf34fe 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -40,7 +40,7 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { } } - replaceMetadata(value: vscode.NotebookDocumentMetadata): void { + replaceMetadata(value: { [key: string]: any }): void { this._throwIfFinalized(); this._collectedEdits.push({ editType: CellEditType.DocumentMetadata, @@ -48,7 +48,7 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { }); } - replaceCellMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + replaceCellMetadata(index: number, metadata: Record): void { this._throwIfFinalized(); this._collectedEdits.push({ editType: CellEditType.Metadata, @@ -73,6 +73,8 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { export class ExtHostNotebookEditor { + public static readonly apiEditorsToExtHost = new WeakMap(); + private _selections: vscode.NotebookRange[] = []; private _visibleRanges: vscode.NotebookRange[] = []; private _viewColumn?: vscode.ViewColumn; @@ -127,6 +129,8 @@ export class ExtHostNotebookEditor { return that.setDecorations(decorationType, range); } }; + + ExtHostNotebookEditor.apiEditorsToExtHost.set(this._editor, this); } return this._editor; } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostNotebookKernels.ts b/lib/vscode/src/vs/workbench/api/common/extHostNotebookKernels.ts index add0d63d6666..626520ce8bda 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ExtHostNotebookKernelsShape, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ExtHostNotebookKernelsShape, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; import * as vscode from 'vscode'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -13,30 +13,42 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; +import { ResourceMap } from 'vs/base/common/map'; +import { timeout } from 'vs/base/common/async'; +import { ExtHostCell, ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; +import { CellEditType, IImmediateCellEditOperation, IOutputDto, NotebookCellExecutionState, NullablePartialNotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { asArray } from 'vs/base/common/arrays'; +import { ILogService } from 'vs/platform/log/common/log'; +import { NotebookCellOutput } from 'vs/workbench/api/common/extHostTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; interface IKernelData { extensionId: ExtensionIdentifier, controller: vscode.NotebookController; onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument; }>; - onDidReceiveMessage: Emitter<{ editor: vscode.NotebookEditor, message: any }>; + onDidReceiveMessage: Emitter<{ editor: vscode.NotebookEditor, message: any; }>; + associatedNotebooks: ResourceMap; } export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { private readonly _proxy: MainThreadNotebookKernelsShape; + private readonly _activeExecutions = new ResourceMap(); private readonly _kernelData = new Map(); private _handlePool: number = 0; constructor( - mainContext: IMainContext, + private readonly _mainContext: IMainContext, private readonly _initData: IExtHostInitDataService, - private readonly _extHostNotebook: ExtHostNotebookController + private readonly _extHostNotebook: ExtHostNotebookController, + @ILogService private readonly _logService: ILogService, ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookKernels); + this._proxy = _mainContext.getProxy(MainContext.MainThreadNotebookKernels); } - createNotebookController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler?: vscode.NotebookExecuteHandler, preloads?: vscode.NotebookKernelPreload[]): vscode.NotebookController { + createNotebookController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler?: (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable, preloads?: vscode.NotebookRendererScript[]): vscode.NotebookController { for (let data of this._kernelData.values()) { if (data.controller.id === id && ExtensionIdentifier.equals(extension.identifier, data.extensionId)) { @@ -44,29 +56,32 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } } + const handle = this._handlePool++; const that = this; + this._logService.trace(`NotebookController[${handle}], CREATED by ${extension.identifier.value}, ${id}`); + const _defaultExecutHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.identifier}'`); let isDisposed = false; const commandDisposables = new DisposableStore(); - const onDidChangeSelection = new Emitter<{ selected: boolean, notebook: vscode.NotebookDocument }>(); - const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor, message: any }>(); + const onDidChangeSelection = new Emitter<{ selected: boolean, notebook: vscode.NotebookDocument; }>(); + const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor, message: any; }>(); const data: INotebookKernelDto2 = { id: `${extension.identifier.value}/${id}`, - viewType, + notebookType: viewType, extensionId: extension.identifier, extensionLocation: extension.extensionLocation, label: label || extension.identifier.value, - preloads: preloads ? preloads.map(extHostTypeConverters.NotebookKernelPreload.from) : [] + preloads: preloads ? preloads.map(extHostTypeConverters.NotebookRendererScript.from) : [] }; // - let _executeHandler: vscode.NotebookExecuteHandler = handler ?? _defaultExecutHandler; - let _interruptHandler: vscode.NotebookInterruptHandler | undefined; + let _executeHandler = handler ?? _defaultExecutHandler; + let _interruptHandler: ((this: vscode.NotebookController, notebook: vscode.NotebookDocument) => void | Thenable) | undefined; // todo@jrieken the selector needs to be massaged this._proxy.$addKernel(handle, data).catch(err => { @@ -91,10 +106,13 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { }); }; + // notebook documents that are associated to this controller + const associatedNotebooks = new ResourceMap(); + const controller: vscode.NotebookController = { get id() { return id; }, - get viewType() { return data.viewType; }, - onDidChangeNotebookAssociation: onDidChangeSelection.event, + get notebookType() { return data.notebookType; }, + onDidChangeSelectedNotebooks: onDidChangeSelection.event, get label() { return data.label; }, @@ -123,15 +141,15 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { data.supportedLanguages = value; _update(); }, - get hasExecutionOrder() { - return data.hasExecutionOrder ?? false; + get supportsExecutionOrder() { + return data.supportsExecutionOrder ?? false; }, - set hasExecutionOrder(value) { - data.hasExecutionOrder = value; + set supportsExecutionOrder(value) { + data.supportsExecutionOrder = value; _update(); }, - get preloads() { - return data.preloads ? data.preloads.map(extHostTypeConverters.NotebookKernelPreload.to) : []; + get rendererScripts() { + return data.preloads ? data.preloads.map(extHostTypeConverters.NotebookRendererScript.to) : []; }, get executeHandler() { return _executeHandler; @@ -147,15 +165,19 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { data.supportsInterrupt = Boolean(value); _update(); }, - createNotebookCellExecutionTask(cell) { + createNotebookCellExecution(cell) { if (isDisposed) { throw new Error('notebook controller is DISPOSED'); } - //todo@jrieken - return that._extHostNotebook.createNotebookCellExecution(cell.notebook.uri, cell.index, data.id)!; + if (!associatedNotebooks.has(cell.notebook.uri)) { + that._logService.trace(`NotebookController[${handle}] NOT associated to notebook, associated to THESE notebooks:`, Array.from(associatedNotebooks.keys()).map(u => u.toString())); + throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); + } + return that._createNotebookCellExecution(cell); }, dispose: () => { if (!isDisposed) { + this._logService.trace(`NotebookController[${handle}], DISPOSED`); isDisposed = true; this._kernelData.delete(handle); commandDisposables.dispose(); @@ -164,30 +186,47 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { this._proxy.$removeKernel(handle); } }, + // --- priority + updateNotebookAffinity(notebook, priority) { + that._proxy.$updateNotebookPriority(handle, notebook.uri, priority); + }, // --- ipc onDidReceiveMessage: onDidReceiveMessage.event, postMessage(message, editor) { + checkProposedApiEnabled(extension); return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, asWebviewUri(uri: URI) { - return asWebviewUri(that._initData.environment, String(handle), uri); + checkProposedApiEnabled(extension); + return asWebviewUri(uri, that._initData.remote); }, - // --- priority - updateNotebookAffinity(notebook, priority) { - that._proxy.$updateNotebookPriority(handle, notebook.uri, priority); - } }; - this._kernelData.set(handle, { extensionId: extension.identifier, controller, onDidChangeSelection, onDidReceiveMessage }); + this._kernelData.set(handle, { + extensionId: extension.identifier, + controller, + onDidReceiveMessage, + onDidChangeSelection, + associatedNotebooks + }); return controller; } - $acceptSelection(handle: number, uri: UriComponents, value: boolean): void { + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this._kernelData.get(handle); if (obj) { + // update data structure + const notebook = this._extHostNotebook.getNotebookDocument(URI.revive(uri))!; + if (value) { + obj.associatedNotebooks.set(notebook.uri, true); + } else { + obj.associatedNotebooks.delete(notebook.uri); + } + this._logService.trace(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), value); + // send event obj.onDidChangeSelection.fire({ selected: value, - notebook: this._extHostNotebook.lookupNotebookDocument(URI.revive(uri))!.apiNotebook + notebook: notebook.apiNotebook }); } } @@ -198,11 +237,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { // extension can dispose kernels in the meantime return; } - const document = this._extHostNotebook.lookupNotebookDocument(URI.revive(uri)); - if (!document) { - throw new Error('MISSING notebook'); - } - + const document = this._extHostNotebook.getNotebookDocument(URI.revive(uri)); const cells: vscode.NotebookCell[] = []; for (let cellHandle of handles) { const cell = document.getCell(cellHandle); @@ -212,9 +247,11 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } try { + this._logService.trace(`NotebookController[${handle}] EXECUTE cells`, document.uri.toString(), cells.length); await obj.controller.executeHandler.call(obj.controller, cells, document.apiNotebook, obj.controller); } catch (err) { // + this._logService.error(`NotebookController[${handle}] execute cells FAILED`, err); console.error(err); } } @@ -225,24 +262,24 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { // extension can dispose kernels in the meantime return; } - const document = this._extHostNotebook.lookupNotebookDocument(URI.revive(uri)); - if (!document) { - throw new Error('MISSING notebook'); - } + + // cancel or interrupt depends on the controller. When an interrupt handler is used we + // don't trigger the cancelation token of executions. + const document = this._extHostNotebook.getNotebookDocument(URI.revive(uri)); if (obj.controller.interruptHandler) { await obj.controller.interruptHandler.call(obj.controller, document.apiNotebook); - } - // we do both? interrupt and cancellation or should we be selective? - for (let cellHandle of handles) { - const cell = document.getCell(cellHandle); - if (cell) { - this._extHostNotebook.cancelOneNotebookCellExecution(cell); + } else { + for (let cellHandle of handles) { + const cell = document.getCell(cellHandle); + if (cell) { + this._activeExecutions.get(cell.uri)?.cancel(); + } } } } - $acceptRendererMessage(handle: number, editorId: string, message: any): void { + $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void { const obj = this._kernelData.get(handle); if (!obj) { // extension can dispose kernels in the meantime @@ -256,4 +293,229 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { obj.onDidReceiveMessage.fire(Object.freeze({ editor: editor.apiEditor, message })); } + + // --- + + _createNotebookCellExecution(cell: vscode.NotebookCell): vscode.NotebookCellExecution { + if (cell.index < 0) { + throw new Error('CANNOT execute cell that has been REMOVED from notebook'); + } + const notebook = this._extHostNotebook.getNotebookDocument(cell.notebook.uri); + const cellObj = notebook.getCellFromApiCell(cell); + if (!cellObj) { + throw new Error('invalid cell'); + } + if (this._activeExecutions.has(cellObj.uri)) { + throw new Error(`duplicate execution for ${cellObj.uri}`); + } + const execution = new NotebookCellExecutionTask(cellObj.notebook, cellObj, this._mainContext.getProxy(MainContext.MainThreadNotebookDocuments)); + this._activeExecutions.set(cellObj.uri, execution); + const listener = execution.onDidChangeState(() => { + if (execution.state === NotebookCellExecutionTaskState.Resolved) { + execution.dispose(); + listener.dispose(); + this._activeExecutions.delete(cellObj.uri); + } + }); + return execution.asApiObject(); + } +} + + +enum NotebookCellExecutionTaskState { + Init, + Started, + Resolved +} + +class NotebookCellExecutionTask extends Disposable { + private _onDidChangeState = new Emitter(); + readonly onDidChangeState = this._onDidChangeState.event; + + private _state = NotebookCellExecutionTaskState.Init; + get state(): NotebookCellExecutionTaskState { return this._state; } + + private readonly _tokenSource = this._register(new CancellationTokenSource()); + + private readonly _collector: TimeoutBasedCollector; + + private _executionOrder: number | undefined; + + constructor( + private readonly _document: ExtHostNotebookDocument, + private readonly _cell: ExtHostCell, + private readonly _proxy: MainThreadNotebookDocumentsShape + ) { + super(); + + this._collector = new TimeoutBasedCollector(10, edits => this.applyEdits(edits)); + + this._executionOrder = _cell.internalMetadata.executionOrder; + this.mixinMetadata({ + runState: NotebookCellExecutionState.Pending, + executionOrder: null + }); + } + + cancel(): void { + this._tokenSource.cancel(); + } + + private async applyEditSoon(edit: IImmediateCellEditOperation): Promise { + await this._collector.addItem(edit); + } + + private async applyEdits(edits: IImmediateCellEditOperation[]): Promise { + return this._proxy.$applyEdits(this._document.uri, edits, false); + } + + private verifyStateForOutput() { + if (this._state === NotebookCellExecutionTaskState.Init) { + throw new Error('Must call start before modifying cell output'); + } + + if (this._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot modify cell output after calling resolve'); + } + } + + private mixinMetadata(mixinMetadata: NullablePartialNotebookCellInternalMetadata) { + const edit: IImmediateCellEditOperation = { editType: CellEditType.PartialInternalMetadata, handle: this._cell.handle, internalMetadata: mixinMetadata }; + this.applyEdits([edit]); + } + + private cellIndexToHandle(cellOrCellIndex: vscode.NotebookCell | number | undefined): number { + let cell: ExtHostCell | undefined = this._cell; + if (typeof cellOrCellIndex === 'number') { + // todo@jrieken remove support for number shortly + cell = this._document.getCellFromIndex(cellOrCellIndex); + } else if (cellOrCellIndex) { + cell = this._document.getCellFromApiCell(cellOrCellIndex); + } + if (!cell) { + throw new Error('INVALID cell'); + } + return cell.handle; + } + + private validateAndConvertOutputs(items: vscode.NotebookCellOutput[]): IOutputDto[] { + return items.map(output => { + const newOutput = NotebookCellOutput.ensureUniqueMimeTypes(output.items, true); + if (newOutput === output.items) { + return extHostTypeConverters.NotebookCellOutput.from(output); + } + return extHostTypeConverters.NotebookCellOutput.from({ + items: newOutput, + id: output.id, + metadata: output.metadata + }); + }); + } + + private async updateOutputs(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell: vscode.NotebookCell | number | undefined, append: boolean): Promise { + const handle = this.cellIndexToHandle(cell); + const outputDtos = this.validateAndConvertOutputs(asArray(outputs)); + return this.applyEditSoon({ editType: CellEditType.Output, handle, append, outputs: outputDtos }); + } + + private async updateOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], outputOrOutputId: vscode.NotebookCellOutput | string, append: boolean): Promise { + if (NotebookCellOutput.isNotebookCellOutput(outputOrOutputId)) { + outputOrOutputId = outputOrOutputId.id; + } + items = NotebookCellOutput.ensureUniqueMimeTypes(asArray(items), true); + return this.applyEditSoon({ editType: CellEditType.OutputItems, items: items.map(extHostTypeConverters.NotebookCellOutputItem.from), outputId: outputOrOutputId, append }); + } + + asApiObject(): vscode.NotebookCellExecution { + const that = this; + const result: vscode.NotebookCellExecution = { + get token() { return that._tokenSource.token; }, + get cell() { return that._cell.apiCell; }, + get executionOrder() { return that._executionOrder; }, + set executionOrder(v: number | undefined) { + that._executionOrder = v; + that.mixinMetadata({ + executionOrder: v + }); + }, + + start(startTime?: number): void { + if (that._state === NotebookCellExecutionTaskState.Resolved || that._state === NotebookCellExecutionTaskState.Started) { + throw new Error('Cannot call start again'); + } + + that._state = NotebookCellExecutionTaskState.Started; + that._onDidChangeState.fire(); + + that.mixinMetadata({ + runState: NotebookCellExecutionState.Executing, + runStartTime: startTime ?? null + }); + }, + + end(success: boolean | undefined, endTime?: number): void { + if (that._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot call resolve twice'); + } + + that._state = NotebookCellExecutionTaskState.Resolved; + that._onDidChangeState.fire(); + + that.mixinMetadata({ + runState: null, + lastRunSuccess: success ?? null, + runEndTime: endTime ?? null, + }); + }, + + clearOutput(cell?: vscode.NotebookCell | number): Thenable { + that.verifyStateForOutput(); + return that.updateOutputs([], cell, false); + }, + + appendOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell?: vscode.NotebookCell | number): Promise { + that.verifyStateForOutput(); + return that.updateOutputs(outputs, cell, true); + }, + + replaceOutput(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell?: vscode.NotebookCell | number): Promise { + that.verifyStateForOutput(); + return that.updateOutputs(outputs, cell, false); + }, + + appendOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], output: vscode.NotebookCellOutput | string): Promise { + that.verifyStateForOutput(); + return that.updateOutputItems(items, output, true); + }, + + replaceOutputItems(items: vscode.NotebookCellOutputItem | vscode.NotebookCellOutputItem[], output: vscode.NotebookCellOutput | string): Promise { + that.verifyStateForOutput(); + return that.updateOutputItems(items, output, false); + } + }; + return Object.freeze(result); + } +} + +class TimeoutBasedCollector { + private batch: T[] = []; + private waitPromise: Promise | undefined; + + constructor( + private readonly delay: number, + private readonly callback: (items: T[]) => Promise) { } + + addItem(item: T): Promise { + this.batch.push(item); + if (!this.waitPromise) { + this.waitPromise = timeout(this.delay).then(() => { + this.waitPromise = undefined; + const batch = this.batch; + this.batch = []; + return this.callback(batch); + }); + } + + return this.waitPromise; + } } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostNotebookRenderers.ts b/lib/vscode/src/vs/workbench/api/common/extHostNotebookRenderers.ts new file mode 100644 index 000000000000..fcf5eb9cfd51 --- /dev/null +++ b/lib/vscode/src/vs/workbench/api/common/extHostNotebookRenderers.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { ExtHostNotebookRenderersShape, IMainContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookEditor } from 'vs/workbench/api/common/extHostNotebookEditor'; +import * as vscode from 'vscode'; + +export class ExtHostNotebookRenderers implements ExtHostNotebookRenderersShape { + private readonly _rendererMessageEmitters = new Map>>(); + private readonly proxy: MainThreadNotebookRenderersShape; + + constructor(mainContext: IMainContext, private readonly _extHostNotebook: ExtHostNotebookController) { + this.proxy = mainContext.getProxy(MainContext.MainThreadNotebookRenderers); + } + + public $postRendererMessage(editorId: string, rendererId: string, message: unknown): void { + const editor = this._extHostNotebook.getEditorById(editorId); + if (!editor) { + return; + } + + this._rendererMessageEmitters.get(rendererId)?.fire({ editor: editor.apiEditor, message }); + } + + public createRendererMessaging(rendererId: string): vscode.NotebookRendererMessaging { + const messaging: vscode.NotebookRendererMessaging = { + onDidReceiveMessage: (...args) => + this.getOrCreateEmitterFor(rendererId).event(...args), + postMessage: (editor, message) => { + const extHostEditor = ExtHostNotebookEditor.apiEditorsToExtHost.get(editor); + if (!extHostEditor) { + throw new Error(`The first argument to postMessage() must be a NotebookEditor`); + } + + this.proxy.$postMessage(extHostEditor.id, rendererId, message); + }, + }; + + return messaging; + } + + private getOrCreateEmitterFor(rendererId: string) { + let emitter = this._rendererMessageEmitters.get(rendererId); + if (emitter) { + return emitter; + } + + emitter = new Emitter({ + onLastListenerRemove: () => { + emitter?.dispose(); + this._rendererMessageEmitters.delete(rendererId); + } + }); + + this._rendererMessageEmitters.set(rendererId, emitter); + + return emitter; + } +} diff --git a/lib/vscode/src/vs/workbench/api/common/extHostStatusBar.ts b/lib/vscode/src/vs/workbench/api/common/extHostStatusBar.ts index ba9c5b184ae8..f85f182011ed 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostStatusBar.ts @@ -10,8 +10,10 @@ import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from import { localize } from 'vs/nls'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { + private static ID_GEN = 0; private static ALLOWED_BACKGROUND_COLORS = new Map( @@ -21,17 +23,20 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { #proxy: MainThreadStatusBarShape; #commands: CommandsConverter; - private _id: number; + private _entryId: number; + + private _extension?: IExtensionDescription; + + private _id?: string; private _alignment: number; private _priority?: number; + private _disposed: boolean = false; private _visible: boolean = false; - private _statusId: string; - private _statusName: string; - private _text: string = ''; private _tooltip?: string; + private _name?: string; private _color?: string | ThemeColor; private _backgroundColor?: ThemeColor; private readonly _internalCommandRegistration = new DisposableStore(); @@ -43,20 +48,23 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { this.#proxy = proxy; this.#commands = commands; - this._id = ExtHostStatusBarEntry.ID_GEN++; - this._statusId = id; - this._statusName = name; + this._entryId = ExtHostStatusBarEntry.ID_GEN++; + + this._extension = extension; + + this._id = id; this._alignment = alignment; this._priority = priority; - this._accessibilityInformation = accessibilityInformation; } - public get id(): number { - return this._id; + public get id(): string { + return this._id ?? this._extension!.identifier.value; } public get alignment(): vscode.StatusBarAlignment { @@ -71,6 +79,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._text; } + public get name(): string | undefined { + return this._name; + } + public get tooltip(): string | undefined { return this._tooltip; } @@ -96,6 +108,11 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this.update(); } + public set name(name: string | undefined) { + this._name = name; + this.update(); + } + public set tooltip(tooltip: string | undefined) { this._tooltip = tooltip; this.update(); @@ -150,7 +167,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { public hide(): void { clearTimeout(this._timeoutHandle); this._visible = false; - this.#proxy.$dispose(this.id); + this.#proxy.$dispose(this._entryId); } private update(): void { @@ -164,6 +181,28 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._timeoutHandle = setTimeout(() => { this._timeoutHandle = undefined; + // If the id is not set, derive it from the extension identifier, + // otherwise make sure to prefix it with the extension identifier + // to get a more unique value across extensions. + let id: string; + if (this._extension) { + if (this._id) { + id = `${this._extension.identifier.value}.${this._id}`; + } else { + id = this._extension.identifier.value; + } + } else { + id = this._id!; + } + + // If the name is not set, derive it from the extension descriptor + let name: string; + if (this._name) { + name = this._name; + } else { + name = localize('extensionLabel', "{0} (Extension)", this._extension!.displayName || this._extension!.name); + } + // If a background color is set, the foreground is determined let color = this._color; if (this._backgroundColor) { @@ -171,7 +210,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } // Set to status bar - this.#proxy.$setEntry(this.id, this._statusId, this._statusName, this._text, this._tooltip, this._command?.internal, color, + this.#proxy.$setEntry(this._entryId, id, name, this._text, this._tooltip, this._command?.internal, color, this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority, this._accessibilityInformation); }, 0); @@ -189,7 +228,8 @@ class StatusBarMessage { private _messages: { message: string }[] = []; constructor(statusBar: ExtHostStatusBar) { - this._item = statusBar.createStatusBarEntry('status.extensionMessage', localize('status.extensionMessage', "Extension Status"), ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item = statusBar.createStatusBarEntry(undefined, 'status.extensionMessage', ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item.name = localize('status.extensionMessage', "Extension Status"); } dispose() { @@ -233,12 +273,13 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation); + createStatusBarEntry(extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; + createStatusBarEntry(extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; + createStatusBarEntry(extension: IExtensionDescription, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, extension, id, alignment, priority); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { - const d = this._statusMessage.setMessage(text); let handle: any; diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTask.ts b/lib/vscode/src/vs/workbench/api/common/extHostTask.ts index 35e824bad1dd..8f5d4a159774 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTask.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTask.ts @@ -605,8 +605,6 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask public abstract $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }>; - public abstract $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }>; - private nextHandle(): number { return this._handleCounter++; } @@ -775,10 +773,6 @@ export class WorkerExtHostTask extends ExtHostTaskBase { return result; } - public $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> { - throw new Error('Not implemented'); - } - public async $jsonTasksSupported(): Promise { return false; } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTerminalService.ts b/lib/vscode/src/vs/workbench/api/common/extHostTerminalService.ts index a10d6b973b4d..23b91c7f5058 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTerminalService.ts @@ -5,13 +5,12 @@ import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType } from './extHostTypes'; +import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, ThemeColor } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; @@ -19,9 +18,10 @@ import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/ter import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { generateUuid } from 'vs/base/common/uuid'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; -import { ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { withNullAsUndefined } from 'vs/base/common/types'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -37,15 +37,22 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID onDidWriteTerminalData: Event; createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; - createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal; + createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; - getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; - getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; + getDefaultShell(useAutomationShell: boolean): string; + getDefaultShellArgs(useAutomationShell: boolean): string[] | string; registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable; + registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable; getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection; } +export interface ITerminalInternalOptions { + isFeatureTerminal?: boolean; + useShellEnvironment?: boolean; + isSplitTerminal?: boolean; +} + export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); export class ExtHostTerminal { @@ -114,29 +121,39 @@ export class ExtHostTerminal { } public async create( - shellPath?: string, - shellArgs?: string[] | string, - cwd?: string | URI, - env?: ITerminalEnvironment, - icon?: string, - initialText?: string, - waitOnExit?: boolean, - strictEnv?: boolean, - hideFromUser?: boolean, - isFeatureTerminal?: boolean, - isExtensionOwnedTerminal?: boolean + options: vscode.TerminalOptions, + internalOptions?: ITerminalInternalOptions, ): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } - await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, icon, initialText, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal, isExtensionOwnedTerminal }); + await this._proxy.$createTerminal(this._id, { + name: options.name, + shellPath: withNullAsUndefined(options.shellPath), + shellArgs: withNullAsUndefined(options.shellArgs), + cwd: withNullAsUndefined(options.cwd), + env: withNullAsUndefined(options.env), + icon: withNullAsUndefined(asTerminalIcon(options.iconPath)), + initialText: withNullAsUndefined(options.message), + strictEnv: withNullAsUndefined(options.strictEnv), + hideFromUser: withNullAsUndefined(options.hideFromUser), + isFeatureTerminal: withNullAsUndefined(internalOptions?.isFeatureTerminal), + isExtensionOwnedTerminal: true, + useShellEnvironment: withNullAsUndefined(internalOptions?.useShellEnvironment), + isSplitTerminal: withNullAsUndefined(internalOptions?.isSplitTerminal) + }); } - public async createExtensionTerminal(): Promise { + public async createExtensionTerminal(isSplitTerminal?: boolean, iconPath?: URI | { light: URI; dark: URI } | ThemeIcon): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } - await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionCustomPtyTerminal: true }); + await this._proxy.$createTerminal(this._id, { + name: this._name, + isExtensionCustomPtyTerminal: true, + icon: iconPath, + isSplitTerminal + }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { throw new Error('Terminal creation failed'); @@ -195,8 +212,8 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { public readonly onProcessData: Event = this._onProcessData.event; private readonly _onProcessExit = new Emitter(); public readonly onProcessExit: Event = this._onProcessExit.event; - private readonly _onProcessReady = new Emitter<{ pid: number, cwd: string }>(); - public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } + private readonly _onProcessReady = new Emitter(); + public get onProcessReady(): Event { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = new Emitter(); public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; private readonly _onProcessOverrideDimensions = new Emitter(); @@ -259,6 +276,9 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { if (this._pty.onDidOverrideDimensions) { this._pty.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : e)); } + if (this._pty.onDidChangeName) { + this._pty.onDidChangeName(title => this._onProcessTitleChanged.fire(title)); + } this._pty.open(initialDimensions ? initialDimensions : undefined); @@ -289,9 +309,12 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I protected _extensionTerminalAwaitingStart: { [id: number]: { initialDimensions: ITerminalDimensionsDto | undefined } | undefined } = {}; protected _getTerminalPromises: { [id: number]: Promise } = {}; protected _environmentVariableCollections: Map = new Map(); + private _defaultProfile: ITerminalProfile | undefined; + private _defaultAutomationProfile: ITerminalProfile | undefined; private readonly _bufferer: TerminalDataBufferer; private readonly _linkProviders: Set = new Set(); + private readonly _profileProviders: Map = new Map(); private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); @@ -331,16 +354,22 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; - public abstract createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal; - public abstract getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; - public abstract getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; - public abstract $getAvailableProfiles(configuredProfilesOnly: boolean): Promise; - public abstract $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; + public abstract createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; + + public getDefaultShell(useAutomationShell: boolean): string { + const profile = useAutomationShell ? this._defaultAutomationProfile : this._defaultProfile; + return profile?.path || ''; + } + + public getDefaultShellArgs(useAutomationShell: boolean): string[] | string { + const profile = useAutomationShell ? this._defaultAutomationProfile : this._defaultProfile; + return profile?.args || []; + } - public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { + public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal().then(id => { + terminal.createExtensionTerminal(internalOptions?.isSplitTerminal, asTerminalIcon(options.iconPath)).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -555,6 +584,34 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } + public registerProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable { + if (this._profileProviders.has(id)) { + throw new Error(`Terminal profile provider "${id}" already registered`); + } + this._profileProviders.set(id, provider); + this._proxy.$registerProfileProvider(id); + return new VSCodeDisposable(() => { + this._profileProviders.delete(id); + this._proxy.$unregisterProfileProvider(id); + }); + } + + public async $createContributedProfileTerminal(id: string, isSplitTerminal: boolean): Promise { + const token = new CancellationTokenSource().token; + const options = await this._profileProviders.get(id)?.provideProfileOptions(token); + if (token.isCancellationRequested) { + return; + } + if (!options) { + throw new Error(`No terminal profile options provided for id "${id}"`); + } + if ('pty' in options) { + this.createExtensionTerminal(options, { isSplitTerminal }); + return; + } + this.createTerminalFromOptions(options, { isSplitTerminal }); + } + public async $provideLinks(terminalId: number, line: string): Promise { const terminal = this._getTerminalById(terminalId); if (!terminal) { @@ -686,6 +743,11 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } + public $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void { + this._defaultProfile = profile; + this._defaultAutomationProfile = automationProfile; + } + private _setEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { this._environmentVariableCollections.set(extensionIdentifier, collection); collection.onDidChangeCollection(() => { @@ -771,23 +833,20 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { throw new NotSupportedError(); } - public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { - throw new NotSupportedError(); - } - - public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { - throw new NotSupportedError(); - } - - public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { + public createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { throw new NotSupportedError(); } +} - public $getAvailableProfiles(configuredProfilesOnly: boolean): Promise { - throw new NotSupportedError(); +function asTerminalIcon(iconPath?: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon): TerminalIcon | undefined { + if (!iconPath) { + return undefined; } - - public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { - throw new NotSupportedError(); + if (!('id' in iconPath)) { + return iconPath; } + return { + id: iconPath.id, + color: iconPath.color as ThemeColor + }; } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTesting.ts b/lib/vscode/src/vs/workbench/api/common/extHostTesting.ts index 7afd44dfc346..46f08ad1d9f0 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTesting.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTesting.ts @@ -666,7 +666,7 @@ export class TestItemFilteredWrapper extends TestItemImpl { } } - const nowMatches = this.children.size > 0 || this.actual.uri.toString() === this.filterDocument.uri.toString(); + const nowMatches = this.children.size > 0 || this.actual.uri?.toString() === this.filterDocument.uri.toString(); this._cachedMatchesFilter = nowMatches; if (nowMatches !== didMatch) { diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTextEditor.ts b/lib/vscode/src/vs/workbench/api/common/extHostTextEditor.ts index f3dff81c5444..88a6ca5f7034 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTextEditor.ts @@ -15,6 +15,7 @@ import { EndOfLine, Position, Range, Selection, SnippetString, TextEditorLineNum import type * as vscode from 'vscode'; import { ILogService } from 'vs/platform/log/common/log'; import { Lazy } from 'vs/base/common/lazy'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class TextEditorDecorationType { @@ -22,9 +23,9 @@ export class TextEditorDecorationType { readonly value: vscode.TextEditorDecorationType; - constructor(proxy: MainThreadTextEditorsShape, options: vscode.DecorationRenderOptions) { + constructor(proxy: MainThreadTextEditorsShape, extension: IExtensionDescription, options: vscode.DecorationRenderOptions) { const key = TextEditorDecorationType._Keys.nextId(); - proxy.$registerTextEditorDecorationType(key, TypeConverters.DecorationRenderOptions.from(options)); + proxy.$registerTextEditorDecorationType(extension.identifier, key, TypeConverters.DecorationRenderOptions.from(options)); this.value = Object.freeze({ key, dispose() { diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTextEditors.ts b/lib/vscode/src/vs/workbench/api/common/extHostTextEditors.ts index dc8bd7bd9108..555b638a1158 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTextEditors.ts @@ -11,6 +11,7 @@ import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/co import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -91,8 +92,8 @@ export class ExtHostEditors implements ExtHostEditorsShape { } } - createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { - return new TextEditorDecorationType(this._proxy, options).value; + createTextEditorDecorationType(extension: IExtensionDescription, options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { + return new TextEditorDecorationType(this._proxy, extension, options).value; } // --- called from main thread diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTreeViews.ts b/lib/vscode/src/vs/workbench/api/common/extHostTreeViews.ts index bf2801705cfb..8b02f4ab8ea2 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTreeViews.ts @@ -84,7 +84,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { if (!options || !options.treeDataProvider) { throw new Error('Options with treeDataProvider is mandatory'); } - const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany }); + const canDragAndDrop = options.dragAndDropController !== undefined; + const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, canDragAndDrop: canDragAndDrop }); const treeView = this.createExtHostTreeView(viewId, options, extension); return { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, @@ -127,6 +128,14 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.getChildren(treeItemHandle); } + $onDrop(treeViewId: string, treeItemHandles: string[], newParentItemHandle: string): Promise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId))); + } + return treeView.onDrop(treeItemHandles, newParentItemHandle); + } + async $hasResolve(treeViewId: string): Promise { const treeView = this.treeViews.get(treeViewId); if (!treeView) { @@ -196,6 +205,7 @@ class ExtHostTreeView extends Disposable { private static readonly ID_HANDLE_PREFIX = '1'; private readonly dataProvider: vscode.TreeDataProvider; + private readonly dndController: vscode.DragAndDropController | undefined; private roots: TreeNode[] | null = null; private elements: Map = new Map(); @@ -242,6 +252,7 @@ class ExtHostTreeView extends Disposable { } } this.dataProvider = options.treeDataProvider; + this.dndController = options.dragAndDropController; if (this.dataProvider.onDidChangeTreeData) { this._register(this.dataProvider.onDidChangeTreeData(element => this._onDidChangeData.fire({ message: false, element }))); } @@ -369,6 +380,15 @@ class ExtHostTreeView extends Disposable { } } + onDrop(treeItemHandleOrNodes: TreeItemHandle[], targetHandleOrNode: TreeItemHandle): Promise { + const elements = treeItemHandleOrNodes.map(item => this.getExtensionElement(item)).filter(element => !isUndefinedOrNull(element)); + const target = this.getExtensionElement(targetHandleOrNode); + if (elements && target) { + return asPromise(() => this.dndController?.onDrop(elements, target)); + } + return Promise.resolve(undefined); + } + get hasResolve(): boolean { return !!this.dataProvider.resolveTreeItem; } diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTypeConverters.ts b/lib/vscode/src/vs/workbench/api/common/extHostTypeConverters.ts index df14612813d4..c5b64e74910a 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -9,7 +9,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import * as marked from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; -import { isDefined, isNumber, isString } from 'vs/base/common/types'; +import { isDefined, isEmptyObject, isNumber, isString } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; @@ -545,7 +545,7 @@ export namespace WorkspaceEdit { resource: entry.uri, edit: entry.edit, notebookMetadata: entry.notebookMetadata, - notebookVersionId: extHostNotebooks?.lookupNotebookDocument(entry.uri)?.apiNotebook.version + notebookVersionId: extHostNotebooks?.getNotebookDocument(entry.uri, true)?.apiNotebook.version }); } else if (entry._type === types.FileEditType.CellOutput) { @@ -580,7 +580,7 @@ export namespace WorkspaceEdit { _type: extHostProtocol.WorkspaceEditType.Cell, metadata: entry.metadata, resource: entry.uri, - notebookVersionId: extHostNotebooks?.lookupNotebookDocument(entry.uri)?.apiNotebook.version, + notebookVersionId: extHostNotebooks?.getNotebookDocument(entry.uri, true)?.apiNotebook.version, edit: { editType: notebooks.CellEditType.Replace, index: entry.index, @@ -1130,37 +1130,35 @@ export namespace SignatureHelp { } } -export namespace InlineHint { +export namespace InlayHint { - export function from(hint: vscode.InlineHint): modes.InlineHint { + export function from(hint: vscode.InlayHint): modes.InlayHint { return { text: hint.text, - range: Range.from(hint.range), - kind: InlineHintKind.from(hint.kind ?? types.InlineHintKind.Other), - description: hint.description && MarkdownString.fromStrict(hint.description), + position: Position.from(hint.position), + kind: InlayHintKind.from(hint.kind ?? types.InlayHintKind.Other), whitespaceBefore: hint.whitespaceBefore, whitespaceAfter: hint.whitespaceAfter }; } - export function to(hint: modes.InlineHint): vscode.InlineHint { - const res = new types.InlineHint( + export function to(hint: modes.InlayHint): vscode.InlayHint { + const res = new types.InlayHint( hint.text, - Range.to(hint.range), - InlineHintKind.to(hint.kind) + Position.to(hint.position), + InlayHintKind.to(hint.kind) ); res.whitespaceAfter = hint.whitespaceAfter; res.whitespaceBefore = hint.whitespaceBefore; - res.description = htmlContent.isMarkdownString(hint.description) ? MarkdownString.to(hint.description) : hint.description; return res; } } -export namespace InlineHintKind { - export function from(kind: vscode.InlineHintKind): modes.InlineHintKind { +export namespace InlayHintKind { + export function from(kind: vscode.InlayHintKind): modes.InlayHintKind { return kind; } - export function to(kind: modes.InlineHintKind): vscode.InlineHintKind { + export function to(kind: modes.InlayHintKind): vscode.InlayHintKind { return kind; } } @@ -1417,49 +1415,20 @@ export namespace NotebookRange { } } -export namespace NotebookCellMetadata { - - export function to(data: notebooks.NotebookCellMetadata): types.NotebookCellMetadata { - return new types.NotebookCellMetadata().with({ - ...data, - ...{ - executionOrder: null, - lastRunSuccess: null, - runState: null, - runStartTime: null, - runStartTimeAdjustment: null, - runEndTime: null - } - }); - } -} - -export namespace NotebookDocumentMetadata { - - export function from(data: types.NotebookDocumentMetadata): notebooks.NotebookDocumentMetadata { - return data; - } - - export function to(data: notebooks.NotebookDocumentMetadata): types.NotebookDocumentMetadata { - return new types.NotebookDocumentMetadata().with(data); - } -} - -export namespace NotebookCellPreviousExecutionResult { - export function to(data: notebooks.NotebookCellMetadata): vscode.NotebookCellExecutionSummary { +export namespace NotebookCellExecutionSummary { + export function to(data: notebooks.NotebookCellInternalMetadata): vscode.NotebookCellExecutionSummary { return { - startTime: data.runStartTime, - endTime: data.runEndTime, + timing: typeof data.runStartTime === 'number' && typeof data.runEndTime === 'number' ? { startTime: data.runStartTime, endTime: data.runEndTime } : undefined, executionOrder: data.executionOrder, success: data.lastRunSuccess }; } - export function from(data: vscode.NotebookCellExecutionSummary): Partial { + export function from(data: vscode.NotebookCellExecutionSummary): Partial { return { lastRunSuccess: data.success, - runStartTime: data.startTime, - runEndTime: data.endTime, + runStartTime: data.timing?.startTime, + runEndTime: data.timing?.endTime, executionOrder: data.executionOrder }; } @@ -1468,8 +1437,8 @@ export namespace NotebookCellPreviousExecutionResult { export namespace NotebookCellKind { export function from(data: vscode.NotebookCellKind): notebooks.CellKind { switch (data) { - case types.NotebookCellKind.Markdown: - return notebooks.CellKind.Markdown; + case types.NotebookCellKind.Markup: + return notebooks.CellKind.Markup; case types.NotebookCellKind.Code: default: return notebooks.CellKind.Code; @@ -1478,8 +1447,8 @@ export namespace NotebookCellKind { export function to(data: notebooks.CellKind): vscode.NotebookCellKind { switch (data) { - case notebooks.CellKind.Markdown: - return types.NotebookCellKind.Markdown; + case notebooks.CellKind.Markup: + return types.NotebookCellKind.Markup; case notebooks.CellKind.Code: default: return types.NotebookCellKind.Code; @@ -1487,17 +1456,40 @@ export namespace NotebookCellKind { } } +export namespace NotebookData { + + export function from(data: vscode.NotebookData): notebooks.NotebookDataDto { + const res: notebooks.NotebookDataDto = { + metadata: data.metadata ?? Object.create(null), + cells: [], + }; + for (let cell of data.cells) { + types.NotebookCellData.validate(cell); + res.cells.push(NotebookCellData.from(cell)); + } + return res; + } + + export function to(data: notebooks.NotebookDataDto): vscode.NotebookData { + const res = new types.NotebookData( + data.cells.map(NotebookCellData.to), + ); + if (!isEmptyObject(data.metadata)) { + res.metadata = data.metadata; + } + return res; + } +} + export namespace NotebookCellData { export function from(data: vscode.NotebookCellData): notebooks.ICellDto2 { return { cellKind: NotebookCellKind.from(data.kind), - language: data.language, - source: data.source, - metadata: { - ...data.metadata, - ...NotebookCellPreviousExecutionResult.from(data.latestExecutionSummary ?? {}) - }, + language: data.languageId, + source: data.value, + metadata: data.metadata, + internalMetadata: NotebookCellExecutionSummary.from(data.executionSummary ?? {}), outputs: data.outputs ? data.outputs.map(NotebookCellOutput.from) : [] }; } @@ -1508,7 +1500,8 @@ export namespace NotebookCellData { data.source, data.language, data.outputs ? data.outputs.map(NotebookCellOutput.to) : undefined, - data.metadata ? NotebookCellMetadata.to(data.metadata) : undefined, + data.metadata, + data.internalMetadata ? NotebookCellExecutionSummary.to(data.internalMetadata) : undefined ); } } @@ -1517,21 +1510,20 @@ export namespace NotebookCellOutputItem { export function from(item: types.NotebookCellOutputItem): notebooks.IOutputItemDto { return { mime: item.mime, - value: item.value, - metadata: item.metadata + valueBytes: Array.from(item.data), //todo@jrieken this HACKY and SLOW... hoist VSBuffer instead }; } export function to(item: notebooks.IOutputItemDto): types.NotebookCellOutputItem { - return new types.NotebookCellOutputItem(item.mime, item.value, item.metadata); + return new types.NotebookCellOutputItem(new Uint8Array(item.valueBytes), item.mime); } } export namespace NotebookCellOutput { - export function from(output: types.NotebookCellOutput): notebooks.IOutputDto { + export function from(output: vscode.NotebookCellOutput): notebooks.IOutputDto { return { outputId: output.id, - outputs: output.outputs.map(NotebookCellOutputItem.from), + outputs: output.items.map(NotebookCellOutputItem.from), metadata: output.metadata }; } @@ -1644,35 +1636,22 @@ export namespace NotebookDocumentContentOptions { export function from(options: vscode.NotebookDocumentContentOptions | undefined): notebooks.TransientOptions { return { transientOutputs: options?.transientOutputs ?? false, - transientCellMetadata: { - ...options?.transientCellMetadata, - executionOrder: true, - runState: true, - runStartTime: true, - runStartTimeAdjustment: true, - runEndTime: true, - lastRunSuccess: true - }, + transientCellMetadata: options?.transientCellMetadata ?? {}, transientDocumentMetadata: options?.transientDocumentMetadata ?? {} }; } } -export namespace NotebookKernelPreload { - export function from(preload: vscode.NotebookKernelPreload): { uri: UriComponents; provides: string[] } { +export namespace NotebookRendererScript { + export function from(preload: vscode.NotebookRendererScript): { uri: UriComponents; provides: string[] } { return { uri: preload.uri, - provides: typeof preload.provides === 'string' - ? [preload.provides] - : preload.provides ?? [] - }; - } - export function to(preload: { uri: UriComponents; provides: string[] }): vscode.NotebookKernelPreload { - return { - uri: URI.revive(preload.uri), provides: preload.provides }; } + export function to(preload: { uri: UriComponents; provides: string[] }): vscode.NotebookRendererScript { + return new types.NotebookRendererScript(URI.revive(preload.uri), preload.provides); + } } export namespace TestMessage { diff --git a/lib/vscode/src/vs/workbench/api/common/extHostTypes.ts b/lib/vscode/src/vs/workbench/api/common/extHostTypes.ts index 85e8d64cc59c..ed30c969a705 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostTypes.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostTypes.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesceInPlace, equals } from 'vs/base/common/arrays'; +import { asArray, coalesceInPlace, equals } from 'vs/base/common/arrays'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString, MarkdownString as BaseMarkdownString } from 'vs/base/common/htmlContent'; import { ReadonlyMapView, ResourceMap } from 'vs/base/common/map'; -import { isFalsyOrWhitespace } from 'vs/base/common/strings'; -import { isStringArray } from 'vs/base/common/types'; +import { normalizeMimeType } from 'vs/base/common/mime'; +import { isArray, isStringArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; @@ -612,7 +612,7 @@ export interface IFileCellEdit { _type: FileEditType.Cell; uri: URI; edit?: ICellEditOperation; - notebookMetadata?: vscode.NotebookDocumentMetadata; + notebookMetadata?: Record; metadata?: vscode.WorkspaceEditEntryMetadata; } @@ -631,7 +631,7 @@ export interface ICellOutputEdit { index: number; append: boolean; newOutputs?: NotebookCellOutput[]; - newMetadata?: vscode.NotebookCellMetadata; + newMetadata?: Record; metadata?: vscode.WorkspaceEditEntryMetadata; } @@ -674,7 +674,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // --- notebook - replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + replaceNotebookMetadata(uri: URI, value: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: value }, notebookMetadata: value }); } @@ -707,7 +707,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } } - replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.PartialMetadata, index, metadata: cellMetadata } }); } @@ -1420,24 +1420,23 @@ export enum SignatureHelpTriggerKind { } -export enum InlineHintKind { +export enum InlayHintKind { Other = 0, Type = 1, Parameter = 2, } @es5ClassCompat -export class InlineHint { +export class InlayHint { text: string; - range: Range; - kind?: vscode.InlineHintKind; - description?: string | vscode.MarkdownString; + position: Position; + kind?: vscode.InlayHintKind; whitespaceBefore?: boolean; whitespaceAfter?: boolean; - constructor(text: string, range: Range, kind?: vscode.InlineHintKind) { + constructor(text: string, position: Position, kind?: vscode.InlayHintKind) { this.text = text; - this.range = range; + this.position = position; this.kind = kind; } } @@ -1548,6 +1547,29 @@ export class CompletionList { } } +@es5ClassCompat +export class InlineSuggestion implements vscode.InlineCompletionItem { + + text: string; + range?: Range; + command?: vscode.Command; + + constructor(text: string, range?: Range, command?: vscode.Command) { + this.text = text; + this.range = range; + this.command = command; + } +} + +@es5ClassCompat +export class InlineSuggestions implements vscode.InlineCompletionList { + items: vscode.InlineCompletionItem[]; + + constructor(items: vscode.InlineCompletionItem[]) { + this.items = items; + } +} + export enum ViewColumn { Active = -1, Beside = -2, @@ -2439,6 +2461,11 @@ export class EvaluatableExpression implements vscode.EvaluatableExpression { } } +export enum InlineCompletionTriggerKind { + Automatic = 0, + Explicit = 1, +} + @es5ClassCompat export class InlineValueText implements vscode.InlineValueText { readonly range: Range; @@ -2956,102 +2983,19 @@ export class NotebookRange { } } -export class NotebookCellMetadata { - readonly inputCollapsed?: boolean; - readonly outputCollapsed?: boolean; - readonly [key: string]: any; - - constructor(inputCollapsed?: boolean, outputCollapsed?: boolean); - constructor(data: Record); - constructor(inputCollapsedOrData: (boolean | undefined) | Record, outputCollapsed?: boolean) { - if (typeof inputCollapsedOrData === 'object') { - Object.assign(this, inputCollapsedOrData); - } else { - this.inputCollapsed = inputCollapsedOrData; - this.outputCollapsed = outputCollapsed; - } - } - - with(change: { - inputCollapsed?: boolean | null, - outputCollapsed?: boolean | null, - [key: string]: any - }): NotebookCellMetadata { - - let { inputCollapsed, outputCollapsed, ...remaining } = change; - - if (inputCollapsed === undefined) { - inputCollapsed = this.inputCollapsed; - } else if (inputCollapsed === null) { - inputCollapsed = undefined; - } - if (outputCollapsed === undefined) { - outputCollapsed = this.outputCollapsed; - } else if (outputCollapsed === null) { - outputCollapsed = undefined; - } - - if (inputCollapsed === this.inputCollapsed && - outputCollapsed === this.outputCollapsed && - Object.keys(remaining).length === 0 - ) { - return this; - } - - return new NotebookCellMetadata( - { - inputCollapsed, - outputCollapsed, - ...remaining - } - ); - } -} - -export class NotebookDocumentMetadata { - readonly trusted: boolean; - readonly [key: string]: any; +export class NotebookCellData { - constructor(trusted?: boolean); - constructor(data: Record); - constructor(trustedOrData: boolean | Record = true) { - if (typeof trustedOrData === 'object') { - Object.assign(this, trustedOrData); - this.trusted = trustedOrData.trusted ?? true; - } else { - this.trusted = trustedOrData; + static validate(data: NotebookCellData): void { + if (typeof data.kind !== 'number') { + throw new Error('NotebookCellData MUST have \'kind\' property'); } - } - - with(change: { - trusted?: boolean | null, - [key: string]: any - }): NotebookDocumentMetadata { - - let { trusted, ...remaining } = change; - - if (trusted === undefined) { - trusted = this.trusted; - } else if (trusted === null) { - trusted = undefined; + if (typeof data.value !== 'string') { + throw new Error('NotebookCellData MUST have \'value\' property'); } - - if (trusted === this.trusted && - Object.keys(remaining).length === 0 - ) { - return this; + if (typeof data.languageId !== 'string') { + throw new Error('NotebookCellData MUST have \'languageId\' property'); } - - return new NotebookDocumentMetadata( - { - trusted, - ...remaining - } - ); } -} - -export class NotebookCellData { static isNotebookCellDataArray(value: unknown): value is vscode.NotebookCellData[] { return Array.isArray(value) && (value).every(elem => NotebookCellData.isNotebookCellData(elem)); @@ -3063,30 +3007,31 @@ export class NotebookCellData { } kind: NotebookCellKind; - source: string; - language: string; - outputs?: NotebookCellOutput[]; - metadata?: NotebookCellMetadata; - latestExecutionSummary?: vscode.NotebookCellExecutionSummary; + value: string; + languageId: string; + outputs?: vscode.NotebookCellOutput[]; + metadata?: Record; + executionSummary?: vscode.NotebookCellExecutionSummary; - constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, latestExecutionSummary?: vscode.NotebookCellExecutionSummary) { + constructor(kind: NotebookCellKind, value: string, languageId: string, outputs?: vscode.NotebookCellOutput[], metadata?: Record, executionSummary?: vscode.NotebookCellExecutionSummary) { this.kind = kind; - this.source = source; - this.language = language; + this.value = value; + this.languageId = languageId; this.outputs = outputs ?? []; this.metadata = metadata; - this.latestExecutionSummary = latestExecutionSummary; + this.executionSummary = executionSummary; + + NotebookCellData.validate(this); } } export class NotebookData { cells: NotebookCellData[]; - metadata: NotebookDocumentMetadata; + metadata?: { [key: string]: any }; - constructor(cells: NotebookCellData[], metadata?: NotebookDocumentMetadata) { + constructor(cells: NotebookCellData[]) { this.cells = cells; - this.metadata = metadata ?? new NotebookDocumentMetadata(); } } @@ -3094,32 +3039,105 @@ export class NotebookData { export class NotebookCellOutputItem { static isNotebookCellOutputItem(obj: unknown): obj is vscode.NotebookCellOutputItem { - return obj instanceof NotebookCellOutputItem; + if (obj instanceof NotebookCellOutputItem) { + return true; + } + if (!obj) { + return false; + } + return typeof (obj).mime === 'string' + && (obj).data instanceof Uint8Array; + } + + static error(err: Error | { name: string, message?: string, stack?: string }): NotebookCellOutputItem { + const obj = { + name: err.name, + message: err.message, + stack: err.stack + }; + return NotebookCellOutputItem.json(obj, 'application/vnd.code.notebook.error'); + } + + static stdout(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/vnd.code.notebook.stdout'); + } + + static stderr(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/vnd.code.notebook.stderr'); + } + + static bytes(value: Uint8Array, mime: string = 'application/octet-stream'): NotebookCellOutputItem { + return new NotebookCellOutputItem(value, mime); + } + + static #encoder = new TextEncoder(); + + static text(value: string, mime: string = 'text/plain'): NotebookCellOutputItem { + const bytes = NotebookCellOutputItem.#encoder.encode(String(value)); + return new NotebookCellOutputItem(bytes, mime); + } + + static json(value: any, mime: string = 'application/json'): NotebookCellOutputItem { + const rawStr = JSON.stringify(value, undefined, '\t'); + return NotebookCellOutputItem.text(rawStr, mime); } constructor( + public data: Uint8Array, public mime: string, - public value: unknown, // JSON'able - public metadata?: Record ) { - if (isFalsyOrWhitespace(this.mime)) { - throw new Error('INVALID mime type, must not be empty or falsy'); + const mimeNormalized = normalizeMimeType(mime, true); + if (!mimeNormalized) { + throw new Error('INVALID mime type, must not be empty or falsy: ' + mime); } + this.mime = mimeNormalized; } } export class NotebookCellOutput { + static isNotebookCellOutput(candidate: any): candidate is vscode.NotebookCellOutput { + if (candidate instanceof NotebookCellOutput) { + return true; + } + if (!candidate || typeof candidate !== 'object') { + return false; + } + return typeof (candidate).id === 'string' && isArray((candidate).items); + } + + static ensureUniqueMimeTypes(items: NotebookCellOutputItem[], warn: boolean = false): NotebookCellOutputItem[] { + const seen = new Set(); + const removeIdx = new Set(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const normalMime = normalizeMimeType(item.mime); + if (!seen.has(normalMime)) { + seen.add(normalMime); + continue; + } + // duplicated mime types... first has won + removeIdx.add(i); + if (warn) { + console.warn(`DUPLICATED mime type '${item.mime}' will be dropped`); + } + } + if (removeIdx.size === 0) { + return items; + } + return items.filter((_item, index) => !removeIdx.has(index)); + } + id: string; - outputs: NotebookCellOutputItem[]; + items: NotebookCellOutputItem[]; metadata?: Record; constructor( - outputs: NotebookCellOutputItem[], + items: NotebookCellOutputItem[], idOrMetadata?: string | Record, metadata?: Record ) { - this.outputs = outputs; + this.items = NotebookCellOutput.ensureUniqueMimeTypes(items, true); if (typeof idOrMetadata === 'string') { this.id = idOrMetadata; this.metadata = metadata; @@ -3131,7 +3149,7 @@ export class NotebookCellOutput { } export enum NotebookCellKind { - Markdown = 1, + Markup = 1, Code = 2 } @@ -3156,11 +3174,7 @@ export enum NotebookEditorRevealType { export class NotebookCellStatusBarItem { constructor( public text: string, - public alignment: NotebookCellStatusBarAlignment, - public command?: string | vscode.Command, - public tooltip?: string, - public priority?: number, - public accessibilityInformation?: vscode.AccessibilityInformation) { } + public alignment: NotebookCellStatusBarAlignment) { } } @@ -3169,6 +3183,18 @@ export enum NotebookControllerAffinity { Preferred = 2 } +export class NotebookRendererScript { + + public provides: string[]; + + constructor( + public uri: vscode.Uri, + provides: string | string[] = [] + ) { + this.provides = asArray(provides); + } +} + //#endregion //#region Timeline @@ -3228,6 +3254,25 @@ export class LinkedEditingRanges { } } +//#region ports +export class PortAttributes { + private _port: number; + private _autoForwardAction: PortAutoForwardAction; + constructor(port: number, autoForwardAction: PortAutoForwardAction) { + this._port = port; + this._autoForwardAction = autoForwardAction; + } + + get port(): number { + return this._port; + } + + get autoForwardAction(): PortAutoForwardAction { + return this._autoForwardAction; + } +} +//#endregion ports + //#region Testing export enum TestResultState { Unset = 0, @@ -3282,7 +3327,7 @@ const rangeComparator = (a: vscode.Range | undefined, b: vscode.Range | undefine export class TestItemImpl implements vscode.TestItem { public readonly id!: string; - public readonly uri!: vscode.Uri; + public readonly uri!: vscode.Uri | undefined; public readonly children!: ReadonlyMap; public readonly parent!: TestItemImpl | undefined; @@ -3296,7 +3341,7 @@ export class TestItemImpl implements vscode.TestItem { /** Extension-owned resolve handler */ public resolveHandler?: (token: vscode.CancellationToken) => void; - constructor(id: string, public label: string, uri: vscode.Uri, public data: unknown) { + constructor(id: string, public label: string, uri: vscode.Uri | undefined, public data: unknown) { const api = getPrivateApiFor(this); Object.defineProperties(this, { @@ -3322,7 +3367,7 @@ export class TestItemImpl implements vscode.TestItem { range: testItemPropAccessor(api, 'range', undefined, rangeComparator), description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator), runnable: testItemPropAccessor(api, 'runnable', true, strictEqualComparator), - debuggable: testItemPropAccessor(api, 'debuggable', true, strictEqualComparator), + debuggable: testItemPropAccessor(api, 'debuggable', false, strictEqualComparator), status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved, strictEqualComparator), error: testItemPropAccessor(api, 'error', undefined, strictEqualComparator), }); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostWebview.ts b/lib/vscode/src/vs/workbench/api/common/extHostWebview.ts index b4a280b116b4..9de76732f606 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostWebview.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostWebview.ts @@ -10,9 +10,9 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; +import { serializeWebviewMessage, deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; @@ -69,12 +69,11 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - return asWebviewUri(this.#initData, this.#handle, resource); + return asWebviewUri(resource, this.#initData.remote); } public get cspSource(): string { - return this.#initData.webviewCspSource - .replace('{{uuid}}', this.#handle); + return webviewGenericCspSource; } public get html(): string { @@ -110,7 +109,7 @@ export class ExtHostWebview implements vscode.Webview { if (this.#isDisposed) { return false; } - const serialized = serializeMessage(message, { serializeBuffersForPostMessage: this.#serializeBuffersForPostMessage }); + const serialized = serializeWebviewMessage(message, { serializeBuffersForPostMessage: this.#serializeBuffersForPostMessage }); return this.#proxy.$postMessage(this.#handle, serialized.message, ...serialized.buffers); } @@ -122,48 +121,14 @@ export class ExtHostWebview implements vscode.Webview { } export function shouldSerializeBuffersForPostMessage(extension: IExtensionDescription): boolean { - if (!extension.enableProposedApi) { - return false; - } - try { const version = normalizeVersion(parseVersion(extension.engines.vscode)); - return !!version && version.majorBase >= 1 && version.minorBase >= 56; + return !!version && version.majorBase >= 1 && version.minorBase >= 57; } catch { return false; } } -export function serializeMessage(message: any, options: { serializeBuffersForPostMessage?: boolean }): { message: string, buffers: VSBuffer[] } { - if (options.serializeBuffersForPostMessage) { - // Extract all ArrayBuffers from the message and replace them with references. - const vsBuffers: Array<{ original: ArrayBuffer, vsBuffer: VSBuffer }> = []; - - const replacer = (_key: string, value: any) => { - if (value && value instanceof ArrayBuffer) { - let index = vsBuffers.findIndex(x => x.original === value); - if (index === -1) { - const bytes = new Uint8Array(value); - const vsBuffer = VSBuffer.wrap(bytes); - index = vsBuffers.length; - vsBuffers.push({ original: value, vsBuffer }); - } - - return { - $$vscode_array_buffer_reference$$: true, - index, - }; - } - return value; - }; - - const serializedMessage = JSON.stringify(message, replacer); - return { message: serializedMessage, buffers: vsBuffers.map(x => x.vsBuffer) }; - } else { - return { message: JSON.stringify(message), buffers: [] }; - } -} - export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; diff --git a/lib/vscode/src/vs/workbench/api/common/extHostWebviewMessaging.ts b/lib/vscode/src/vs/workbench/api/common/extHostWebviewMessaging.ts index 06e76ab5b5e0..bba4171c6f1b 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostWebviewMessaging.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostWebviewMessaging.ts @@ -21,9 +21,9 @@ class ArrayBufferSet { export function serializeWebviewMessage( message: any, - transfer?: readonly ArrayBuffer[] + options: { serializeBuffersForPostMessage?: boolean } ): { message: string, buffers: VSBuffer[] } { - if (transfer) { + if (options.serializeBuffersForPostMessage) { // Extract all ArrayBuffers from the message and replace them with references. const arrayBuffers = new ArrayBufferSet(); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostWindow.ts b/lib/vscode/src/vs/workbench/api/common/extHostWindow.ts index a8c05964b01c..46e6191675f8 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostWindow.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostWindow.ts @@ -61,8 +61,6 @@ export class ExtHostWindow implements ExtHostWindowShape { async asExternalUri(uri: URI, options: IOpenUriOptions): Promise { if (isFalsyOrWhitespace(uri.scheme)) { return Promise.reject('Invalid scheme - cannot be empty'); - } else if (!new Set([Schemas.http, Schemas.https]).has(uri.scheme)) { - return Promise.reject(`Invalid scheme '${uri.scheme}'`); } const result = await this._proxy.$asExternalUri(uri, options); diff --git a/lib/vscode/src/vs/workbench/api/common/extHostWorkspace.ts b/lib/vscode/src/vs/workbench/api/common/extHostWorkspace.ts index 4e80bce81f91..b81eea794a6e 100644 --- a/lib/vscode/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/lib/vscode/src/vs/workbench/api/common/extHostWorkspace.ts @@ -563,8 +563,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } requestWorkspaceTrust(options?: vscode.WorkspaceTrustRequestOptions): Promise { - const promise = this._proxy.$requestWorkspaceTrust(options); - return options?.modal ? promise : Promise.resolve(this._trusted); + return this._proxy.$requestWorkspaceTrust(options); } $onDidGrantWorkspaceTrust(): void { diff --git a/lib/vscode/src/vs/workbench/api/common/menusExtensionPoint.ts b/lib/vscode/src/vs/workbench/api/common/menusExtensionPoint.ts index 3046d981ff38..513eb79d4855 100644 --- a/lib/vscode/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/lib/vscode/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -23,6 +23,7 @@ interface IAPIMenu { readonly description: string; readonly proposed?: boolean; // defaults to false readonly supportsSubmenus?: boolean; // defaults to true + readonly deprecationMessage?: string; } const apiMenus: IAPIMenu[] = [ @@ -136,7 +137,8 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.StatusBarWindowIndicatorMenu, description: localize('menus.statusBarWindowIndicator', "The window indicator menu in the status bar"), proposed: true, - supportsSubmenus: false + supportsSubmenus: false, + deprecationMessage: localize('menus.statusBarWindowIndicator.deprecated', "Use menu 'statusBar/remoteIndicator' instead."), }, { key: 'statusBar/remoteIndicator', @@ -182,6 +184,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('notebook.toolbar', "The contributed notebook toolbar menu"), proposed: true }, + { + key: 'notebook/toolbar/right', + id: MenuId.NotebookRightToolbar, + description: localize('notebook.toolbar.right', "The contributed notebook right toolbar menu"), + proposed: true + }, { key: 'notebook/cell/title', id: MenuId.NotebookCellTitle, @@ -223,7 +231,14 @@ const apiMenus: IAPIMenu[] = [ key: 'ports/item/port/inline', id: MenuId.TunnelPortInline, description: localize('view.tunnelPortInline', "The Ports view item port inline menu") - } + }, + { + key: 'editor/inlineCompletions/actions', + id: MenuId.InlineCompletionsActions, + description: localize('inlineCompletions.actions', "The actions shown when hovering on an inline completion"), + supportsSubmenus: false, + proposed: true + }, ]; namespace schema { @@ -411,6 +426,7 @@ namespace schema { type: 'object', properties: index(apiMenus, menu => menu.key, menu => ({ description: menu.proposed ? `(${localize('proposed', "Proposed API")}) ${menu.description}` : menu.description, + deprecationMessage: menu.deprecationMessage, type: 'array', items: menu.supportsSubmenus === false ? menuItem : { oneOf: [menuItem, submenuItem] } })), @@ -432,6 +448,7 @@ namespace schema { export interface IUserFriendlyCommand { command: string; title: string | ILocalizedString; + shortTitle?: string | ILocalizedString; enablement?: string; category?: string | ILocalizedString; icon?: IUserFriendlyIcon; @@ -451,6 +468,9 @@ namespace schema { if (!isValidLocalizedString(command.title, collector, 'title')) { return false; } + if (command.shortTitle && !isValidLocalizedString(command.shortTitle, collector, 'shortTitle')) { + return false; + } if (command.enablement && typeof command.enablement !== 'string') { collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'precondition')); return false; @@ -504,6 +524,10 @@ namespace schema { description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'), type: 'string' }, + shortTitle: { + description: localize('vscode.extension.contributes.commandType.shortTitle', 'Short title by which the command is represented in the UI'), + type: 'string' + }, category: { description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by the command is grouped in the UI'), type: 'string' @@ -561,7 +585,7 @@ commandsExtensionPoint.setHandler(extensions => { return; } - const { icon, enablement, category, title, command } = userFriendlyCommand; + const { icon, enablement, category, title, shortTitle, command } = userFriendlyCommand; let absoluteIcon: { dark: URI; light?: URI; } | ThemeIcon | undefined; if (icon) { @@ -582,6 +606,7 @@ commandsExtensionPoint.setHandler(extensions => { bucket.push({ id: command, title, + shortTitle: extension.description.enableProposedApi ? shortTitle : undefined, category, precondition: ContextKeyExpr.deserialize(enablement), icon: absoluteIcon diff --git a/lib/vscode/src/vs/workbench/api/common/shared/tasks.ts b/lib/vscode/src/vs/workbench/api/common/shared/tasks.ts index 4de859f671b3..066b22f90ea7 100644 --- a/lib/vscode/src/vs/workbench/api/common/shared/tasks.ts +++ b/lib/vscode/src/vs/workbench/api/common/shared/tasks.ts @@ -19,6 +19,7 @@ export interface TaskPresentationOptionsDTO { showReuseMessage?: boolean; clear?: boolean; group?: string; + close?: boolean; } export interface RunOptionsDTO { diff --git a/lib/vscode/src/vs/workbench/api/common/shared/webview.ts b/lib/vscode/src/vs/workbench/api/common/shared/webview.ts index 851a56985db2..9c78a6bf9f1e 100644 --- a/lib/vscode/src/vs/workbench/api/common/shared/webview.ts +++ b/lib/vscode/src/vs/workbench/api/common/shared/webview.ts @@ -3,28 +3,63 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import type * as vscode from 'vscode'; export interface WebviewInitData { - readonly isExtensionDevelopmentDebug: boolean; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; + readonly remote: { + readonly isRemote: boolean; + readonly authority: string | undefined + }; } +/** + * Root from which resources in webviews are loaded. + * + * This is hardcoded because we never expect to actually hit it. Instead these requests + * should always go to a service worker. + */ +export const webviewResourceBaseHost = 'vscode-webview.net'; + +export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; + +export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; + +/** + * Construct a uri that can load resources inside a webview + * + * We encode the resource component of the uri so that on the main thread + * we know where to load the resource from (remote or truly local): + * + * ```txt + * ${scheme}+${resource-authority}.vscode-resource.vscode-webview.net/${path} + * ``` + * + * @param resource Uri of the resource to load. + * @param remoteInfo Optional information about the remote that specifies where `resource` should be resolved from. + */ export function asWebviewUri( - initData: WebviewInitData, - uuid: string, resource: vscode.Uri, + remoteInfo?: { authority: string | undefined, isRemote: boolean } ): vscode.Uri { - const uri = initData.webviewResourceRoot - // Make sure we preserve the scheme of the resource but convert it into a normal path segment - // The scheme is important as we need to know if we are requesting a local or a remote resource. - .replace('{{resource}}', resource.scheme + withoutScheme(resource)) - .replace('{{uuid}}', uuid); - return URI.parse(uri); -} + if (resource.scheme === Schemas.http || resource.scheme === Schemas.https) { + return resource; + } + + if (remoteInfo && remoteInfo.authority && remoteInfo.isRemote && resource.scheme === Schemas.file) { + resource = URI.from({ + scheme: Schemas.vscodeRemote, + authority: remoteInfo.authority, + path: resource.path, + }); + } -function withoutScheme(resource: vscode.Uri): string { - return resource.toString().replace(/^\S+?:/, ''); + return URI.from({ + scheme: Schemas.https, + authority: `${resource.scheme}+${resource.authority}.${webviewRootResourceAuthority}`, + path: resource.path, + fragment: resource.fragment, + query: resource.query, + }); } diff --git a/lib/vscode/src/vs/workbench/api/node/extHostCLIServer.ts b/lib/vscode/src/vs/workbench/api/node/extHostCLIServer.ts index d5bc09abd495..ee509b68f189 100644 --- a/lib/vscode/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/lib/vscode/src/vs/workbench/api/node/extHostCLIServer.ts @@ -11,8 +11,8 @@ import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/ import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { ILogService } from 'vs/platform/log/common/log'; -import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; export interface OpenCommandPipeArgs { type: 'open'; diff --git a/lib/vscode/src/vs/workbench/api/node/extHostDebugService.ts b/lib/vscode/src/vs/workbench/api/node/extHostDebugService.ts index 9ba96a894b50..193490ea7b0d 100644 --- a/lib/vscode/src/vs/workbench/api/node/extHostDebugService.ts +++ b/lib/vscode/src/vs/workbench/api/node/extHostDebugService.ts @@ -20,10 +20,11 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostDebugServiceBase, ExtHostDebugSession, ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { hasChildProcesses, prepareCommand, runInExternalTerminal } from 'vs/workbench/contrib/debug/node/terminals'; import { IDisposable } from 'vs/base/common/lifecycle'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { hasChildProcesses, prepareCommand, runInExternalTerminal } from 'vs/workbench/contrib/debug/node/terminals'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -38,9 +39,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostExtensionService extensionService: IExtHostExtensionService, @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, - @IExtHostTerminalService private _terminalService: IExtHostTerminalService + @IExtHostTerminalService private _terminalService: IExtHostTerminalService, + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService); + super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -79,8 +81,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } const configProvider = await this._configurationService.getConfigProvider(); - const shell = this._terminalService.getDefaultShell(true, configProvider); - const shellArgs = this._terminalService.getDefaultShellArgs(true, configProvider); + const shell = this._terminalService.getDefaultShell(true); + const shellArgs = this._terminalService.getDefaultShellArgs(true); const shellConfig = JSON.stringify({ shell, shellArgs }); let terminal = await this._integratedTerminalInstances.checkout(shellConfig); @@ -96,7 +98,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { name: args.title || nls.localize('debug.terminal.title', "debuggee"), }; giveShellTimeToInitialize = true; - terminal = this._terminalService.createTerminalFromOptions(options, true); + terminal = this._terminalService.createTerminalFromOptions(options, { + isFeatureTerminal: true, + useShellEnvironment: true + }); this._integratedTerminalInstances.insert(terminal, shellConfig); } else { @@ -139,14 +144,13 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { return shellProcessId; } else if (args.kind === 'external') { - return runInExternalTerminal(args, await this._configurationService.getConfigProvider()); } return super.$runInTerminal(args, sessionId); } protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._workspaceService); + return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs, this._workspaceService); } } diff --git a/lib/vscode/src/vs/workbench/api/node/extHostOutputService.ts b/lib/vscode/src/vs/workbench/api/node/extHostOutputService.ts index 518c90b4b732..9935633b766e 100644 --- a/lib/vscode/src/vs/workbench/api/node/extHostOutputService.ts +++ b/lib/vscode/src/vs/workbench/api/node/extHostOutputService.ts @@ -8,26 +8,27 @@ import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { toLocalISOString } from 'vs/base/common/date'; -import { SymlinkSupport } from 'vs/base/node/pfs'; -import { promises } from 'fs'; +import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { createRotatingLogger } from 'vs/platform/log/node/spdlogLog'; -import { RotatingLogger } from 'spdlog'; +import { Logger } from 'spdlog'; import { ByteSize } from 'vs/platform/files/common/files'; class OutputAppender { - private appender: RotatingLogger; + static async create(name: string, file: string): Promise { + const appender = await createRotatingLogger(name, file, 30 * ByteSize.MB, 1); + appender.clearFormatters(); - constructor(name: string, readonly file: string) { - this.appender = createRotatingLogger(name, file, 30 * ByteSize.MB, 1); - this.appender.clearFormatters(); + return new OutputAppender(name, file, appender); } + private constructor(readonly name: string, readonly file: string, private readonly appender: Logger) { } + append(content: string): void { this.appender.critical(content); } @@ -38,7 +39,7 @@ class OutputAppender { } -export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { +class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { private _appender: OutputAppender; @@ -109,11 +110,11 @@ export class ExtHostOutputService2 extends ExtHostOutputService { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); const exists = await SymlinkSupport.existsDirectory(outputDirPath); if (!exists) { - await promises.mkdir(outputDirPath, { recursive: true }); + await Promises.mkdir(outputDirPath, { recursive: true }); } const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`; const file = URI.file(join(outputDirPath, `${fileName}.log`)); - const appender = new OutputAppender(fileName, file.fsPath); + const appender = await OutputAppender.create(fileName, file.fsPath); return new ExtHostOutputChannelBackedByFile(name, appender, this._proxy); } catch (error) { // Do not crash if logger cannot be created diff --git a/lib/vscode/src/vs/workbench/api/node/extHostTask.ts b/lib/vscode/src/vs/workbench/api/node/extHostTask.ts index dd250c83c291..98a5b1970225 100644 --- a/lib/vscode/src/vs/workbench/api/node/extHostTask.ts +++ b/lib/vscode/src/vs/workbench/api/node/extHostTask.ts @@ -23,6 +23,7 @@ import { ExtHostTaskBase, TaskHandleDTO, TaskDTO, CustomExecutionDTO, HandlerDat import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export class ExtHostTask extends ExtHostTaskBase { private _variableResolver: ExtHostVariableResolverService | undefined; @@ -35,7 +36,8 @@ export class ExtHostTask extends ExtHostTaskBase { @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @ILogService logService: ILogService, - @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService + @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService, + @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs ) { super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService); if (initData.remote.isRemote && initData.remote.authority) { @@ -127,7 +129,7 @@ export class ExtHostTask extends ExtHostTaskBase { private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise { if (this._variableResolver === undefined) { const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.workspaceService); + this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.editorTabs, this.workspaceService); } return this._variableResolver; } @@ -172,10 +174,6 @@ export class ExtHostTask extends ExtHostTaskBase { return result; } - public $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> { - return this._terminalService.$getDefaultShellAndArgs(true); - } - public async $jsonTasksSupported(): Promise { return true; } diff --git a/lib/vscode/src/vs/workbench/api/node/extHostTerminalService.ts b/lib/vscode/src/vs/workbench/api/node/extHostTerminalService.ts index 872181891471..652223e1a45e 100644 --- a/lib/vscode/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/lib/vscode/src/vs/workbench/api/node/extHostTerminalService.ts @@ -3,143 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as platform from 'vs/base/common/platform'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; -import { getSystemShell, getSystemShellSync } from 'vs/base/node/shell'; -import { ILogService } from 'vs/platform/log/common/log'; -import { SafeConfigProvider } from 'vs/platform/terminal/common/terminal'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IShellAndArgsDto } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfigProvider, ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; -import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; -import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService'; -import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; -import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { detectAvailableProfiles } from 'vs/workbench/contrib/terminal/node/terminalProfiles'; +import { BaseExtHostTerminalService, ExtHostTerminal, ITerminalInternalOptions } from 'vs/workbench/api/common/extHostTerminalService'; import type * as vscode from 'vscode'; export class ExtHostTerminalService extends BaseExtHostTerminalService { - private _variableResolver: ExtHostVariableResolverService | undefined; - private _variableResolverPromise: Promise; - private _lastActiveWorkspace: IWorkspaceFolder | undefined; - - private _defaultShell: string | undefined; - constructor( - @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostConfiguration private _extHostConfiguration: ExtHostConfiguration, - @IExtHostWorkspace private _extHostWorkspace: ExtHostWorkspace, - @IExtHostDocumentsAndEditors private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - @ILogService private _logService: ILogService + @IExtHostRpcService extHostRpc: IExtHostRpcService ) { super(true, extHostRpc); - - // Getting the SystemShell is an async operation, however, the ExtHost terminal service is mostly synchronous - // and the API `vscode.env.shell` is also synchronous. The default shell _should_ be set when extensions are - // starting up but if not, we run getSystemShellSync below which gets a sane default. - getSystemShell(platform.OS, process.env as platform.IProcessEnvironment).then(s => this._defaultShell = s); - - this._updateLastActiveWorkspace(); - this._variableResolverPromise = this._updateVariableResolver(); - this._registerListeners(); } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name); - this._terminals.push(terminal); - terminal.create(shellPath, shellArgs); - return terminal.value; + return this.createTerminalFromOptions({ name, shellPath, shellArgs }); } - public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal { + public createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); this._terminals.push(terminal); - terminal.create( - withNullAsUndefined(options.shellPath), - withNullAsUndefined(options.shellArgs), - withNullAsUndefined(options.cwd), - withNullAsUndefined(options.env), - withNullAsUndefined(options.icon), - withNullAsUndefined(options.message), - /*options.waitOnExit*/ undefined, - withNullAsUndefined(options.strictEnv), - withNullAsUndefined(options.hideFromUser), - withNullAsUndefined(isFeatureTerminal), - true - ); + terminal.create(options, internalOptions); return terminal.value; } - - public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { - return terminalEnvironment.getDefaultShell( - this._buildSafeConfigProvider(configProvider), - this._defaultShell ?? getSystemShellSync(platform.OS, process.env as platform.IProcessEnvironment), - process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), - process.env.windir, - terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, process.env, this._variableResolver), - this._logService, - useAutomationShell - ); - } - - public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { - return terminalEnvironment.getDefaultShellArgs( - this._buildSafeConfigProvider(configProvider), - useAutomationShell, - terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, process.env, this._variableResolver), - this._logService - ); - } - - private _registerListeners(): void { - this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(() => this._updateLastActiveWorkspace()); - this._extHostWorkspace.onDidChangeWorkspace(() => { - this._variableResolverPromise = this._updateVariableResolver(); - }); - } - - private _updateLastActiveWorkspace(): void { - const activeEditor = this._extHostDocumentsAndEditors.activeEditor(); - if (activeEditor) { - this._lastActiveWorkspace = this._extHostWorkspace.getWorkspaceFolder(activeEditor.document.uri) as IWorkspaceFolder; - } - } - - private async _updateVariableResolver(): Promise { - const configProvider = await this._extHostConfiguration.getConfigProvider(); - const workspaceFolders = await this._extHostWorkspace.getWorkspaceFolders2(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders || [], this._extHostDocumentsAndEditors, configProvider); - return this._variableResolver; - } - - public async $getAvailableProfiles(configuredProfilesOnly: boolean): Promise { - const safeConfigProvider = this._buildSafeConfigProvider(await this._extHostConfiguration.getConfigProvider()); - return detectAvailableProfiles(configuredProfilesOnly, safeConfigProvider, undefined, this._logService, await this._variableResolverPromise, this._lastActiveWorkspace); - } - - public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { - const configProvider = await this._extHostConfiguration.getConfigProvider(); - return { - shell: this.getDefaultShell(useAutomationShell, configProvider), - args: this.getDefaultShellArgs(useAutomationShell, configProvider) - }; - } - - // TODO: Remove when workspace trust is enabled - private _buildSafeConfigProvider(configProvider: ExtHostConfigProvider): SafeConfigProvider { - const config = configProvider.getConfiguration(); - return (key: string) => { - const isWorkspaceConfigAllowed = config.get('terminal.integrated.allowWorkspaceConfiguration'); - if (isWorkspaceConfigAllowed) { - return config.get(key) as any; - } - const inspected = config.inspect(key); - return inspected?.globalValue || inspected?.defaultValue; - }; - } } diff --git a/lib/vscode/src/vs/workbench/api/node/extHostTunnelService.ts b/lib/vscode/src/vs/workbench/api/node/extHostTunnelService.ts index a33f7851f347..caefe078c1ab 100644 --- a/lib/vscode/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/lib/vscode/src/vs/workbench/api/node/extHostTunnelService.ts @@ -11,7 +11,6 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { URI } from 'vs/base/common/uri'; import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isLinux } from 'vs/base/common/platform'; @@ -365,8 +364,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe let tcp: string = ''; let tcp6: string = ''; try { - tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); - tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); + tcp = await pfs.Promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await pfs.Promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -387,10 +386,10 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = await fs.promises.stat(childUri.fsPath); + const childStat = await pfs.Promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await pfs.Promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await pfs.Promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { diff --git a/lib/vscode/src/vs/workbench/browser/menuActions.ts b/lib/vscode/src/vs/workbench/browser/actions.ts similarity index 99% rename from lib/vscode/src/vs/workbench/browser/menuActions.ts rename to lib/vscode/src/vs/workbench/browser/actions.ts index 7aecec66effe..53eaf6741966 100644 --- a/lib/vscode/src/vs/workbench/browser/menuActions.ts +++ b/lib/vscode/src/vs/workbench/browser/actions.ts @@ -32,7 +32,9 @@ class MenuActions extends Disposable { private readonly contextKeyService: IContextKeyService ) { super(); + this.menu = this._register(menuService.createMenu(menuId, contextKeyService)); + this._register(this.menu.onDidChange(() => this.updateActions())); this.updateActions(); } @@ -48,6 +50,7 @@ class MenuActions extends Disposable { private updateSubmenus(actions: readonly IAction[], submenus: { [id: number]: IMenu }): IDisposable { const disposables = new DisposableStore(); + for (const action of actions) { if (action instanceof SubmenuItemAction && !submenus[action.item.submenu.id]) { const menu = submenus[action.item.submenu.id] = disposables.add(this.menuService.createMenu(action.item.submenu, this.contextKeyService)); @@ -55,6 +58,7 @@ class MenuActions extends Disposable { disposables.add(this.updateSubmenus(action.actions, submenus)); } } + return disposables; } } @@ -75,7 +79,9 @@ export class CompositeMenuActions extends Disposable { @IMenuService private readonly menuService: IMenuService, ) { super(); + this.menuActions = this._register(new MenuActions(menuId, this.options, menuService, contextKeyService)); + this._register(this.menuActions.onDidChange(() => this._onDidChange.fire())); } @@ -89,11 +95,13 @@ export class CompositeMenuActions extends Disposable { getContextMenuActions(): IAction[] { const actions: IAction[] = []; + if (this.contextMenuId) { const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService); this.contextMenuActionsDisposable.value = createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions }); menu.dispose(); } + return actions; } } diff --git a/lib/vscode/src/vs/workbench/browser/actions/developerActions.ts b/lib/vscode/src/vs/workbench/browser/actions/developerActions.ts index 82c377cd1f83..b12c147f01ff 100644 --- a/lib/vscode/src/vs/workbench/browser/actions/developerActions.ts +++ b/lib/vscode/src/vs/workbench/browser/actions/developerActions.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/actions'; import { localize } from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { domEvent } from 'vs/base/browser/event'; +import { DomEmitter } from 'vs/base/browser/event'; import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IDisposable, toDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -63,8 +63,8 @@ class InspectContextKeysAction extends Action2 { hoverFeedback.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; hoverFeedback.style.zIndex = '1000'; - const onMouseMove = domEvent(document.body, 'mousemove', true); - disposables.add(onMouseMove(e => { + const onMouseMove = disposables.add(new DomEmitter(document.body, 'mousemove', true)); + disposables.add(onMouseMove.event(e => { const target = e.target as HTMLElement; const position = getDomNodePagePosition(target); @@ -74,11 +74,11 @@ class InspectContextKeysAction extends Action2 { hoverFeedback.style.height = `${position.height}px`; })); - const onMouseDown = Event.once(domEvent(document.body, 'mousedown', true)); - onMouseDown(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables); + const onMouseDown = disposables.add(new DomEmitter(document.body, 'mousedown', true)); + Event.once(onMouseDown.event)(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables); - const onMouseUp = Event.once(domEvent(document.body, 'mouseup', true)); - onMouseUp(e => { + const onMouseUp = disposables.add(new DomEmitter(document.body, 'mouseup', true)); + Event.once(onMouseUp.event)(e => { e.preventDefault(); e.stopPropagation(); @@ -120,9 +120,9 @@ class ToggleScreencastModeAction extends Action2 { const mouseMarker = append(container, $('.screencast-mouse')); disposables.add(toDisposable(() => mouseMarker.remove())); - const onMouseDown = domEvent(container, 'mousedown', true); - const onMouseUp = domEvent(container, 'mouseup', true); - const onMouseMove = domEvent(container, 'mousemove', true); + const onMouseDown = disposables.add(new DomEmitter(container, 'mousedown', true)); + const onMouseUp = disposables.add(new DomEmitter(container, 'mouseup', true)); + const onMouseMove = disposables.add(new DomEmitter(container, 'mousemove', true)); const updateMouseIndicatorColor = () => { mouseMarker.style.borderColor = Color.fromHex(configurationService.getValue('screencastMode.mouseIndicatorColor')).toString(); @@ -139,17 +139,17 @@ class ToggleScreencastModeAction extends Action2 { updateMouseIndicatorColor(); updateMouseIndicatorSize(); - disposables.add(onMouseDown(e => { + disposables.add(onMouseDown.event(e => { mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; mouseMarker.style.display = 'block'; - const mouseMoveListener = onMouseMove(e => { + const mouseMoveListener = onMouseMove.event(e => { mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; }); - Event.once(onMouseUp)(() => { + Event.once(onMouseUp.event)(() => { mouseMarker.style.display = 'none'; mouseMoveListener.dispose(); }); @@ -197,11 +197,11 @@ class ToggleScreencastModeAction extends Action2 { } })); - const onKeyDown = domEvent(window, 'keydown', true); + const onKeyDown = disposables.add(new DomEmitter(window, 'keydown', true)); let keyboardTimeout: IDisposable = Disposable.None; let length = 0; - disposables.add(onKeyDown(e => { + disposables.add(onKeyDown.event(e => { keyboardTimeout.dispose(); const event = new StandardKeyboardEvent(e); diff --git a/lib/vscode/src/vs/workbench/browser/actions/helpActions.ts b/lib/vscode/src/vs/workbench/browser/actions/helpActions.ts index fccf60726145..107ccbc251d2 100644 --- a/lib/vscode/src/vs/workbench/browser/actions/helpActions.ts +++ b/lib/vscode/src/vs/workbench/browser/actions/helpActions.ts @@ -9,7 +9,7 @@ import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; -import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IProductService } from 'vs/platform/product/common/productService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -24,13 +24,22 @@ class KeybindingsReferenceAction extends Action2 { constructor() { super({ id: KeybindingsReferenceAction.ID, - title: { value: localize('keybindingsReference', "Keyboard Shortcuts Reference"), original: 'Keyboard Shortcuts Reference' }, + title: { + value: localize('keybindingsReference', "Keyboard Shortcuts Reference"), + mnemonicTitle: localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference"), + original: 'Keyboard Shortcuts Reference' + }, category: CATEGORIES.Help, f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: null, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_R) + }, + menu: { + id: MenuId.MenubarHelpMenu, + group: '2_reference', + order: 1 } }); } @@ -46,17 +55,26 @@ class KeybindingsReferenceAction extends Action2 { } } -class OpenDocumentationUrlAction extends Action2 { +class OpenIntroductoryVideosUrlAction extends Action2 { - static readonly ID = 'workbench.action.openDocumentationUrl'; - static readonly AVAILABLE = !!product.documentationUrl; + static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; + static readonly AVAILABLE = !!product.introductoryVideosUrl; constructor() { super({ - id: OpenDocumentationUrlAction.ID, - title: { value: localize('openDocumentationUrl', "Documentation"), original: 'Documentation' }, + id: OpenIntroductoryVideosUrlAction.ID, + title: { + value: localize('openIntroductoryVideosUrl', "Introductory Videos"), + mnemonicTitle: localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos"), + original: 'Introductory Videos' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '2_reference', + order: 2 + } }); } @@ -64,23 +82,32 @@ class OpenDocumentationUrlAction extends Action2 { const productService = accessor.get(IProductService); const openerService = accessor.get(IOpenerService); - if (productService.documentationUrl) { - openerService.open(URI.parse(productService.documentationUrl)); + if (productService.introductoryVideosUrl) { + openerService.open(URI.parse(productService.introductoryVideosUrl)); } } } -class OpenIntroductoryVideosUrlAction extends Action2 { +class OpenTipsAndTricksUrlAction extends Action2 { - static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; - static readonly AVAILABLE = !!product.introductoryVideosUrl; + static readonly ID = 'workbench.action.openTipsAndTricksUrl'; + static readonly AVAILABLE = !!product.tipsAndTricksUrl; constructor() { super({ - id: OpenIntroductoryVideosUrlAction.ID, - title: { value: localize('openIntroductoryVideosUrl', "Introductory Videos"), original: 'Introductory Videos' }, + id: OpenTipsAndTricksUrlAction.ID, + title: { + value: localize('openTipsAndTricksUrl', "Tips and Tricks"), + mnemonicTitle: localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "Tips and Tri&&cks"), + original: 'Tips and Tricks' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '2_reference', + order: 3 + } }); } @@ -88,23 +115,32 @@ class OpenIntroductoryVideosUrlAction extends Action2 { const productService = accessor.get(IProductService); const openerService = accessor.get(IOpenerService); - if (productService.introductoryVideosUrl) { - openerService.open(URI.parse(productService.introductoryVideosUrl)); + if (productService.tipsAndTricksUrl) { + openerService.open(URI.parse(productService.tipsAndTricksUrl)); } } } -class OpenTipsAndTricksUrlAction extends Action2 { +class OpenDocumentationUrlAction extends Action2 { - static readonly ID = 'workbench.action.openTipsAndTricksUrl'; - static readonly AVAILABLE = !!product.tipsAndTricksUrl; + static readonly ID = 'workbench.action.openDocumentationUrl'; + static readonly AVAILABLE = !!product.documentationUrl; constructor() { super({ - id: OpenTipsAndTricksUrlAction.ID, - title: { value: localize('openTipsAndTricksUrl', "Tips and Tricks"), original: 'Tips and Tricks' }, + id: OpenDocumentationUrlAction.ID, + title: { + value: localize('openDocumentationUrl', "Documentation"), + mnemonicTitle: localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation"), + original: 'Documentation' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '1_welcome', + order: 3 + } }); } @@ -112,8 +148,8 @@ class OpenTipsAndTricksUrlAction extends Action2 { const productService = accessor.get(IProductService); const openerService = accessor.get(IOpenerService); - if (productService.tipsAndTricksUrl) { - openerService.open(URI.parse(productService.tipsAndTricksUrl)); + if (productService.documentationUrl) { + openerService.open(URI.parse(productService.documentationUrl)); } } } @@ -151,9 +187,18 @@ class OpenTwitterUrlAction extends Action2 { constructor() { super({ id: OpenTwitterUrlAction.ID, - title: { value: localize('openTwitterUrl', "Join Us on Twitter"), original: 'Join Us on Twitter' }, + title: { + value: localize('openTwitterUrl', "Join Us on Twitter"), + mnemonicTitle: localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join Us on Twitter"), + original: 'Join Us on Twitter' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '3_feedback', + order: 1 + } }); } @@ -175,9 +220,18 @@ class OpenRequestFeatureUrlAction extends Action2 { constructor() { super({ id: OpenRequestFeatureUrlAction.ID, - title: { value: localize('openUserVoiceUrl', "Search Feature Requests"), original: 'Search Feature Requests' }, + title: { + value: localize('openUserVoiceUrl', "Search Feature Requests"), + mnemonicTitle: localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests"), + original: 'Search Feature Requests' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '3_feedback', + order: 2 + } }); } @@ -199,9 +253,18 @@ class OpenLicenseUrlAction extends Action2 { constructor() { super({ id: OpenLicenseUrlAction.ID, - title: { value: localize('openLicenseUrl', "View License"), original: 'View License' }, + title: { + value: localize('openLicenseUrl', "View License"), + mnemonicTitle: localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License"), + original: 'View License' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '4_legal', + order: 1 + } }); } @@ -228,9 +291,18 @@ class OpenPrivacyStatementUrlAction extends Action2 { constructor() { super({ id: OpenPrivacyStatementUrlAction.ID, - title: { value: localize('openPrivacyStatement', "Privacy Statement"), original: 'Privacy Statement' }, + title: { + value: localize('openPrivacyStatement', "Privacy Statement"), + mnemonicTitle: localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "Privac&&y Statement"), + original: 'Privacy Statement' + }, category: CATEGORIES.Help, - f1: true + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '4_legal', + order: 2 + } }); } @@ -255,10 +327,6 @@ if (KeybindingsReferenceAction.AVAILABLE) { registerAction2(KeybindingsReferenceAction); } -if (OpenDocumentationUrlAction.AVAILABLE) { - registerAction2(OpenDocumentationUrlAction); -} - if (OpenIntroductoryVideosUrlAction.AVAILABLE) { registerAction2(OpenIntroductoryVideosUrlAction); } @@ -267,6 +335,10 @@ if (OpenTipsAndTricksUrlAction.AVAILABLE) { registerAction2(OpenTipsAndTricksUrlAction); } +if (OpenDocumentationUrlAction.AVAILABLE) { + registerAction2(OpenDocumentationUrlAction); +} + if (OpenNewsletterSignupUrlAction.AVAILABLE) { registerAction2(OpenNewsletterSignupUrlAction); } @@ -286,98 +358,3 @@ if (OpenLicenseUrlAction.AVAILABLE) { if (OpenPrivacyStatementUrlAction.AVAILABE) { registerAction2(OpenPrivacyStatementUrlAction); } - -// --- Menu Registration - -// Help - -if (OpenDocumentationUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '1_welcome', - command: { - id: OpenDocumentationUrlAction.ID, - title: localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation") - }, - order: 3 - }); -} - -// Reference -if (KeybindingsReferenceAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '2_reference', - command: { - id: KeybindingsReferenceAction.ID, - title: localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference") - }, - order: 1 - }); -} - -if (OpenIntroductoryVideosUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '2_reference', - command: { - id: OpenIntroductoryVideosUrlAction.ID, - title: localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos") - }, - order: 2 - }); -} - -if (OpenTipsAndTricksUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '2_reference', - command: { - id: OpenTipsAndTricksUrlAction.ID, - title: localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "Tips and Tri&&cks") - }, - order: 3 - }); -} - -// Feedback -if (OpenTwitterUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '3_feedback', - command: { - id: OpenTwitterUrlAction.ID, - title: localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join Us on Twitter") - }, - order: 1 - }); -} - -if (OpenRequestFeatureUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '3_feedback', - command: { - id: OpenRequestFeatureUrlAction.ID, - title: localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests") - }, - order: 2 - }); -} - -// Legal -if (OpenLicenseUrlAction.AVAILABLE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '4_legal', - command: { - id: OpenLicenseUrlAction.ID, - title: localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License") - }, - order: 1 - }); -} - -if (OpenPrivacyStatementUrlAction.AVAILABE) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '4_legal', - command: { - id: OpenPrivacyStatementUrlAction.ID, - title: localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "Privac&&y Statement") - }, - order: 2 - }); -} diff --git a/lib/vscode/src/vs/workbench/browser/actions/layoutActions.ts b/lib/vscode/src/vs/workbench/browser/actions/layoutActions.ts index 1cd44e6d2362..3d94cd32bc65 100644 --- a/lib/vscode/src/vs/workbench/browser/actions/layoutActions.ts +++ b/lib/vscode/src/vs/workbench/browser/actions/layoutActions.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import Severity from 'vs/base/common/severity'; +import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,15 +19,13 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); - // --- Close Side Bar -class CloseSidebarAction extends Action2 { +registerAction2(class extends Action2 { constructor() { super({ @@ -42,25 +39,32 @@ class CloseSidebarAction extends Action2 { run(accessor: ServicesAccessor): void { accessor.get(IWorkbenchLayoutService).setSideBarHidden(true); } -} - -registerAction2(CloseSidebarAction); +}); // --- Toggle Activity Bar export class ToggleActivityBarVisibilityAction extends Action2 { static readonly ID = 'workbench.action.toggleActivityBarVisibility'; - static readonly LABEL = localize('toggleActivityBar', "Toggle Activity Bar Visibility"); private static readonly activityBarVisibleKey = 'workbench.activityBar.visible'; constructor() { super({ id: ToggleActivityBarVisibilityAction.ID, - title: { value: ToggleActivityBarVisibilityAction.LABEL, original: 'Toggle Activity Bar Visibility' }, + title: { + value: localize('toggleActivityBar', "Toggle Activity Bar Visibility"), + mnemonicTitle: localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"), + original: 'Toggle Activity Bar Visibility' + }, category: CATEGORIES.View, - f1: true + f1: true, + toggled: ContextKeyExpr.equals('config.workbench.activityBar.visible', true), + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 4 + } }); } @@ -77,28 +81,26 @@ export class ToggleActivityBarVisibilityAction extends Action2 { registerAction2(ToggleActivityBarVisibilityAction); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleActivityBarVisibilityAction.ID, - title: localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"), - toggled: ContextKeyExpr.equals('config.workbench.activityBar.visible', true) - }, - order: 4 -}); - // --- Toggle Centered Layout -class ToggleCenteredLayout extends Action2 { - - static readonly ID = 'workbench.action.toggleCenteredLayout'; +registerAction2(class extends Action2 { constructor() { super({ - id: ToggleCenteredLayout.ID, - title: { value: localize('toggleCenteredLayout', "Toggle Centered Layout"), original: 'Toggle Centered Layout' }, + id: 'workbench.action.toggleCenteredLayout', + title: { + value: localize('toggleCenteredLayout', "Toggle Centered Layout"), + mnemonicTitle: localize({ key: 'miToggleCenteredLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered Layout"), + original: 'Toggle Centered Layout' + }, category: CATEGORIES.View, - f1: true + f1: true, + toggled: IsCenteredLayoutContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '1_toggle_view', + order: 3 + } }); } @@ -107,51 +109,21 @@ class ToggleCenteredLayout extends Action2 { layoutService.centerEditorLayout(!layoutService.isEditorLayoutCentered()); } -} - -registerAction2(ToggleCenteredLayout); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleCenteredLayout.ID, - title: localize({ key: 'miToggleCenteredLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered Layout"), - toggled: IsCenteredLayoutContext - }, - order: 3 }); // --- Toggle Sidebar Position -export class ToggleSidebarPositionAction extends Action { +export class ToggleSidebarPositionAction extends Action2 { static readonly ID = 'workbench.action.toggleSidebarPosition'; static readonly LABEL = localize('toggleSidebarPosition', "Toggle Side Bar Position"); private static readonly sidebarPositionConfigurationKey = 'workbench.sideBar.location'; - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - } - - override run(): Promise { - const position = this.layoutService.getSideBarPosition(); - const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; - - return this.configurationService.updateValue(ToggleSidebarPositionAction.sidebarPositionConfigurationKey, newPositionValue); - } - static getLabel(layoutService: IWorkbenchLayoutService): string { return layoutService.getSideBarPosition() === Position.LEFT ? localize('moveSidebarRight', "Move Side Bar Right") : localize('moveSidebarLeft', "Move Side Bar Left"); } -} -registerAction2(class extends Action2 { constructor() { super({ id: ToggleSidebarPositionAction.ID, @@ -160,10 +132,20 @@ registerAction2(class extends Action2 { f1: true }); } - run(accessor: ServicesAccessor) { - accessor.get(IInstantiationService).createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL).run(); + + run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IWorkbenchLayoutService); + const configurationService = accessor.get(IConfigurationService); + + const position = layoutService.getSideBarPosition(); + const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; + + return configurationService.updateValue(ToggleSidebarPositionAction.sidebarPositionConfigurationKey, newPositionValue); } -}); +} + +registerAction2(ToggleSidebarPositionAction); + MenuRegistry.appendMenuItems([{ id: MenuId.ViewContainerTitleContext, item: { @@ -230,35 +212,32 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 2 }); -// --- Toggle Sidebar Visibility +// --- Toggle Editor Visibility -export class ToggleEditorVisibilityAction extends Action { - static readonly ID = 'workbench.action.toggleEditorVisibility'; - static readonly LABEL = localize('toggleEditor', "Toggle Editor Area Visibility"); +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleEditorVisibility', + title: { + value: localize('toggleEditor', "Toggle Editor Area Visibility"), + mnemonicTitle: localize({ key: 'miShowEditorArea', comment: ['&& denotes a mnemonic'] }, "Show &&Editor Area"), + original: 'Toggle Editor Area Visibility' + }, + category: CATEGORIES.View, + f1: true, + toggled: EditorAreaVisibleContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 5 + } + }); } - override async run(): Promise { - this.layoutService.toggleMaximizedPanel(); + run(accessor: ServicesAccessor): void { + accessor.get(IWorkbenchLayoutService).toggleMaximizedPanel(); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorVisibilityAction), 'View: Toggle Editor Area Visibility', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleEditorVisibilityAction.ID, - title: localize({ key: 'miShowEditorArea', comment: ['&& denotes a mnemonic'] }, "Show &&Editor Area"), - toggled: EditorAreaVisibleContext - }, - order: 5 }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { @@ -268,11 +247,15 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 1 }); -export const TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID = 'workbench.action.toggleSidebarVisibility'; -registerAction2(class extends Action2 { +// Toggle Sidebar Visibility + +class ToggleSidebarVisibilityAction extends Action2 { + + static readonly ID = 'workbench.action.toggleSidebarVisibility'; + constructor() { super({ - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + id: ToggleSidebarVisibilityAction.ID, title: { value: localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' }, category: CATEGORIES.View, f1: true, @@ -282,17 +265,22 @@ registerAction2(class extends Action2 { } }); } - run(accessor: ServicesAccessor) { + + run(accessor: ServicesAccessor): void { const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.setSideBarHidden(layoutService.isVisible(Parts.SIDEBAR_PART)); } -}); +} + +registerAction2(ToggleSidebarVisibilityAction); + MenuRegistry.appendMenuItems([{ id: MenuId.ViewContainerTitleContext, item: { group: '3_workbench_layout_move', command: { - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + id: ToggleSidebarVisibilityAction.ID, title: localize('compositePart.hideSideBarLabel', "Hide Side Bar"), }, when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), @@ -303,123 +291,127 @@ MenuRegistry.appendMenuItems([{ item: { group: '3_workbench_layout_move', command: { - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + id: ToggleSidebarVisibilityAction.ID, title: localize('compositePart.hideSideBarLabel', "Hide Side Bar"), }, when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 2 } +}, { + id: MenuId.MenubarAppearanceMenu, + item: { + group: '2_workbench_layout', + command: { + id: ToggleSidebarVisibilityAction.ID, + title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), + toggled: SideBarVisibleContext + }, + order: 1 + } }]); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, - title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), - toggled: SideBarVisibleContext - }, - order: 1 -}); - // --- Toggle Statusbar Visibility -export class ToggleStatusbarVisibilityAction extends Action { +export class ToggleStatusbarVisibilityAction extends Action2 { static readonly ID = 'workbench.action.toggleStatusbarVisibility'; - static readonly LABEL = localize('toggleStatusbar', "Toggle Status Bar Visibility"); private static readonly statusbarVisibleKey = 'workbench.statusBar.visible'; - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleStatusbarVisibilityAction.ID, + title: { + value: localize('toggleStatusbar', "Toggle Status Bar Visibility"), + mnemonicTitle: localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "Show S&&tatus Bar"), + original: 'Toggle Status Bar Visibility' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.equals('config.workbench.statusBar.visible', true), + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 3 + } + }); } - override run(): Promise { - const visibility = this.layoutService.isVisible(Parts.STATUSBAR_PART); + run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IWorkbenchLayoutService); + const configurationService = accessor.get(IConfigurationService); + + const visibility = layoutService.isVisible(Parts.STATUSBAR_PART); const newVisibilityValue = !visibility; - return this.configurationService.updateValue(ToggleStatusbarVisibilityAction.statusbarVisibleKey, newVisibilityValue); + return configurationService.updateValue(ToggleStatusbarVisibilityAction.statusbarVisibleKey, newVisibilityValue); } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleStatusbarVisibilityAction), 'View: Toggle Status Bar Visibility', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleStatusbarVisibilityAction.ID, - title: localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "Show S&&tatus Bar"), - toggled: ContextKeyExpr.equals('config.workbench.statusBar.visible', true) - }, - order: 3 -}); +registerAction2(ToggleStatusbarVisibilityAction); // --- Toggle Tabs Visibility -class ToggleTabsVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleTabsVisibility'; - static readonly LABEL = localize('toggleTabs', "Toggle Tab Visibility"); - - private static readonly tabsVisibleKey = 'workbench.editor.showTabs'; +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleTabsVisibility', + title: { + value: localize('toggleTabs', "Toggle Tab Visibility"), + original: 'Toggle Tab Visibility' + }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, } + } + }); } - override run(): Promise { - const visibility = this.configurationService.getValue(ToggleTabsVisibilityAction.tabsVisibleKey); + run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const visibility = configurationService.getValue('workbench.editor.showTabs'); const newVisibilityValue = !visibility; - return this.configurationService.updateValue(ToggleTabsVisibilityAction.tabsVisibleKey, newVisibilityValue); + return configurationService.updateValue('workbench.editor.showTabs', newVisibilityValue); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTabsVisibilityAction, { - primary: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W, } -}), 'View: Toggle Tab Visibility', CATEGORIES.View.value); +}); // --- Toggle Zen Mode -class ToggleZenMode extends Action { - - static readonly ID = 'workbench.action.toggleZenMode'; - static readonly LABEL = localize('toggleZenMode', "Toggle Zen Mode"); +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleZenMode', + title: { + value: localize('toggleZenMode', "Toggle Zen Mode"), + mnemonicTitle: localize('miToggleZenMode', "Zen Mode"), + original: 'Toggle Zen Mode' + }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) + }, + toggled: InEditorZenModeContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '1_toggle_view', + order: 2 + } + }); } - override async run(): Promise { - this.layoutService.toggleZenMode(); + run(accessor: ServicesAccessor): void { + return accessor.get(IWorkbenchLayoutService).toggleZenMode(); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleZenMode, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) }), 'View: Toggle Zen Mode', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleZenMode.ID, - title: localize('miToggleZenMode', "Zen Mode"), - toggled: InEditorZenModeContext - }, - order: 2 }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -435,85 +427,103 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // --- Toggle Menu Bar -export class ToggleMenuBarAction extends Action { +if (isWindows || isLinux || isWeb) { + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.toggleMenuBar', + title: { + value: localize('toggleMenuBar', "Toggle Menu Bar"), + mnemonicTitle: localize({ key: 'miShowMenuBar', comment: ['&& denotes a mnemonic'] }, "Show Menu &&Bar"), + original: 'Toggle Menu Bar' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')), + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '2_workbench_layout', + order: 0 + } + }); + } - static readonly ID = 'workbench.action.toggleMenuBar'; - static readonly LABEL = localize('toggleMenuBar', "Toggle Menu Bar"); + run(accessor: ServicesAccessor): void { + return accessor.get(IWorkbenchLayoutService).toggleMenuBar(); + } + }); +} - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); - } +// --- Reset View Locations - override async run(): Promise { - this.layoutService.toggleMenuBar(); - } -} +registerAction2(class extends Action2 { -if (isWindows || isLinux || isWeb) { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMenuBarAction), 'View: Toggle Menu Bar', CATEGORIES.View.value); -} + constructor() { + super({ + id: 'workbench.action.resetViewLocations', + title: { + value: localize('resetViewLocations', "Reset View Locations"), + original: 'Reset View Locations' + }, + category: CATEGORIES.View, + f1: true + }); + } -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleMenuBarAction.ID, - title: localize({ key: 'miShowMenuBar', comment: ['&& denotes a mnemonic'] }, "Show Menu &&Bar"), - toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) - }, - when: IsMacNativeContext.toNegated(), - order: 0 + run(accessor: ServicesAccessor): void { + return accessor.get(IViewDescriptorService).reset(); + } }); -// --- Reset View Positions +// --- Move View -export class ResetViewLocationsAction extends Action { - static readonly ID = 'workbench.action.resetViewLocations'; - static readonly LABEL = localize('resetViewLocations', "Reset View Locations"); +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.moveView', + title: { + value: localize('moveView', "Move View"), + original: 'Move View' + }, + category: CATEGORIES.View, + f1: true + }); } - override async run(): Promise { - this.viewDescriptorService.reset(); - } -} + async run(accessor: ServicesAccessor): Promise { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const instantiationService = accessor.get(IInstantiationService); + const quickInputService = accessor.get(IQuickInputService); + const contextKeyService = accessor.get(IContextKeyService); + const activityBarService = accessor.get(IActivityBarService); + const panelService = accessor.get(IPanelService); + + const focusedViewId = FocusedViewContext.getValue(contextKeyService); + let viewId: string; + + if (focusedViewId && viewDescriptorService.getViewDescriptorById(focusedViewId)?.canMoveView) { + viewId = focusedViewId; + } -registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetViewLocationsAction), 'View: Reset View Locations', CATEGORIES.View.value); + viewId = await this.getView(quickInputService, activityBarService, viewDescriptorService, panelService, viewId!); -// --- Move View with Command -export class MoveViewAction extends Action { - static readonly ID = 'workbench.action.moveView'; - static readonly LABEL = localize('moveView', "Move View"); + if (!viewId) { + return; + } - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @IInstantiationService private instantiationService: IInstantiationService, - @IQuickInputService private quickInputService: IQuickInputService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IActivityBarService private activityBarService: IActivityBarService, - @IPanelService private panelService: IPanelService - ) { - super(id, label); + const moveFocusedViewAction = new MoveFocusedViewAction(); + instantiationService.invokeFunction(accessor => moveFocusedViewAction.run(accessor, viewId)); } - private getViewItems(): Array { + private getViewItems(activityBarService: IActivityBarService, viewDescriptorService: IViewDescriptorService, panelService: IPanelService): Array { const results: Array = []; - const viewlets = this.activityBarService.getVisibleViewContainerIds(); + const viewlets = activityBarService.getVisibleViewContainerIds(); viewlets.forEach(viewletId => { - const container = this.viewDescriptorService.getViewContainerById(viewletId)!; - const containerModel = this.viewDescriptorService.getViewContainerModel(container); + const container = viewDescriptorService.getViewContainerById(viewletId)!; + const containerModel = viewDescriptorService.getViewContainerModel(container); let hasAddedView = false; containerModel.visibleViewDescriptors.forEach(viewDescriptor => { @@ -534,10 +544,10 @@ export class MoveViewAction extends Action { }); }); - const panels = this.panelService.getPinnedPanels(); + const panels = panelService.getPinnedPanels(); panels.forEach(panel => { - const container = this.viewDescriptorService.getViewContainerById(panel.id)!; - const containerModel = this.viewDescriptorService.getViewContainerModel(container); + const container = viewDescriptorService.getViewContainerById(panel.id)!; + const containerModel = viewDescriptorService.getViewContainerModel(container); let hasAddedView = false; containerModel.visibleViewDescriptors.forEach(viewDescriptor => { @@ -561,10 +571,10 @@ export class MoveViewAction extends Action { return results; } - private async getView(viewId?: string): Promise { - const quickPick = this.quickInputService.createQuickPick(); + private async getView(quickInputService: IQuickInputService, activityBarService: IActivityBarService, viewDescriptorService: IViewDescriptorService, panelService: IPanelService, viewId?: string): Promise { + const quickPick = quickInputService.createQuickPick(); quickPick.placeholder = localize('moveFocusedView.selectView', "Select a View to Move"); - quickPick.items = this.getViewItems(); + quickPick.items = this.getViewItems(activityBarService, viewDescriptorService, panelService); quickPick.selectedItems = quickPick.items.filter(item => (item as IQuickPickItem).id === viewId) as IQuickPickItem[]; return new Promise((resolve, reject) => { @@ -584,68 +594,55 @@ export class MoveViewAction extends Action { quickPick.show(); }); } +}); - override async run(): Promise { - const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); - let viewId: string; +// --- Move Focused View - if (focusedViewId && this.viewDescriptorService.getViewDescriptorById(focusedViewId)?.canMoveView) { - viewId = focusedViewId; - } +class MoveFocusedViewAction extends Action2 { - viewId = await this.getView(viewId!); - - if (!viewId) { - return; - } - - this.instantiationService.createInstance(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL).run(viewId); + constructor() { + super({ + id: 'workbench.action.moveFocusedView', + title: { + value: localize('moveFocusedView', "Move Focused View"), + original: 'Move Focused View' + }, + category: CATEGORIES.View, + precondition: FocusedViewContext.notEqualsTo(''), + f1: true + }); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveViewAction), 'View: Move View', CATEGORIES.View.value); - -// --- Move Focused View with Command -export class MoveFocusedViewAction extends Action { - static readonly ID = 'workbench.action.moveFocusedView'; - static readonly LABEL = localize('moveFocusedView', "Move Focused View"); - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @IViewsService private viewsService: IViewsService, - @IQuickInputService private quickInputService: IQuickInputService, - @IContextKeyService private contextKeyService: IContextKeyService, - @INotificationService private notificationService: INotificationService, - @IActivityBarService private activityBarService: IActivityBarService, - @IPanelService private panelService: IPanelService - ) { - super(id, label); - } + run(accessor: ServicesAccessor, viewId?: string): void { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const viewsService = accessor.get(IViewsService); + const quickInputService = accessor.get(IQuickInputService); + const contextKeyService = accessor.get(IContextKeyService); + const dialogService = accessor.get(IDialogService); + const activityBarService = accessor.get(IActivityBarService); + const panelService = accessor.get(IPanelService); - override async run(viewId: string): Promise { - const focusedViewId = viewId || FocusedViewContext.getValue(this.contextKeyService); + const focusedViewId = viewId || FocusedViewContext.getValue(contextKeyService); if (focusedViewId === undefined || focusedViewId.trim() === '') { - this.notificationService.error(localize('moveFocusedView.error.noFocusedView', "There is no view currently focused.")); + dialogService.show(Severity.Error, localize('moveFocusedView.error.noFocusedView', "There is no view currently focused."), [localize('ok', 'OK')]); return; } - const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(focusedViewId); + const viewDescriptor = viewDescriptorService.getViewDescriptorById(focusedViewId); if (!viewDescriptor || !viewDescriptor.canMoveView) { - this.notificationService.error(localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable.")); + dialogService.show(Severity.Error, localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable."), [localize('ok', 'OK')]); return; } - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick(); quickPick.placeholder = localize('moveFocusedView.selectDestination', "Select a Destination for the View"); quickPick.title = localize({ key: 'moveFocusedView.title', comment: ['{0} indicates the title of the view the user has selected to move.'] }, "View: Move {0}", viewDescriptor.name); const items: Array = []; - const currentContainer = this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!; - const currentLocation = this.viewDescriptorService.getViewLocationById(focusedViewId)!; - const isViewSolo = this.viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; + const currentContainer = viewDescriptorService.getViewContainerByViewId(focusedViewId)!; + const currentLocation = viewDescriptorService.getViewLocationById(focusedViewId)!; + const isViewSolo = viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; if (!(isViewSolo && currentLocation === ViewContainerLocation.Panel)) { items.push({ @@ -666,19 +663,19 @@ export class MoveFocusedViewAction extends Action { label: localize('sidebar', "Side Bar") }); - const pinnedViewlets = this.activityBarService.getVisibleViewContainerIds(); + const pinnedViewlets = activityBarService.getVisibleViewContainerIds(); items.push(...pinnedViewlets .filter(viewletId => { - if (viewletId === this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { + if (viewletId === viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { return false; } - return !this.viewDescriptorService.getViewContainerById(viewletId)!.rejectAddedViews; + return !viewDescriptorService.getViewContainerById(viewletId)!.rejectAddedViews; }) .map(viewletId => { return { id: viewletId, - label: this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerById(viewletId)!)!.title + label: viewDescriptorService.getViewContainerModel(viewDescriptorService.getViewContainerById(viewletId)!)!.title }; })); @@ -687,19 +684,19 @@ export class MoveFocusedViewAction extends Action { label: localize('panel', "Panel") }); - const pinnedPanels = this.panelService.getPinnedPanels(); + const pinnedPanels = panelService.getPinnedPanels(); items.push(...pinnedPanels .filter(panel => { - if (panel.id === this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { + if (panel.id === viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { return false; } - return !this.viewDescriptorService.getViewContainerById(panel.id)!.rejectAddedViews; + return !viewDescriptorService.getViewContainerById(panel.id)!.rejectAddedViews; }) .map(panel => { return { id: panel.id, - label: this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerById(panel.id)!)!.title + label: viewDescriptorService.getViewContainerModel(viewDescriptorService.getViewContainerById(panel.id)!)!.title }; })); @@ -709,14 +706,14 @@ export class MoveFocusedViewAction extends Action { const destination = quickPick.selectedItems[0]; if (destination.id === '_.panel.newcontainer') { - this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); - this.viewsService.openView(focusedViewId, true); + viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); + viewsService.openView(focusedViewId, true); } else if (destination.id === '_.sidebar.newcontainer') { - this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Sidebar); - this.viewsService.openView(focusedViewId, true); + viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Sidebar); + viewsService.openView(focusedViewId, true); } else if (destination.id) { - this.viewDescriptorService.moveViewsToContainer([viewDescriptor], this.viewDescriptorService.getViewContainerById(destination.id)!); - this.viewsService.openView(focusedViewId, true); + viewDescriptorService.moveViewsToContainer([viewDescriptor], viewDescriptorService.getViewContainerById(destination.id)!); + viewsService.openView(focusedViewId, true); } quickPick.hide(); @@ -726,54 +723,56 @@ export class MoveFocusedViewAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveFocusedViewAction), 'View: Move Focused View', CATEGORIES.View.value, FocusedViewContext.notEqualsTo('')); +registerAction2(MoveFocusedViewAction); + +// --- Reset Focused View Location -// --- Reset View Location with Command -export class ResetFocusedViewLocationAction extends Action { - static readonly ID = 'workbench.action.resetFocusedViewLocation'; - static readonly LABEL = localize('resetFocusedViewLocation', "Reset Focused View Location"); +registerAction2(class extends Action2 { - constructor( - id: string, - label: string, - @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, - @IContextKeyService private contextKeyService: IContextKeyService, - @INotificationService private notificationService: INotificationService, - @IViewsService private viewsService: IViewsService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.resetFocusedViewLocation', + title: { + value: localize('resetFocusedViewLocation', "Reset Focused View Location"), + original: 'Reset Focused View Location' + }, + category: CATEGORIES.View, + f1: true, + precondition: FocusedViewContext.notEqualsTo('') + }); } - override async run(): Promise { - const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + run(accessor: ServicesAccessor): void { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const contextKeyService = accessor.get(IContextKeyService); + const dialogService = accessor.get(IDialogService); + const viewsService = accessor.get(IViewsService); + + const focusedViewId = FocusedViewContext.getValue(contextKeyService); let viewDescriptor: IViewDescriptor | null = null; if (focusedViewId !== undefined && focusedViewId.trim() !== '') { - viewDescriptor = this.viewDescriptorService.getViewDescriptorById(focusedViewId); + viewDescriptor = viewDescriptorService.getViewDescriptorById(focusedViewId); } if (!viewDescriptor) { - this.notificationService.error(localize('resetFocusedView.error.noFocusedView', "There is no view currently focused.")); + dialogService.show(Severity.Error, localize('resetFocusedView.error.noFocusedView', "There is no view currently focused."), [localize('ok', 'OK')]); return; } - const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewDescriptor.id); - if (!defaultContainer || defaultContainer === this.viewDescriptorService.getViewContainerByViewId(viewDescriptor.id)) { + const defaultContainer = viewDescriptorService.getDefaultContainerById(viewDescriptor.id); + if (!defaultContainer || defaultContainer === viewDescriptorService.getViewContainerByViewId(viewDescriptor.id)) { return; } - this.viewDescriptorService.moveViewsToContainer([viewDescriptor], defaultContainer); - this.viewsService.openView(viewDescriptor.id, true); - + viewDescriptorService.moveViewsToContainer([viewDescriptor], defaultContainer); + viewsService.openView(viewDescriptor.id, true); } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetFocusedViewLocationAction), 'View: Reset Focused View Location', CATEGORIES.View.value, FocusedViewContext.notEqualsTo('')); - +}); // --- Resize View -export abstract class BaseResizeViewAction extends Action2 { +abstract class BaseResizeViewAction extends Action2 { protected static readonly RESIZE_INCREMENT = 6.5; // This is a media-size percentage @@ -802,7 +801,7 @@ export abstract class BaseResizeViewAction extends Action2 { } } -export class IncreaseViewSizeAction extends BaseResizeViewAction { +class IncreaseViewSizeAction extends BaseResizeViewAction { constructor() { super({ @@ -812,12 +811,12 @@ export class IncreaseViewSizeAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT, BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService)); } } -export class IncreaseViewWidthAction extends BaseResizeViewAction { +class IncreaseViewWidthAction extends BaseResizeViewAction { constructor() { super({ @@ -827,12 +826,12 @@ export class IncreaseViewWidthAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT, 0, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } -export class IncreaseViewHeightAction extends BaseResizeViewAction { +class IncreaseViewHeightAction extends BaseResizeViewAction { constructor() { super({ @@ -841,12 +840,13 @@ export class IncreaseViewHeightAction extends BaseResizeViewAction { f1: true }); } - async run(accessor: ServicesAccessor): Promise { + + run(accessor: ServicesAccessor): void { this.resizePart(0, BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } -export class DecreaseViewSizeAction extends BaseResizeViewAction { +class DecreaseViewSizeAction extends BaseResizeViewAction { constructor() { super({ @@ -856,12 +856,12 @@ export class DecreaseViewSizeAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT, -BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService)); } } -export class DecreaseViewWidthAction extends BaseResizeViewAction { +class DecreaseViewWidthAction extends BaseResizeViewAction { constructor() { super({ id: 'workbench.action.decreaseViewWidth', @@ -870,13 +870,12 @@ export class DecreaseViewWidthAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT, 0, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } - -export class DecreaseViewHeightAction extends BaseResizeViewAction { +class DecreaseViewHeightAction extends BaseResizeViewAction { constructor() { super({ @@ -886,7 +885,7 @@ export class DecreaseViewHeightAction extends BaseResizeViewAction { }); } - async run(accessor: ServicesAccessor): Promise { + run(accessor: ServicesAccessor): void { this.resizePart(0, -BaseResizeViewAction.RESIZE_INCREMENT, accessor.get(IWorkbenchLayoutService), Parts.EDITOR_PART); } } diff --git a/lib/vscode/src/vs/workbench/browser/actions/windowActions.ts b/lib/vscode/src/vs/workbench/browser/actions/windowActions.ts index 358af6804197..08c85b251c21 100644 --- a/lib/vscode/src/vs/workbench/browser/actions/windowActions.ts +++ b/lib/vscode/src/vs/workbench/browser/actions/windowActions.ts @@ -4,15 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { SyncActionDescriptor, MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { MenuRegistry, MenuId, Action2, registerAction2, IAction2Options } from 'vs/platform/actions/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys'; -import { IsMacNativeContext, IsDevelopmentContext, IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; -import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { IsMacNativeContext, IsDevelopmentContext, IsWebContext, IsIOSContext } from 'vs/platform/contextkey/common/contextkeys'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -34,6 +32,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { isHTMLElement } from 'vs/base/browser/dom'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -42,7 +41,9 @@ interface IRecentlyOpenedPick extends IQuickPickItem { openable: IWindowOpenable; } -abstract class BaseOpenRecentAction extends Action { +const fileCategory = { value: localize('file', "File"), original: 'File' }; + +abstract class BaseOpenRecentAction extends Action2 { private readonly removeFromRecentlyOpened: IQuickInputButton = { iconClass: Codicon.removeClose.classNames, @@ -60,27 +61,25 @@ abstract class BaseOpenRecentAction extends Action { tooltip: localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"), }; - constructor( - id: string, - label: string, - private workspacesService: IWorkspacesService, - private quickInputService: IQuickInputService, - private contextService: IWorkspaceContextService, - private labelService: ILabelService, - private keybindingService: IKeybindingService, - private modelService: IModelService, - private modeService: IModeService, - private hostService: IHostService, - private dialogService: IDialogService - ) { - super(id, label); + constructor(desc: Readonly) { + super(desc); } protected abstract isQuickNavigate(): boolean; - override async run(): Promise { - const recentlyOpened = await this.workspacesService.getRecentlyOpened(); - const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces(); + override async run(accessor: ServicesAccessor): Promise { + const workspacesService = accessor.get(IWorkspacesService); + const quickInputService = accessor.get(IQuickInputService); + const contextService = accessor.get(IWorkspaceContextService); + const labelService = accessor.get(ILabelService); + const keybindingService = accessor.get(IKeybindingService); + const modelService = accessor.get(IModelService); + const modeService = accessor.get(IModeService); + const hostService = accessor.get(IHostService); + const dialogService = accessor.get(IDialogService); + + const recentlyOpened = await workspacesService.getRecentlyOpened(); + const dirtyWorkspacesAndFolders = await workspacesService.getDirtyWorkspaces(); let hasWorkspaces = false; @@ -113,23 +112,23 @@ abstract class BaseOpenRecentAction extends Action { for (const recent of recentlyOpened.workspaces) { const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath); - workspacePicks.push(this.toQuickPick(recent, isDirty)); + workspacePicks.push(this.toQuickPick(modelService, modeService, labelService, recent, isDirty)); } // Fill any backup workspace that is not yet shown at the end for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) { if (URI.isUri(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder)) { - workspacePicks.push(this.toQuickPick({ folderUri: dirtyWorkspaceOrFolder }, true)); + workspacePicks.push(this.toQuickPick(modelService, modeService, labelService, { folderUri: dirtyWorkspaceOrFolder }, true)); } else if (isWorkspaceIdentifier(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.configPath)) { - workspacePicks.push(this.toQuickPick({ workspace: dirtyWorkspaceOrFolder }, true)); + workspacePicks.push(this.toQuickPick(modelService, modeService, labelService, { workspace: dirtyWorkspaceOrFolder }, true)); } } - const filePicks = recentlyOpened.files.map(p => this.toQuickPick(p, false)); + const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, modeService, labelService, p, false)); // focus second entry if the first recent workspace is the current workspace const firstEntry = recentlyOpened.workspaces[0]; - const autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); + const autoFocusSecondEntry: boolean = firstEntry && contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); let keyMods: IKeyMods | undefined; @@ -137,25 +136,25 @@ abstract class BaseOpenRecentAction extends Action { const fileSeparator: IQuickPickSeparator = { type: 'separator', label: localize('files', "files") }; const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; - const pick = await this.quickInputService.pick(picks, { + const pick = await quickInputService.pick(picks, { contextKey: inRecentFilesPickerContextKey, activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], placeHolder: isMacintosh ? localize('openRecentPlaceholderMac', "Select to open (hold Cmd-key to force new window or Alt-key for same window)") : localize('openRecentPlaceholder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"), matchOnDescription: true, onKeyMods: mods => keyMods = mods, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, onDidTriggerItemButton: async context => { // Remove if (context.button === this.removeFromRecentlyOpened) { - await this.workspacesService.removeRecentlyOpened([context.item.resource]); + await workspacesService.removeRecentlyOpened([context.item.resource]); context.removeItem(); } // Dirty Folder/Workspace else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) { const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace; - const result = await this.dialogService.confirm({ + const result = await dialogService.confirm({ type: 'question', title: isDirtyWorkspace ? localize('dirtyWorkspace', "Workspace with Unsaved Files") : localize('dirtyFolder', "Folder with Unsaved Files"), message: isDirtyWorkspace ? localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"), @@ -163,19 +162,19 @@ abstract class BaseOpenRecentAction extends Action { }); if (result.confirmed) { - this.hostService.openWindow([context.item.openable]); - this.quickInputService.cancel(); + hostService.openWindow([context.item.openable]); + quickInputService.cancel(); } } } }); if (pick) { - return this.hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt }); + return hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt }); } } - private toQuickPick(recent: IRecent, isDirty: boolean): IRecentlyOpenedPick { + private toQuickPick(modelService: IModelService, modeService: IModeService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick { let openable: IWindowOpenable | undefined; let iconClasses: string[]; let fullLabel: string | undefined; @@ -185,26 +184,26 @@ abstract class BaseOpenRecentAction extends Action { // Folder if (isRecentFolder(recent)) { resource = recent.folderUri; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER); + iconClasses = getIconClasses(modelService, modeService, resource, FileKind.FOLDER); openable = { folderUri: resource }; - fullLabel = recent.label || this.labelService.getWorkspaceLabel(resource, { verbose: true }); + fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: true }); } // Workspace else if (isRecentWorkspace(recent)) { resource = recent.workspace.configPath; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); + iconClasses = getIconClasses(modelService, modeService, resource, FileKind.ROOT_FOLDER); openable = { workspaceUri: resource }; - fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); isWorkspace = true; } // File else { resource = recent.fileUri; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE); + iconClasses = getIconClasses(modelService, modeService, resource, FileKind.FILE); openable = { fileUri: resource }; - fullLabel = recent.label || this.labelService.getUriLabel(resource); + fullLabel = recent.label || labelService.getUriLabel(resource); } const { name, parentPath } = splitName(fullLabel); @@ -223,23 +222,27 @@ abstract class BaseOpenRecentAction extends Action { export class OpenRecentAction extends BaseOpenRecentAction { - static readonly ID = 'workbench.action.openRecent'; - static readonly LABEL = localize('openRecent', "Open Recent..."); - - constructor( - id: string, - label: string, - @IWorkspacesService workspacesService: IWorkspacesService, - @IQuickInputService quickInputService: IQuickInputService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILabelService labelService: ILabelService, - @IHostService hostService: IHostService, - @IDialogService dialogService: IDialogService - ) { - super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService); + constructor() { + super({ + id: 'workbench.action.openRecent', + title: { + value: localize('openRecent', "Open Recent..."), + mnemonicTitle: localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."), + original: 'Open Recent...' + }, + category: fileCategory, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_R, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_R } + }, + menu: { + id: MenuId.MenubarRecentMenu, + group: 'y_more', + order: 1 + } + }); } protected isQuickNavigate(): boolean { @@ -249,23 +252,13 @@ export class OpenRecentAction extends BaseOpenRecentAction { class QuickPickRecentAction extends BaseOpenRecentAction { - static readonly ID = 'workbench.action.quickOpenRecent'; - static readonly LABEL = localize('quickOpenRecent', "Quick Open Recent..."); - - constructor( - id: string, - label: string, - @IWorkspacesService workspacesService: IWorkspacesService, - @IQuickInputService quickInputService: IQuickInputService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILabelService labelService: ILabelService, - @IHostService hostService: IHostService, - @IDialogService dialogService: IDialogService - ) { - super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService); + constructor() { + super({ + id: 'workbench.action.quickOpenRecent', + title: { value: localize('quickOpenRecent', "Quick Open Recent..."), original: 'Quick Open Recent...' }, + category: fileCategory, + f1: true + }); } protected isQuickNavigate(): boolean { @@ -273,75 +266,122 @@ class QuickPickRecentAction extends BaseOpenRecentAction { } } -class ToggleFullScreenAction extends Action { - - static readonly ID = 'workbench.action.toggleFullScreen'; - static readonly LABEL = localize('toggleFullScreen', "Toggle Full Screen"); +class ToggleFullScreenAction extends Action2 { - constructor( - id: string, - label: string, - @IHostService private readonly hostService: IHostService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.toggleFullScreen', + title: { + value: localize('toggleFullScreen', "Toggle Full Screen"), + mnemonicTitle: localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "&&Full Screen"), + original: 'Toggle Full Screen' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F11, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_F + } + }, + precondition: IsIOSContext.toNegated(), + toggled: IsFullscreenContext, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '1_toggle_view', + order: 1 + } + }); } - override run(): Promise { - return this.hostService.toggleFullScreen(); + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + + return hostService.toggleFullScreen(); } } -export class ReloadWindowAction extends Action { +export class ReloadWindowAction extends Action2 { static readonly ID = 'workbench.action.reloadWindow'; - static readonly LABEL = localize('reloadWindow', "Reload Window"); - - constructor( - id: string, - label: string, - @IHostService private readonly hostService: IHostService - ) { - super(id, label); + + constructor() { + super({ + id: ReloadWindowAction.ID, + title: { value: localize('reloadWindow', "Reload Window"), original: 'Reload Window' }, + category: CATEGORIES.Developer.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 50, + when: IsDevelopmentContext, + primary: KeyMod.CtrlCmd | KeyCode.KEY_R + } + }); } - override async run(): Promise { - await this.hostService.reload(); + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + + return hostService.reload(); } } -class ShowAboutDialogAction extends Action { - - static readonly ID = 'workbench.action.showAboutDialog'; - static readonly LABEL = localize('about', "About"); +class ShowAboutDialogAction extends Action2 { - constructor( - id: string, - label: string, - @IDialogService private readonly dialogService: IDialogService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.showAboutDialog', + title: { + value: localize('about', "About"), + mnemonicTitle: localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About"), + original: 'About' + }, + category: CATEGORIES.Help.value, + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: 'z_about', + order: 1, + when: IsMacNativeContext.toNegated() + } + }); } - override run(): Promise { - return this.dialogService.about(); + override run(accessor: ServicesAccessor): Promise { + const dialogService = accessor.get(IDialogService); + + return dialogService.about(); } } -export class NewWindowAction extends Action { - - static readonly ID = 'workbench.action.newWindow'; - static readonly LABEL = localize('newWindow', "New Window"); +class NewWindowAction extends Action2 { - constructor( - id: string, - label: string, - @IHostService private readonly hostService: IHostService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.newWindow', + title: { + value: localize('newWindow', "New Window"), + mnemonicTitle: localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window"), + original: 'New Window' + }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_N + }, + menu: { + id: MenuId.MenubarFileMenu, + group: '1_new', + order: 2 + } + }); } - override run(): Promise { - return this.hostService.openWindow({ remoteAuthority: null }); + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + + return hostService.openWindow({ remoteAuthority: null }); } } @@ -350,7 +390,7 @@ class BlurAction extends Action2 { constructor() { super({ id: 'workbench.action.blur', - title: localize('blur', "Remove keyboard focus from focused element") + title: { value: localize('blur', "Remove keyboard focus from focused element"), original: 'Remove keyboard focus from focused element' } }); } @@ -363,21 +403,14 @@ class BlurAction extends Action2 { } } -const registry = Registry.as(Extensions.WorkbenchActions); - // --- Actions Registration -const fileCategory = localize('file', "File"); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NewWindowAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_N }), 'New Window'); -registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickPickRecentAction), 'File: Quick Open Recent...', fileCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenRecentAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_R, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_R } }), 'File: Open Recent...', fileCategory); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleFullScreenAction, { primary: KeyCode.F11, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_F } }), 'View: Toggle Full Screen', CATEGORIES.View.value); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ReloadWindowAction), 'Developer: Reload Window', CATEGORIES.Developer.value, IsWebContext.toNegated()); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAboutDialogAction), `Help: About`, CATEGORIES.Help.value); - +registerAction2(NewWindowAction); +registerAction2(ToggleFullScreenAction); +registerAction2(QuickPickRecentAction); +registerAction2(OpenRecentAction); +registerAction2(ReloadWindowAction); +registerAction2(ShowAboutDialogAction); registerAction2(BlurAction); // --- Commands/Keybindings Registration @@ -404,13 +437,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R } }); -KeybindingsRegistry.registerKeybindingRule({ - id: ReloadWindowAction.ID, - weight: KeybindingWeight.WorkbenchContrib + 50, - when: IsDevelopmentContext, - primary: KeyMod.CtrlCmd | KeyCode.KEY_R -}); - CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', accessor => { const configurationService = accessor.get(IConfigurationService); const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; @@ -431,47 +457,9 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { when: IsWebContext }); -MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '1_new', - command: { - id: NewWindowAction.ID, - title: localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window") - }, - order: 2 -}); - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { title: localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"), submenu: MenuId.MenubarRecentMenu, group: '2_open', order: 4 }); - -MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { - group: 'y_more', - command: { - id: OpenRecentAction.ID, - title: localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More...") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleFullScreenAction.ID, - title: localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "&&Full Screen"), - toggled: IsFullscreenContext - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: 'z_about', - command: { - id: ShowAboutDialogAction.ID, - title: localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About") - }, - order: 1, - when: IsMacNativeContext.toNegated() -}); diff --git a/lib/vscode/src/vs/workbench/browser/actions/workspaceActions.ts b/lib/vscode/src/vs/workbench/browser/actions/workspaceActions.ts index 58837b5a2582..3824b4d3c4ba 100644 --- a/lib/vscode/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/lib/vscode/src/vs/workbench/browser/actions/workspaceActions.ts @@ -11,20 +11,21 @@ import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/commo import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { MenuRegistry, MenuId, SyncActionDescriptor, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { MenuRegistry, MenuId, Action2, registerAction2, ILocalizedString } from 'vs/platform/actions/common/actions'; import { EmptyWorkspaceSupportContext, WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import Severity from 'vs/base/common/severity'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspacesService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { WORKSPACE_TRUST_ENABLED } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { WorkspaceTrustContext, WORKSPACE_TRUST_ENABLED } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; + +const workspacesCategory: ILocalizedString = { value: localize('workspaces', "Workspaces"), original: 'Workspaces' }; export class OpenFileAction extends Action { @@ -98,29 +99,36 @@ export class OpenWorkspaceAction extends Action { } } -export class CloseWorkspaceAction extends Action { +export class CloseWorkspaceAction extends Action2 { static readonly ID = 'workbench.action.closeFolder'; - static readonly LABEL = localize('closeWorkspace', "Close Workspace"); - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @INotificationService private readonly notificationService: INotificationService, - @IHostService private readonly hostService: IHostService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService - ) { - super(id, label); + constructor() { + super({ + id: CloseWorkspaceAction.ID, + title: { value: localize('closeWorkspace', "Close Workspace"), original: 'Close Workspace' }, + category: workspacesCategory, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: EmptyWorkspaceSupportContext, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) + } + }); } - override async run(): Promise { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.info(localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close.")); + override async run(accessor: ServicesAccessor): Promise { + const contextService = accessor.get(IWorkspaceContextService); + const dialogService = accessor.get(IDialogService); + const hostService = accessor.get(IHostService); + const environmentService = accessor.get(IWorkbenchEnvironmentService); + + if (contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + dialogService.show(Severity.Error, localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close."), [localize('ok', 'OK')]); return; } - return this.hostService.openWindow({ forceReuseWindow: true, remoteAuthority: this.environmentService.remoteAuthority }); + return hostService.openWindow({ forceReuseWindow: true, remoteAuthority: environmentService.remoteAuthority }); } } @@ -148,116 +156,120 @@ export class OpenWorkspaceConfigFileAction extends Action { } } -export class AddRootFolderAction extends Action { +export class AddRootFolderAction extends Action2 { static readonly ID = 'workbench.action.addRootFolder'; - static readonly LABEL = ADD_ROOT_FOLDER_LABEL; - constructor( - id: string, - label: string, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label); + constructor() { + super({ + id: AddRootFolderAction.ID, + title: ADD_ROOT_FOLDER_LABEL, + category: workspacesCategory, + f1: true + }); } - override run(): Promise { - return this.commandService.executeCommand(ADD_ROOT_FOLDER_COMMAND_ID); + override run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + + return commandService.executeCommand(ADD_ROOT_FOLDER_COMMAND_ID); } } -export class GlobalRemoveRootFolderAction extends Action { - - static readonly ID = 'workbench.action.removeRootFolder'; - static readonly LABEL = localize('globalRemoveFolderFromWorkspace', "Remove Folder from Workspace..."); +class RemoveRootFolderAction extends Action2 { - constructor( - id: string, - label: string, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.removeRootFolder', + title: { value: localize('globalRemoveFolderFromWorkspace', "Remove Folder from Workspace..."), original: 'Remove Folder from Workspace...' }, + category: workspacesCategory, + f1: true + }); } - override async run(): Promise { - const state = this.contextService.getWorkbenchState(); + override async run(accessor: ServicesAccessor): Promise { + const contextService = accessor.get(IWorkspaceContextService); + const commandService = accessor.get(ICommandService); + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + + const state = contextService.getWorkbenchState(); // Workspace / Folder if (state === WorkbenchState.WORKSPACE || state === WorkbenchState.FOLDER) { - const folder = await this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); + const folder = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); if (folder) { - await this.workspaceEditingService.removeFolders([folder.uri]); + await workspaceEditingService.removeFolders([folder.uri]); } } } } -export class SaveWorkspaceAsAction extends Action { +class SaveWorkspaceAsAction extends Action2 { static readonly ID = 'workbench.action.saveWorkspaceAs'; - static readonly LABEL = localize('saveWorkspaceAsAction', "Save Workspace As..."); - - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService - ) { - super(id, label); + constructor() { + super({ + id: SaveWorkspaceAsAction.ID, + title: { value: localize('saveWorkspaceAsAction', "Save Workspace As..."), original: 'Save Workspace As...' }, + category: workspacesCategory, + f1: true + }); } - override async run(): Promise { - const configPathUri = await this.workspaceEditingService.pickNewWorkspacePath(); + override async run(accessor: ServicesAccessor): Promise { + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + const contextService = accessor.get(IWorkspaceContextService); + + const configPathUri = await workspaceEditingService.pickNewWorkspacePath(); if (configPathUri && hasWorkspaceFileExtension(configPathUri)) { - switch (this.contextService.getWorkbenchState()) { + switch (contextService.getWorkbenchState()) { case WorkbenchState.EMPTY: case WorkbenchState.FOLDER: - const folders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); - return this.workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); + const folders = contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); + return workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); case WorkbenchState.WORKSPACE: - return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri); + return workspaceEditingService.saveAndEnterWorkspace(configPathUri); } } } } -export class DuplicateWorkspaceInNewWindowAction extends Action { +class DuplicateWorkspaceInNewWindowAction extends Action2 { - static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; - static readonly LABEL = localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"); - - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, - @IHostService private readonly hostService: IHostService, - @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.duplicateWorkspaceInNewWindow', + title: { value: localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"), original: 'Duplicate As Workspace in New Window' }, + category: workspacesCategory, + f1: true + }); } - override async run(): Promise { - const folders = this.workspaceContextService.getWorkspace().folders; - const remoteAuthority = this.environmentService.remoteAuthority; + override async run(accessor: ServicesAccessor): Promise { + const workspaceContextService = accessor.get(IWorkspaceContextService); + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + const hostService = accessor.get(IHostService); + const workspacesService = accessor.get(IWorkspacesService); + const environmentService = accessor.get(IWorkbenchEnvironmentService); - const newWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority); - await this.workspaceEditingService.copyWorkspaceSettings(newWorkspace); + const folders = workspaceContextService.getWorkspace().folders; + const remoteAuthority = environmentService.remoteAuthority; - return this.hostService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); + const newWorkspace = await workspacesService.createUntitledWorkspace(folders, remoteAuthority); + await workspaceEditingService.copyWorkspaceSettings(newWorkspace); + + return hostService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); } } class WorkspaceTrustManageAction extends Action2 { + constructor() { super({ id: 'workbench.action.manageTrust', title: { value: localize('manageTrustAction', "Manage Workspace Trust"), original: 'Manage Workspace Trust' }, - precondition: ContextKeyExpr.and(IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), + precondition: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), category: localize('workspacesCategory', "Workspaces"), f1: true }); @@ -273,14 +285,11 @@ registerAction2(WorkspaceTrustManageAction); // --- Actions Registration -const registry = Registry.as(Extensions.WorkbenchActions); -const workspacesCategory = localize('workspaces', "Workspaces"); - -registry.registerWorkbenchAction(SyncActionDescriptor.from(AddRootFolderAction), 'Workspaces: Add Folder to Workspace...', workspacesCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalRemoveRootFolderAction), 'Workspaces: Remove Folder from Workspace...', workspacesCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseWorkspaceAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, EmptyWorkspaceSupportContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveWorkspaceAsAction), 'Workspaces: Save Workspace As...', workspacesCategory, EmptyWorkspaceSupportContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate As Workspace in New Window', workspacesCategory); +registerAction2(AddRootFolderAction); +registerAction2(RemoveRootFolderAction); +registerAction2(CloseWorkspaceAction); +registerAction2(SaveWorkspaceAsAction); +registerAction2(DuplicateWorkspaceInNewWindowAction); // --- Menu Registration @@ -310,7 +319,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: OpenWorkspaceConfigFileAction.ID, - title: { value: `${workspacesCategory}: ${OpenWorkspaceConfigFileAction.LABEL}`, original: 'Workspaces: Open Workspace Configuration File' }, + title: { value: OpenWorkspaceConfigFileAction.LABEL, original: 'Workspaces: Open Workspace Configuration File' }, + category: workspacesCategory }, when: WorkbenchStateContext.isEqualTo('workspace') }); diff --git a/lib/vscode/src/vs/workbench/browser/actions/workspaceCommands.ts b/lib/vscode/src/vs/workbench/browser/actions/workspaceCommands.ts index c9e7bdde3c81..2b834b652c89 100644 --- a/lib/vscode/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/lib/vscode/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -20,12 +20,13 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IFileDialogService, IPickAndOpenOptions } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { IOpenWindowOptions, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +import { IOpenEmptyWindowOptions, IOpenWindowOptions, IWindowOpenable } from 'vs/platform/windows/common/windows'; +import { hasWorkspaceFileExtension, IRecent, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { ILocalizedString } from 'vs/platform/actions/common/actions'; export const ADD_ROOT_FOLDER_COMMAND_ID = 'addRootFolder'; -export const ADD_ROOT_FOLDER_LABEL = localize('addFolderToWorkspace', "Add Folder to Workspace..."); +export const ADD_ROOT_FOLDER_LABEL: ILocalizedString = { value: localize('addFolderToWorkspace', "Add Folder to Workspace..."), original: 'Add Folder to Workspace...' }; export const PICK_WORKSPACE_FOLDER_COMMAND_ID = '_workbench.pickWorkspaceFolder'; @@ -184,3 +185,91 @@ CommandsRegistry.registerCommand({ ] } }); + +interface INewWindowAPICommandOptions { + reuseWindow?: boolean; + /** + * If set, defines the remoteAuthority of the new window. `null` will open a local window. + * If not set, defaults to remoteAuthority of the current window. + */ + remoteAuthority?: string | null; +} + +CommandsRegistry.registerCommand({ + id: 'vscode.newWindow', + handler: (accessor: ServicesAccessor, options?: INewWindowAPICommandOptions) => { + const commandOptions: IOpenEmptyWindowOptions = { + forceReuseWindow: options && options.reuseWindow, + remoteAuthority: options && options.remoteAuthority + }; + const commandService = accessor.get(ICommandService); + return commandService.executeCommand('_files.newWindow', commandOptions); + }, + description: { + description: 'Opens an new window depending on the newWindow argument.', + args: [ + { + name: 'options', + description: '(optional) Options. Object with the following properties: ' + + '`reuseWindow`: Whether to open a new window or the same. Defaults to opening in a new window. ', + constraint: (value: any) => value === undefined || typeof value === 'object' + } + ] + } +}); + +// recent history commands + +CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, uri: URI) { + const workspacesService = accessor.get(IWorkspacesService); + return workspacesService.removeRecentlyOpened([uri]); +}); + + +CommandsRegistry.registerCommand({ + id: 'vscode.removeFromRecentlyOpened', + handler: (accessor: ServicesAccessor, path: string | URI): Promise => { + if (typeof path === 'string') { + path = path.match(/^[^:/?#]+:\/\//) ? URI.parse(path) : URI.file(path); + } else { + path = URI.revive(path); // called from extension host + } + const workspacesService = accessor.get(IWorkspacesService); + return workspacesService.removeRecentlyOpened([path]); + }, + description: { + description: 'Removes an entry with the given path from the recently opened list.', + args: [ + { name: 'path', description: 'URI or URI string to remove from recently opened.', constraint: (value: any) => typeof value === 'string' || value instanceof URI } + ] + } +}); + +interface RecentEntry { + uri: URI; + type: 'workspace' | 'folder' | 'file'; + label?: string; + remoteAuthority?: string; +} + +CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async function (accessor: ServicesAccessor, recentEntry: RecentEntry) { + const workspacesService = accessor.get(IWorkspacesService); + let recent: IRecent | undefined = undefined; + const uri = recentEntry.uri; + const label = recentEntry.label; + const remoteAuthority = recentEntry.remoteAuthority; + if (recentEntry.type === 'workspace') { + const workspace = await workspacesService.getWorkspaceIdentifier(uri); + recent = { workspace, label, remoteAuthority }; + } else if (recentEntry.type === 'folder') { + recent = { folderUri: uri, label, remoteAuthority }; + } else { + recent = { fileUri: uri, label, remoteAuthority }; + } + return workspacesService.addRecentlyOpened([recent]); +}); + +CommandsRegistry.registerCommand('_workbench.getRecentlyOpened', async function (accessor: ServicesAccessor) { + const workspacesService = accessor.get(IWorkspacesService); + return workspacesService.getRecentlyOpened(); +}); diff --git a/lib/vscode/src/vs/server/browser/client.ts b/lib/vscode/src/vs/workbench/browser/client.ts similarity index 79% rename from lib/vscode/src/vs/server/browser/client.ts rename to lib/vscode/src/vs/workbench/browser/client.ts index c1c5b4507de3..855fbc9064df 100644 --- a/lib/vscode/src/vs/server/browser/client.ts +++ b/lib/vscode/src/vs/workbench/browser/client.ts @@ -1,22 +1,16 @@ import * as path from 'vs/base/common/path'; -import { Options } from 'vs/ipc'; +import { Options } from 'vs/base/common/ipc'; import { localize } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { TelemetryChannelClient } from 'vs/server/common/telemetry'; -import { getOptions } from 'vs/server/common/util'; -import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; +import { getOptions } from 'vs/base/common/util'; +import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; // eslint-disable-line code-import-patterns import 'vs/workbench/services/localizations/browser/localizationsService'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; /** * All client-side customization to VS Code should live in this file when @@ -25,32 +19,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA const options = getOptions(); -class TelemetryService extends TelemetryChannelClient { - public constructor( - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - ) { - super(remoteAgentService.getConnection()!.getChannel('telemetry')); - } -} - -const TELEMETRY_SECTION_ID = 'telemetry'; -Registry.as(Extensions.Configuration).registerConfiguration({ - 'id': TELEMETRY_SECTION_ID, - 'order': 110, - 'type': 'object', - 'title': localize('telemetryConfigurationTitle', 'Telemetry'), - 'properties': { - 'telemetry.enableTelemetry': { - 'type': 'boolean', - 'description': localize('telemetry.enableTelemetry', 'Enable usage data and errors to be sent to a Microsoft online service.'), - 'default': !options.disableTelemetry, - 'tags': ['usesOnlineServices'] - } - } -}); - -registerSingleton(ITelemetryService, TelemetryService); - /** * This is called by vs/workbench/browser/web.main.ts after the workbench has * been initialized so we can initialize our own client-side code. diff --git a/lib/vscode/src/vs/workbench/browser/codeeditor.ts b/lib/vscode/src/vs/workbench/browser/codeeditor.ts index c25acef38ad5..3d381ca8089f 100644 --- a/lib/vscode/src/vs/workbench/browser/codeeditor.ts +++ b/lib/vscode/src/vs/workbench/browser/codeeditor.ts @@ -107,12 +107,14 @@ export class RangeHighlightDecorations extends Disposable { } private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + description: 'codeeditor-range-highlight-whole', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', isWholeLine: true }); private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + description: 'codeeditor-range-highlight', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight' }); diff --git a/lib/vscode/src/vs/workbench/browser/composite.ts b/lib/vscode/src/vs/workbench/browser/composite.ts index 59cec8515e10..2239c0b97931 100644 --- a/lib/vscode/src/vs/workbench/browser/composite.ts +++ b/lib/vscode/src/vs/workbench/browser/composite.ts @@ -118,10 +118,6 @@ export abstract class Composite extends Component implements IComposite { this.parent = parent; } - override updateStyles(): void { - super.updateStyles(); - } - /** * Returns the container this composite is being build in. */ @@ -158,6 +154,13 @@ export abstract class Composite extends Component implements IComposite { */ abstract layout(dimension: Dimension): void; + /** + * Update the styles of the contents of this composite. + */ + override updateStyles(): void { + super.updateStyles(); + } + /** * Returns an array of actions to show in the action bar of the composite. */ diff --git a/lib/vscode/src/vs/workbench/browser/contextkeys.ts b/lib/vscode/src/vs/workbench/browser/contextkeys.ts index ebf069f22981..baeaddc1e443 100644 --- a/lib/vscode/src/vs/workbench/browser/contextkeys.ts +++ b/lib/vscode/src/vs/workbench/browser/contextkeys.ts @@ -7,8 +7,8 @@ import { localize } from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; -import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext } from 'vs/workbench/common/editor'; +import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType, WebFileSystemAccess } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -22,6 +22,7 @@ import { PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from import { getRemoteName, getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; +import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; export const WorkbenchStateContext = new RawContextKey('workbenchState', undefined, { type: 'string', description: localize('workbenchState', "The kind of workspace opened in the window, either 'empty' (no workspace), 'folder' (single folder) or 'workspace' (multi-root workspace)") }); export const WorkspaceFolderCountContext = new RawContextKey('workspaceFolderCount', 0, localize('workspaceFolderCount', "The number of root folders in the workspace")); @@ -77,6 +78,7 @@ export class WorkbenchContextKeysHandler extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IEditorService private readonly editorService: IEditorService, + @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IViewletService private readonly viewletService: IViewletService, @@ -91,6 +93,7 @@ export class WorkbenchContextKeysHandler extends Disposable { IsWebContext.bindTo(this.contextKeyService); IsMacNativeContext.bindTo(this.contextKeyService); + IsIOSContext.bindTo(this.contextKeyService); RemoteNameContext.bindTo(this.contextKeyService).set(getRemoteName(this.environmentService.remoteAuthority) || ''); @@ -239,11 +242,11 @@ export class WorkbenchContextKeysHandler extends Disposable { if (activeEditorPane) { this.activeEditorContext.set(activeEditorPane.getId()); - this.activeEditorIsReadonly.set(activeEditorPane.input.isReadonly()); + this.activeEditorIsReadonly.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly)); const activeEditorResource = activeEditorPane.input.resource; - const editors = activeEditorResource ? this.editorService.getEditorOverrides(activeEditorResource, undefined, activeGroup) : []; - this.activeEditorAvailableEditorIds.set(editors.map(([_, entry]) => entry.id).join(',')); + const editors = activeEditorResource ? this.editorOverrideService.getEditorIds(activeEditorResource) : []; + this.activeEditorAvailableEditorIds.set(editors.join(',')); } else { this.activeEditorContext.reset(); this.activeEditorIsReadonly.reset(); diff --git a/lib/vscode/src/vs/workbench/browser/dnd.ts b/lib/vscode/src/vs/workbench/browser/dnd.ts index 8a188db95668..4d2d97b6a738 100644 --- a/lib/vscode/src/vs/workbench/browser/dnd.ts +++ b/lib/vscode/src/vs/workbench/browser/dnd.ts @@ -3,73 +3,57 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Registry } from 'vs/platform/registry/common/platform'; -import { hasWorkspaceFileExtension, IWorkspaceFolderCreationData, IRecentFile, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { normalize } from 'vs/base/common/path'; +import { hasWorkspaceFileExtension, IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { basename, isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; import { MIME_BINARY } from 'vs/base/common/mime'; import { isWindows } from 'vs/base/common/platform'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorIdentifier, GroupIdentifier, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; -import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorIdentifier, GroupIdentifier, isEditorIdentifier } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Emitter } from 'vs/base/common/event'; -import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { coalesce } from 'vs/base/common/arrays'; +import { parse, stringify } from 'vs/base/common/marshalling'; +import { ILabelService } from 'vs/platform/label/common/label'; -export interface IDraggedResource { - resource: URI; - isExternal: boolean; -} - -interface ISerializedDraggedResource { - resource: string; -} +//#region Editor / Resources DND export class DraggedEditorIdentifier { - constructor(public readonly identifier: IEditorIdentifier) { } + constructor(readonly identifier: IEditorIdentifier) { } } export class DraggedEditorGroupIdentifier { - constructor(public readonly identifier: GroupIdentifier) { } + constructor(readonly identifier: GroupIdentifier) { } } -interface IDraggedEditorProps { - dirtyContent?: string; - encoding?: string; - mode?: string; - options?: ITextEditorOptions; -} - -export interface IDraggedEditor extends IDraggedResource, IDraggedEditorProps { } - -export interface ISerializedDraggedEditor extends ISerializedDraggedResource, IDraggedEditorProps { } - export const CodeDataTransfers = { EDITORS: 'CodeEditors', FILES: 'CodeFiles' }; -export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - const resources: Array = []; +export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { + resource?: URI; + isExternal?: boolean; +} + +export function extractEditorsDropData(e: DragEvent, externalOnly?: boolean): Array { + const editors: IDraggedResourceEditorInput[] = []; if (e.dataTransfer && e.dataTransfer.types.length > 0) { // Check for window-to-window DND @@ -79,17 +63,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - resources.push({ - resource: URI.parse(draggedEditor.resource), - dirtyContent: draggedEditor.dirtyContent, - options: draggedEditor.options, - encoding: draggedEditor.encoding, - mode: draggedEditor.mode, - isExternal: false - }); - }); + editors.push(...parse(rawEditorsData)); } catch (error) { // Invalid transfer } @@ -100,8 +74,8 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array ({ resource: URI.parse(uriStr), isExternal: false }))); + const resourcesRaw: string[] = JSON.parse(rawResourcesData); + editors.push(...resourcesRaw.map(resourceRaw => ({ resource: URI.parse(resourceRaw) }))); } } catch (error) { // Invalid transfer @@ -110,12 +84,12 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array resource.resource.fsPath === file.path) /* prevent duplicates */) { + if (file?.path /* Electron only */) { try { - resources.push({ resource: URI.file(file.path), isExternal: true }); + editors.push({ resource: URI.file(file.path), isExternal: true }); } catch (error) { // Invalid URI } @@ -128,18 +102,16 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - if (!resources.some(resource => resource.resource.fsPath === codeFile) /* prevent duplicates */) { - resources.push({ resource: URI.file(codeFile), isExternal: true }); - } - }); + for (const codeFile of codeFiles) { + editors.push({ resource: URI.file(codeFile), isExternal: true }); + } } catch (error) { // Invalid transfer } } } - return resources; + return editors; } export interface IResourcesDropHandlerOptions { @@ -148,21 +120,20 @@ export interface IResourcesDropHandlerOptions { * Whether to open the actual workspace when a workspace configuration file is dropped * or whether to open the configuration file within the editor as normal file. */ - allowWorkspaceOpen: boolean; + readonly allowWorkspaceOpen: boolean; } /** - * Shared function across some components to handle drag & drop of resources. E.g. of folders and workspace files - * to open them in the window instead of the editor or to handle dirty editors being dropped between instances of Code. + * Shared function across some components to handle drag & drop of resources. + * E.g. of folders and workspace files to open them in the window instead of + * the editor or to handle dirty editors being dropped between instances of Code. */ export class ResourcesDropHandler { constructor( - private options: IResourcesDropHandlerOptions, + private readonly options: IResourcesDropHandlerOptions, @IFileService private readonly fileService: IFileService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @ITextFileService private readonly textFileService: ITextFileService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @IEditorService private readonly editorService: IEditorService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IHostService private readonly hostService: IHostService @@ -170,110 +141,61 @@ export class ResourcesDropHandler { } async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise { - const untitledOrFileResources = extractResources(event).filter(resource => this.fileService.canHandleResource(resource.resource) || resource.resource.scheme === Schemas.untitled); - if (!untitledOrFileResources.length) { + const editors = extractEditorsDropData(event); + if (!editors.length) { return; } // Make the window active to handle the drop properly within await this.hostService.focus(); - // Check for special things being dropped - const isWorkspaceOpening = await this.doHandleDrop(untitledOrFileResources); - if (isWorkspaceOpening) { - return; // return early if the drop operation resulted in this window changing to a workspace + // Check for workspace file being dropped if we are allowed to do so + const externalLocalFiles = coalesce(editors.filter(editor => editor.isExternal && editor.resource?.scheme === Schemas.file).map(editor => editor.resource)); + if (this.options.allowWorkspaceOpen) { + if (externalLocalFiles.length > 0) { + const isWorkspaceOpening = await this.handleWorkspaceFileDrop(externalLocalFiles); + if (isWorkspaceOpening) { + return; // return early if the drop operation resulted in this window changing to a workspace + } + } } // Add external ones to recently open list unless dropped resource is a workspace - const recentFiles: IRecentFile[] = untitledOrFileResources.filter(untitledOrFileResource => untitledOrFileResource.isExternal && untitledOrFileResource.resource.scheme === Schemas.file).map(d => ({ fileUri: d.resource })); - if (recentFiles.length) { - this.workspacesService.addRecentlyOpened(recentFiles); + if (externalLocalFiles.length) { + this.workspacesService.addRecentlyOpened(externalLocalFiles.map(resource => ({ fileUri: resource }))); } - const editors: IResourceEditorInputType[] = untitledOrFileResources.map(untitledOrFileResource => ({ - resource: untitledOrFileResource.resource, - encoding: (untitledOrFileResource as IDraggedEditor).encoding, - mode: (untitledOrFileResource as IDraggedEditor).mode, + // Open in Editor + const targetGroup = resolveTargetGroup(); + await this.editorService.openEditors(editors.map(editor => ({ + ...editor, options: { - ...(untitledOrFileResource as IDraggedEditor).options, + ...editor.options, pinned: true, index: targetIndex } - })); - - // Open in Editor - const targetGroup = resolveTargetGroup(); - await this.editorService.openEditors(editors, targetGroup); + })), targetGroup, { validateTrust: true }); // Finish with provided function afterDrop(targetGroup); } - private async doHandleDrop(untitledOrFileResources: Array): Promise { - - // Check for dirty editors being dropped - const dirtyEditors: IDraggedEditor[] = untitledOrFileResources.filter(untitledOrFileResource => !untitledOrFileResource.isExternal && typeof (untitledOrFileResource as IDraggedEditor).dirtyContent === 'string'); - if (dirtyEditors.length > 0) { - await Promise.all(dirtyEditors.map(dirtyEditor => this.handleDirtyEditorDrop(dirtyEditor))); - return false; - } - - // Check for workspace file being dropped if we are allowed to do so - if (this.options.allowWorkspaceOpen) { - const externalFileOnDiskResources = untitledOrFileResources.filter(untitledOrFileResource => untitledOrFileResource.isExternal && untitledOrFileResource.resource.scheme === Schemas.file).map(d => d.resource); - if (externalFileOnDiskResources.length > 0) { - return this.handleWorkspaceFileDrop(externalFileOnDiskResources); - } - } - - return false; - } - - private async handleDirtyEditorDrop(droppedDirtyEditor: IDraggedEditor): Promise { - const fileEditorFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); - - // Untitled: always ensure that we open a new untitled editor for each file we drop - if (droppedDirtyEditor.resource.scheme === Schemas.untitled) { - const untitledEditorResource = this.editorService.createEditorInput({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding, forceUntitled: true }).resource; - if (untitledEditorResource) { - droppedDirtyEditor.resource = untitledEditorResource; - } - } - - // File: ensure the file is not dirty or opened already - else if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpened({ resource: droppedDirtyEditor.resource, typeId: fileEditorFactory.typeId })) { - return false; - } - - // If the dropped editor is dirty with content we simply take that - // content and turn it into a backup so that it loads the contents - if (typeof droppedDirtyEditor.dirtyContent === 'string') { - try { - await this.workingCopyBackupService.backup({ resource: droppedDirtyEditor.resource, typeId: NO_TYPE_ID }, bufferToReadable(VSBuffer.fromString(droppedDirtyEditor.dirtyContent))); - } catch (e) { - // Ignore error - } - } - - return false; - } - - private async handleWorkspaceFileDrop(fileOnDiskResources: URI[]): Promise { + private async handleWorkspaceFileDrop(resources: URI[]): Promise { const toOpen: IWindowOpenable[] = []; const folderURIs: IWorkspaceFolderCreationData[] = []; - await Promise.all(fileOnDiskResources.map(async fileOnDiskResource => { + await Promise.all(resources.map(async resource => { // Check for Workspace - if (hasWorkspaceFileExtension(fileOnDiskResource)) { - toOpen.push({ workspaceUri: fileOnDiskResource }); + if (hasWorkspaceFileExtension(resource)) { + toOpen.push({ workspaceUri: resource }); return; } // Check for Folder try { - const stat = await this.fileService.resolve(fileOnDiskResource); + const stat = await this.fileService.resolve(resource); if (stat.isDirectory) { toOpen.push({ folderUri: stat.resource }); folderURIs.push({ uri: stat.resource }); @@ -305,92 +227,141 @@ export class ResourcesDropHandler { } } -export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: (URI | { resource: URI, isDirectory: boolean })[], optionsCallback: ((resource: URI) => ITextEditorOptions) | undefined, event: DragMouseEvent | DragEvent): void { - if (resources.length === 0 || !event.dataTransfer) { +interface IResourceStat { + resource: URI; + isDirectory?: boolean; +} + +export function fillEditorsDragData(accessor: ServicesAccessor, resources: URI[], event: DragMouseEvent | DragEvent): void; +export function fillEditorsDragData(accessor: ServicesAccessor, resources: IResourceStat[], event: DragMouseEvent | DragEvent): void; +export function fillEditorsDragData(accessor: ServicesAccessor, editors: IEditorIdentifier[], event: DragMouseEvent | DragEvent): void; +export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEditors: Array, event: DragMouseEvent | DragEvent): void { + if (resourcesOrEditors.length === 0 || !event.dataTransfer) { return; } - const sources = resources.map(obj => { - if (URI.isUri(obj)) { - return { resource: obj, isDirectory: false /* assume resource is not a directory */ }; + const textFileService = accessor.get(ITextFileService); + const editorService = accessor.get(IEditorService); + const fileService = accessor.get(IFileService); + const labelService = accessor.get(ILabelService); + + // Extract resources from URIs or Editors that + // can be handled by the file service + const fileSystemResources = coalesce(resourcesOrEditors.map(resourceOrEditor => { + if (URI.isUri(resourceOrEditor)) { + return { resource: resourceOrEditor }; + } + + if (isEditorIdentifier(resourceOrEditor)) { + if (URI.isUri(resourceOrEditor.editor.resource)) { + return { resource: resourceOrEditor.editor.resource }; + } + + return undefined; // editor without resource } - return obj; - }); + return resourceOrEditor; + })).filter(({ resource }) => fileService.canHandleResource(resource)); // Text: allows to paste into text-capable areas const lineDelimiter = isWindows ? '\r\n' : '\n'; - event.dataTransfer.setData(DataTransfers.TEXT, sources.map(source => source.resource.scheme === Schemas.file ? normalize(normalizeDriveLetter(source.resource.fsPath)) : source.resource.toString()).join(lineDelimiter)); + event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter)); // Download URL: enables support to drag a tab as file to desktop (only single file supported) - if (!sources[0].isDirectory) { - event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [MIME_BINARY, basename(sources[0].resource), FileAccess.asBrowserUri(sources[0].resource).toString()].join(':')); + const firstFile = fileSystemResources.find(({ isDirectory }) => !isDirectory); + if (firstFile) { + // TODO@sandbox this will no longer work when `vscode-file` + // is enabled because we block loading resources that are not + // inside installation dir + event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [MIME_BINARY, basename(firstFile.resource), FileAccess.asBrowserUri(firstFile.resource).toString()].join(':')); } - // Resource URLs: allows to drop multiple resources to a target in VS Code (not directories) - const files = sources.filter(source => !source.isDirectory); + // Resource URLs: allows to drop multiple file resources to a target in VS Code + const files = fileSystemResources.filter(({ isDirectory }) => !isDirectory); if (files.length) { - event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(file => file.resource.toString()))); + event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(({ resource }) => resource.toString()))); } - // Editors: enables cross window DND of tabs into the editor area - const textFileService = accessor.get(ITextFileService); - const editorService = accessor.get(IEditorService); + // Editors: enables cross window DND of editors + // into the editor area while presering UI state + const draggedEditors: IDraggedResourceEditorInput[] = []; - const draggedEditors: ISerializedDraggedEditor[] = []; - files.forEach(file => { - let options: ITextEditorOptions | undefined = undefined; + for (const resourceOrEditor of resourcesOrEditors) { - // Use provided callback for editor options - if (typeof optionsCallback === 'function') { - options = optionsCallback(file.resource); + // Extract resource editor from provided object or URI + let editor: IDraggedResourceEditorInput | undefined = undefined; + if (isEditorIdentifier(resourceOrEditor)) { + editor = resourceOrEditor.editor.asResourceEditorInput(resourceOrEditor.groupId); + } else if (URI.isUri(resourceOrEditor)) { + editor = { resource: resourceOrEditor }; + } else if (!resourceOrEditor.isDirectory) { + editor = { resource: resourceOrEditor.resource }; } - // Otherwise try to figure out the view state from opened editors that match - else { - options = { - viewState: (() => { - const textEditorControls = editorService.visibleTextEditorControls; - for (const textEditorControl of textEditorControls) { - if (isCodeEditor(textEditorControl)) { - const model = textEditorControl.getModel(); - if (isEqual(model?.uri, file.resource)) { - return withNullAsUndefined(textEditorControl.saveViewState()); - } - } + if (!editor) { + continue; // skip over editors that cannot be transferred via dnd + } + + // Fill in some properties if they are not there already by accessing + // some well known things from the text file universe. + // This is not ideal for custom editors, but those have a chance to + // provide everything from the `asResourceEditorInput` method. + { + const resource = editor.resource; + if (resource) { + const textFileModel = textFileService.files.get(resource); + if (textFileModel) { + + // mode + if (typeof editor.mode !== 'string') { + editor.mode = textFileModel.getMode(); } - return undefined; - })() - }; - } + // encoding + if (typeof editor.encoding !== 'string') { + editor.encoding = textFileModel.getEncoding(); + } - // Try to find encoding and mode from text model - let encoding: string | undefined = undefined; - let mode: string | undefined = undefined; + // contents (only if dirty) + if (typeof editor.contents !== 'string' && textFileModel.isDirty()) { + editor.contents = textFileModel.textEditorModel.getValue(); + } + } - const model = file.resource.scheme === Schemas.untitled ? textFileService.untitled.get(file.resource) : textFileService.files.get(file.resource); - if (model) { - encoding = model.getEncoding(); - mode = model.getMode(); - } + // viewState + if (!editor.options?.viewState) { + editor.options = { + ...editor.options, + viewState: (() => { + for (const textEditorControl of editorService.visibleTextEditorControls) { + if (isCodeEditor(textEditorControl)) { + const model = textEditorControl.getModel(); + if (isEqual(model?.uri, resource)) { + return withNullAsUndefined(textEditorControl.saveViewState()); + } + } + } - // If the resource is dirty or untitled, send over its content - // to restore dirty state. Get that from the text model directly - let dirtyContent: string | undefined = undefined; - if (model?.isDirty()) { - dirtyContent = model.textEditorModel.getValue(); + return undefined; + })() + }; + } + } } // Add as dragged editor - draggedEditors.push({ resource: file.resource.toString(), dirtyContent, options, encoding, mode }); - }); + draggedEditors.push(editor); + } if (draggedEditors.length) { - event.dataTransfer.setData(CodeDataTransfers.EDITORS, JSON.stringify(draggedEditors)); + event.dataTransfer.setData(CodeDataTransfers.EDITORS, stringify(draggedEditors)); } } +//#endregion + +//#region DND Utilities + /** * A singleton to store transfer data during drag & drop operations that are only valid within the application. */ @@ -437,12 +408,12 @@ export class LocalSelectionTransfer { } export interface IDragAndDropObserverCallbacks { - onDragEnter: (e: DragEvent) => void; - onDragLeave: (e: DragEvent) => void; - onDrop: (e: DragEvent) => void; - onDragEnd: (e: DragEvent) => void; + readonly onDragEnter: (e: DragEvent) => void; + readonly onDragLeave: (e: DragEvent) => void; + readonly onDrop: (e: DragEvent) => void; + readonly onDragEnd: (e: DragEvent) => void; - onDragOver?: (e: DragEvent) => void; + readonly onDragOver?: (e: DragEvent) => void; } export class DragAndDropObserver extends Disposable { @@ -453,7 +424,7 @@ export class DragAndDropObserver extends Disposable { // repeadedly. private counter: number = 0; - constructor(private element: HTMLElement, private callbacks: IDragAndDropObserverCallbacks) { + constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { super(); this.registerListeners(); @@ -514,7 +485,14 @@ export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]) return false; } -export type Before2D = { verticallyBefore: boolean; horizontallyBefore: boolean; }; +//#endregion + +//#region Composites DND + +export type Before2D = { + readonly verticallyBefore: boolean; + readonly horizontallyBefore: boolean; +}; export interface ICompositeDragAndDrop { drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent, before?: Before2D): void; @@ -532,10 +510,13 @@ export interface ICompositeDragAndDropObserverCallbacks { } export class CompositeDragAndDropData implements IDragAndDropData { + constructor(private type: 'view' | 'composite', private id: string) { } + update(dataTransfer: DataTransfer): void { // no-op } + getData(): { type: 'view' | 'composite'; id: string; @@ -545,44 +526,51 @@ export class CompositeDragAndDropData implements IDragAndDropData { } export interface IDraggedCompositeData { - eventData: DragEvent; - dragAndDropData: CompositeDragAndDropData; + readonly eventData: DragEvent; + readonly dragAndDropData: CompositeDragAndDropData; } export class DraggedCompositeIdentifier { - constructor(private _compositeId: string) { } + + constructor(private compositeId: string) { } get id(): string { - return this._compositeId; + return this.compositeId; } } export class DraggedViewIdentifier { - constructor(private _viewId: string) { } + + constructor(private viewId: string) { } get id(): string { - return this._viewId; + return this.viewId; } } export type ViewType = 'composite' | 'view'; export class CompositeDragAndDropObserver extends Disposable { - private transferData: LocalSelectionTransfer; - private _onDragStart = this._register(new Emitter()); - private _onDragEnd = this._register(new Emitter()); - private static _instance: CompositeDragAndDropObserver | undefined; + + private static instance: CompositeDragAndDropObserver | undefined; + static get INSTANCE(): CompositeDragAndDropObserver { - if (!CompositeDragAndDropObserver._instance) { - CompositeDragAndDropObserver._instance = new CompositeDragAndDropObserver(); + if (!CompositeDragAndDropObserver.instance) { + CompositeDragAndDropObserver.instance = new CompositeDragAndDropObserver(); } - return CompositeDragAndDropObserver._instance; + + return CompositeDragAndDropObserver.instance; } + + private readonly transferData = LocalSelectionTransfer.getInstance(); + + private readonly onDragStart = this._register(new Emitter()); + private readonly onDragEnd = this._register(new Emitter()); + private constructor() { super(); - this.transferData = LocalSelectionTransfer.getInstance(); - this._register(this._onDragEnd.event(e => { + this._register(this.onDragEnd.event(e => { const id = e.dragAndDropData.getData().id; const type = e.dragAndDropData.getData().type; const data = this.readDragData(type); @@ -591,6 +579,7 @@ export class CompositeDragAndDropObserver extends Disposable { } })); } + private readDragData(type: ViewType): CompositeDragAndDropData | undefined { if (this.transferData.hasData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype)) { const data = this.transferData.getData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); @@ -598,11 +587,14 @@ export class CompositeDragAndDropObserver extends Disposable { return new CompositeDragAndDropData(type, data[0].id); } } + return undefined; } + private writeDragData(id: string, type: ViewType): void { this.transferData.setData([type === 'view' ? new DraggedViewIdentifier(id) : new DraggedCompositeIdentifier(id)], type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); } + registerTarget(element: HTMLElement, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { const disposableStore = new DisposableStore(); disposableStore.add(new DragAndDropObserver(element, { @@ -611,6 +603,7 @@ export class CompositeDragAndDropObserver extends Disposable { }, onDragEnter: e => { e.preventDefault(); + if (callbacks.onDragEnter) { const data = this.readDragData('composite') || this.readDragData('view'); if (data) { @@ -634,11 +627,12 @@ export class CompositeDragAndDropObserver extends Disposable { callbacks.onDrop({ eventData: e, dragAndDropData: data! }); // Fire drag event in case drop handler destroys the dragged element - this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + this.onDragEnd.fire({ eventData: e, dragAndDropData: data! }); } }, onDragOver: e => { e.preventDefault(); + if (callbacks.onDragOver) { const data = this.readDragData('composite') || this.readDragData('view'); if (!data) { @@ -649,45 +643,47 @@ export class CompositeDragAndDropObserver extends Disposable { } } })); + if (callbacks.onDragStart) { - this._onDragStart.event(e => { + this.onDragStart.event(e => { callbacks.onDragStart!(e); }, this, disposableStore); } + if (callbacks.onDragEnd) { - this._onDragEnd.event(e => { + this.onDragEnd.event(e => { callbacks.onDragEnd!(e); }); } + return this._register(disposableStore); } registerDraggable(element: HTMLElement, draggedItemProvider: () => { type: ViewType, id: string }, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { element.draggable = true; + const disposableStore = new DisposableStore(); + disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => { const { id, type } = draggedItemProvider(); this.writeDragData(id, type); - if (e.dataTransfer) { - e.dataTransfer.setDragImage(element, 0, 0); - } + e.dataTransfer?.setDragImage(element, 0, 0); - this._onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); + this.onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); })); + disposableStore.add(new DragAndDropObserver(element, { onDragEnd: e => { const { type } = draggedItemProvider(); const data = this.readDragData(type); - if (!data) { return; } - this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + this.onDragEnd.fire({ eventData: e, dragAndDropData: data! }); }, onDragEnter: e => { - if (callbacks.onDragEnter) { const data = this.readDragData('composite') || this.readDragData('view'); if (!data) { @@ -712,14 +708,14 @@ export class CompositeDragAndDropObserver extends Disposable { onDrop: e => { if (callbacks.onDrop) { const data = this.readDragData('composite') || this.readDragData('view'); - if (!data) { return; } + callbacks.onDrop({ eventData: e, dragAndDropData: data! }); // Fire drag event in case drop handler destroys the dragged element - this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + this.onDragEnd.fire({ eventData: e, dragAndDropData: data! }); } }, onDragOver: e => { @@ -733,16 +729,19 @@ export class CompositeDragAndDropObserver extends Disposable { } } })); + if (callbacks.onDragStart) { - this._onDragStart.event(e => { + this.onDragStart.event(e => { callbacks.onDragStart!(e); }, this, disposableStore); } + if (callbacks.onDragEnd) { - this._onDragEnd.event(e => { + this.onDragEnd.event(e => { callbacks.onDragEnd!(e); }, this, disposableStore); } + return this._register(disposableStore); } } @@ -754,3 +753,5 @@ export function toggleDropEffect(dataTransfer: DataTransfer | null, dropEffect: dataTransfer.dropEffect = shouldHaveIt ? dropEffect : 'none'; } + +//#endregion diff --git a/lib/vscode/src/vs/workbench/browser/editor.ts b/lib/vscode/src/vs/workbench/browser/editor.ts index adce1b321d3b..8fae366d3e42 100644 --- a/lib/vscode/src/vs/workbench/browser/editor.ts +++ b/lib/vscode/src/vs/workbench/browser/editor.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorInput, EditorResourceAccessor, IEditorInput, EditorExtensions, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IEditorInput, EditorExtensions, SideBySideEditor, IEditorDescriptor as ICommonEditorDescriptor } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -20,22 +21,7 @@ import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsSe //#region Editors Registry -export interface IEditorDescriptor { - - /** - * The unique identifier of the editor - */ - getId(): string; - - /** - * The display name of the editor - */ - getName(): string; - - instantiate(instantiationService: IInstantiationService): EditorPane; - - describes(obj: unknown): boolean; -} +export interface IEditorDescriptor extends ICommonEditorDescriptor { } export interface IEditorRegistry { @@ -48,22 +34,12 @@ export interface IEditorRegistry { * @param inputDescriptors A set of constructor functions that return an instance of EditorInput for which the * registered editor should be used for. */ - registerEditor(descriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable; + registerEditor(editorDescriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable; /** * Returns the editor descriptor for the given input or `undefined` if none. */ getEditor(input: EditorInput): IEditorDescriptor | undefined; - - /** - * Returns the editor descriptor for the given identifier or `undefined` if none. - */ - getEditorById(editorId: string): IEditorDescriptor | undefined; - - /** - * Returns an array of registered editors known to the platform. - */ - getEditors(): readonly IEditorDescriptor[]; } /** @@ -74,36 +50,28 @@ export class EditorDescriptor implements IEditorDescriptor { static create( ctor: { new(...services: Services): EditorPane }, - id: string, + typeId: string, name: string ): EditorDescriptor { - return new EditorDescriptor(ctor as IConstructorSignature0, id, name); + return new EditorDescriptor(ctor as IConstructorSignature0, typeId, name); } - constructor( + private constructor( private readonly ctor: IConstructorSignature0, - private readonly id: string, - private readonly name: string + readonly typeId: string, + readonly name: string ) { } instantiate(instantiationService: IInstantiationService): EditorPane { return instantiationService.createInstance(this.ctor); } - getId(): string { - return this.id; - } - - getName(): string { - return this.name; - } - - describes(obj: unknown): boolean { - return obj instanceof EditorPane && obj.getId() === this.id; + describes(editorPane: EditorPane): boolean { + return editorPane.getId() === this.typeId; } } -class EditorRegistry implements IEditorRegistry { +export class EditorRegistry implements IEditorRegistry { private readonly editors: EditorDescriptor[] = []; private readonly mapEditorToInputs = new Map[]>(); @@ -120,58 +88,53 @@ class EditorRegistry implements IEditorRegistry { } getEditor(input: EditorInput): EditorDescriptor | undefined { - const findEditorDescriptors = (input: EditorInput, byInstanceOf?: boolean): EditorDescriptor[] => { - const matchingDescriptors: EditorDescriptor[] = []; - - for (const editor of this.editors) { - const inputDescriptors = this.mapEditorToInputs.get(editor) || []; - for (const inputDescriptor of inputDescriptors) { - const inputClass = inputDescriptor.ctor; - - // Direct check on constructor type (ignores prototype chain) - if (!byInstanceOf && input.constructor === inputClass) { - matchingDescriptors.push(editor); - break; - } - - // Normal instanceof check - else if (byInstanceOf && input instanceof inputClass) { - matchingDescriptors.push(editor); - break; - } - } - } + const descriptors = this.findEditorDescriptors(input); - // If no descriptors found, continue search using instanceof and prototype chain - if (!byInstanceOf && matchingDescriptors.length === 0) { - return findEditorDescriptors(input, true); - } + if (descriptors.length === 0) { + return undefined; + } - if (byInstanceOf) { - return matchingDescriptors; - } + if (descriptors.length === 1) { + return descriptors[0]; + } + + return input.prefersEditor(descriptors); + } - return matchingDescriptors; - }; + private findEditorDescriptors(input: EditorInput, byInstanceOf?: boolean): EditorDescriptor[] { + const matchingDescriptors: EditorDescriptor[] = []; - const descriptors = findEditorDescriptors(input); - if (descriptors.length > 0) { + for (const editor of this.editors) { + const inputDescriptors = this.mapEditorToInputs.get(editor) || []; + for (const inputDescriptor of inputDescriptors) { + const inputClass = inputDescriptor.ctor; + + // Direct check on constructor type (ignores prototype chain) + if (!byInstanceOf && input.constructor === inputClass) { + matchingDescriptors.push(editor); + break; + } - // Ask the input for its preferred Editor - const preferredEditorId = input.getPreferredEditorId(descriptors.map(descriptor => descriptor.getId())); - if (preferredEditorId) { - return this.getEditorById(preferredEditorId); + // Normal instanceof check + else if (byInstanceOf && input instanceof inputClass) { + matchingDescriptors.push(editor); + break; + } } + } - // Otherwise, first come first serve - return descriptors[0]; + // If no descriptors found, continue search using instanceof and prototype chain + if (!byInstanceOf && matchingDescriptors.length === 0) { + return this.findEditorDescriptors(input, true); } - return undefined; + return matchingDescriptors; } - getEditorById(editorId: string): EditorDescriptor | undefined { - return this.editors.find(editor => editor.getId() === editorId); + //#region Used for tests only + + getEditorByType(typeId: string): EditorDescriptor | undefined { + return this.editors.find(editor => editor.typeId === typeId); } getEditors(): readonly EditorDescriptor[] { @@ -189,6 +152,8 @@ class EditorRegistry implements IEditorRegistry { return inputClasses; } + + //#endregion } Registry.add(EditorExtensions.Editors, new EditorRegistry()); @@ -255,7 +220,6 @@ export function whenEditorClosed(accessor: ServicesAccessor, resources: URI[]): //#endregion - //#region ARIA export function computeEditorAriaLabel(input: IEditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number): string { diff --git a/lib/vscode/src/vs/workbench/browser/layout.ts b/lib/vscode/src/vs/workbench/browser/layout.ts index 02ff8b3f52af..0670647b892f 100644 --- a/lib/vscode/src/vs/workbench/browser/layout.ts +++ b/lib/vscode/src/vs/workbench/browser/layout.ts @@ -9,8 +9,9 @@ import { EventType, addDisposableListener, getClientArea, Dimension, position, s import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Registry } from 'vs/platform/registry/common/platform'; -import { isWindows, isLinux, isMacintosh, isWeb, isNative } from 'vs/base/common/platform'; -import { pathsToEditors, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; +import { IResourceDiffEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; @@ -48,6 +49,7 @@ import { mark } from 'vs/base/common/performance'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { Promises } from 'vs/base/common/async'; +import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; export enum Settings { ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', @@ -150,6 +152,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private disposed: boolean | undefined; private titleBarPartView!: ISerializableView; + private bannerPartView!: ISerializableView; private activityBarPartView!: ISerializableView; private sideBarPartView!: ISerializableView; private panelPartView!: ISerializableView; @@ -263,6 +266,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.notificationService = accessor.get(INotificationService); this.activityBarService = accessor.get(IActivityBarService); this.statusBarService = accessor.get(IStatusbarService); + accessor.get(IBannerService); // Listeners this.registerLayoutListeners(); @@ -438,11 +442,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (position === Position.LEFT) { - this.workbenchGrid.moveViewTo(this.activityBarPartView, [1, 0]); - this.workbenchGrid.moveViewTo(this.sideBarPartView, [1, 1]); + this.workbenchGrid.moveViewTo(this.activityBarPartView, [2, 0]); + this.workbenchGrid.moveViewTo(this.sideBarPartView, [2, 1]); } else { - this.workbenchGrid.moveViewTo(this.sideBarPartView, [1, 4]); - this.workbenchGrid.moveViewTo(this.activityBarPartView, [1, 4]); + this.workbenchGrid.moveViewTo(this.sideBarPartView, [2, 4]); + this.workbenchGrid.moveViewTo(this.activityBarPartView, [2, 4]); } this.layout(); @@ -597,12 +601,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Files to diff is exclusive return pathsToEditors(initialFilesToOpen.filesToDiff, fileService).then(filesToDiff => { if (filesToDiff.length === 2) { - return [{ - leftResource: filesToDiff[0].resource, - rightResource: filesToDiff[1].resource, + const diffEditorInput: IResourceDiffEditorInput[] = [{ + originalInput: { resource: filesToDiff[0].resource }, + modifiedInput: { resource: filesToDiff[1].resource }, options: { pinned: true }, forceFile: true }]; + + return diffEditorInput; } // Otherwise: Open/Create files @@ -697,7 +703,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let openEditorsPromise: Promise | undefined = undefined; if (editors.length) { - openEditorsPromise = this.editorService.openEditors(editors); + openEditorsPromise = this.editorService.openEditors(editors, undefined, { validateTrust: true }); } // do not block the overall layout ready flow from potentially @@ -900,7 +906,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi case Parts.STATUSBAR_PART: this.statusBarService.focus(); default: - // Title Bar simply pass focus to container + // Title Bar & Banner simply pass focus to container const container = this.getContainer(part); if (container) { container.focus(); @@ -912,6 +918,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi switch (part) { case Parts.TITLEBAR_PART: return this.getPart(Parts.TITLEBAR_PART).getContainer(); + case Parts.BANNER_PART: + return this.getPart(Parts.BANNER_PART).getContainer(); case Parts.ACTIVITYBAR_PART: return this.getPart(Parts.ACTIVITYBAR_PART).getContainer(); case Parts.SIDEBAR_PART: @@ -1044,7 +1052,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi silentNotifications: boolean; } = this.configurationService.getValue('zenMode'); - toggleFullScreen = !this.state.fullscreen && config.fullScreen; + toggleFullScreen = !this.state.fullscreen && config.fullScreen && !isIOS; this.state.zenMode.transitionedToFullScreen = restoring ? config.fullScreen : toggleFullScreen; this.state.zenMode.transitionedToCenteredEditorLayout = !this.isEditorLayoutCentered() && config.centerLayout; @@ -1159,6 +1167,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi protected createWorkbenchLayout(): void { const titleBar = this.getPart(Parts.TITLEBAR_PART); + const bannerPart = this.getPart(Parts.BANNER_PART); const editorPart = this.getPart(Parts.EDITOR_PART); const activityBar = this.getPart(Parts.ACTIVITYBAR_PART); const panelPart = this.getPart(Parts.PANEL_PART); @@ -1167,6 +1176,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // View references for all parts this.titleBarPartView = titleBar; + this.bannerPartView = bannerPart; this.sideBarPartView = sideBar; this.activityBarPartView = activityBar; this.editorPartView = editorPart; @@ -1175,6 +1185,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const viewMap = { [Parts.ACTIVITYBAR_PART]: this.activityBarPartView, + [Parts.BANNER_PART]: this.bannerPartView, [Parts.TITLEBAR_PART]: this.titleBarPartView, [Parts.EDITOR_PART]: this.editorPartView, [Parts.PANEL_PART]: this.panelPartView, @@ -1223,6 +1234,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.storageService.store(Storage.PANEL_SIZE, panelSize, StorageScope.GLOBAL, StorageTarget.MACHINE); this.storageService.store(Storage.PANEL_DIMENSION, positionToString(this.state.panel.position), StorageScope.GLOBAL, StorageTarget.MACHINE); + // Remember last panel size for both dimensions + this.storageService.store(Storage.PANEL_LAST_NON_MAXIMIZED_HEIGHT, this.state.panel.lastNonMaximizedHeight, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(Storage.PANEL_LAST_NON_MAXIMIZED_WIDTH, this.state.panel.lastNonMaximizedWidth, StorageScope.GLOBAL, StorageTarget.MACHINE); + const gridSize = grid.getViewSize(); this.storageService.store(Storage.GRID_WIDTH, gridSize.width, StorageScope.GLOBAL, StorageTarget.MACHINE); this.storageService.store(Storage.GRID_HEIGHT, gridSize.height, StorageScope.GLOBAL, StorageTarget.MACHINE); @@ -1354,6 +1369,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.setViewVisible(this.activityBarPartView, !hidden); } + setBannerHidden(hidden: boolean): void { + this.workbenchGrid.setViewVisible(this.bannerPartView, !hidden); + } + setEditorHidden(hidden: boolean, skipLayout?: boolean): void { this.state.editor.hidden = hidden; @@ -1609,7 +1628,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let newVisibilityValue: string; if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'classic') { - newVisibilityValue = 'compact'; + newVisibilityValue = getTitleBarStyle(this.configurationService) === 'native' ? 'toggle' : 'compact'; } else { newVisibilityValue = 'classic'; } @@ -1738,6 +1757,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const panelSize = panelDimension === this.state.panel.position ? this.storageService.getNumber(Storage.PANEL_SIZE, StorageScope.GLOBAL, fallbackPanelSize) : fallbackPanelSize; const titleBarHeight = this.titleBarPartView.minimumHeight; + const bannerHeight = this.bannerPartView.minimumHeight; const statusBarHeight = this.statusBarPartView.minimumHeight; const activityBarWidth = this.activityBarPartView.minimumWidth; const middleSectionHeight = height - titleBarHeight - statusBarHeight; @@ -1790,6 +1810,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi size: titleBarHeight, visible: this.isVisible(Parts.TITLEBAR_PART) }, + { + type: 'leaf', + data: { type: Parts.BANNER_PART }, + size: bannerHeight, + visible: false + }, { type: 'branch', data: middleSection, diff --git a/lib/vscode/src/vs/workbench/browser/panel.ts b/lib/vscode/src/vs/workbench/browser/panel.ts index 1364f49a41e7..8f861e04dc74 100644 --- a/lib/vscode/src/vs/workbench/browser/panel.ts +++ b/lib/vscode/src/vs/workbench/browser/panel.ts @@ -10,7 +10,7 @@ import { IConstructorSignature0, BrandedService, IInstantiationService } from 'v import { assertIsDefined } from 'vs/base/common/types'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; import { IAction, Separator } from 'vs/base/common/actions'; -import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IStorageService } from 'vs/platform/storage/common/storage'; diff --git a/lib/vscode/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/lib/vscode/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 21c6e8d0c79f..fdfb3f17dac4 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -169,16 +169,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Menu const menuBarVisibility = getMenuBarVisibility(this.configurationService); if (menuBarVisibility === 'compact' || menuBarVisibility === 'hidden' || menuBarVisibility === 'toggle') { - topActions.push({ - id: 'toggleMenuVisibility', - label: localize('menu', "Menu"), - class: undefined, - tooltip: localize('menu', "Menu"), - checked: menuBarVisibility === 'compact', - enabled: true, - run: async () => this.configurationService.updateValue('window.menuBarVisibility', menuBarVisibility === 'compact' ? 'toggle' : 'compact'), - dispose: () => { } - }); + topActions.push(toAction({ id: 'toggleMenuVisibility', label: localize('menu', "Menu"), checked: menuBarVisibility === 'compact', run: () => this.configurationService.updateValue('window.menuBarVisibility', menuBarVisibility === 'compact' ? 'toggle' : 'compact') })); } if (topActions.length) { @@ -187,24 +178,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Accounts actions.push(new Separator()); - actions.push({ - id: 'toggleAccountsVisibility', - label: localize('accounts', "Accounts"), - class: undefined, - tooltip: localize('accounts', "Accounts"), - checked: this.accountsVisibilityPreference, - enabled: true, - run: async () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference, - dispose: () => { } - }); - + actions.push(toAction({ id: 'toggleAccountsVisibility', label: localize('accounts', "Accounts"), checked: this.accountsVisibilityPreference, run: () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference })); actions.push(new Separator()); // Toggle Sidebar - actions.push(this.instantiationService.createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService))); + actions.push(toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) })); // Toggle Activity Bar - actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: async () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); + actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); }, getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.viewDescriptorService.getDefaultViewContainer(this.location)!.id, diff --git a/lib/vscode/src/vs/workbench/browser/parts/banner/bannerPart.ts b/lib/vscode/src/vs/workbench/browser/parts/banner/bannerPart.ts new file mode 100644 index 000000000000..0a9e336bb293 --- /dev/null +++ b/lib/vscode/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/bannerpart'; +import { localize } from 'vs/nls'; +import { $, addDisposableListener, append, clearNode, EventType } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Codicon, registerCodicon } from 'vs/base/common/codicons'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Part } from 'vs/workbench/browser/part'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; +import { Action } from 'vs/base/common/actions'; +import { Link } from 'vs/platform/opener/browser/link'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Emitter } from 'vs/base/common/event'; +import { IBannerItem, IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { BANNER_BACKGROUND, BANNER_FOREGROUND, BANNER_ICON_FOREGROUND } from 'vs/workbench/common/theme'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + + +// Icons + +const bannerCloseIcon = registerCodicon('banner-close', Codicon.close); + + +// Theme support + +registerThemingParticipant((theme, collector) => { + const backgroundColor = theme.getColor(BANNER_BACKGROUND); + if (backgroundColor) { + collector.addRule(`.monaco-workbench .part.banner { background-color: ${backgroundColor}; }`); + } + + const foregroundColor = theme.getColor(BANNER_FOREGROUND); + if (foregroundColor) { + collector.addRule(` + .monaco-workbench .part.banner, + .monaco-workbench .part.banner .action-container .codicon, + .monaco-workbench .part.banner .message-actions-container .monaco-link + { color: ${foregroundColor}; } + `); + } + + const iconForegroundColor = theme.getColor(BANNER_ICON_FOREGROUND); + if (iconForegroundColor) { + collector.addRule(`.monaco-workbench .part.banner .icon-container .codicon { color: ${iconForegroundColor} }`); + } +}); + + +// Banner Part + +const CONTEXT_BANNER_FOCUSED = new RawContextKey('bannerFocused', false, localize('bannerFocused', "Whether the banner has keyboard focus")); + +export class BannerPart extends Part implements IBannerService { + declare readonly _serviceBrand: undefined; + + // #region IView + + readonly height: number = 26; + readonly minimumWidth: number = 0; + readonly maximumWidth: number = Number.POSITIVE_INFINITY; + + get minimumHeight(): number { + return this.visible ? this.height : 0; + } + + get maximumHeight(): number { + return this.visible ? this.height : 0; + } + + private _onDidChangeSize = new Emitter<{ width: number; height: number; } | undefined>(); + + override get onDidChange() { return this._onDidChangeSize.event; } + + //#endregion + + private item: IBannerItem | undefined; + private readonly markdownRenderer: MarkdownRenderer; + private visible = false; + + private actionBar: ActionBar | undefined; + private messageActionsContainer: HTMLElement | undefined; + private focusedActionIndex: number = -1; + + constructor( + @IThemeService themeService: IThemeService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IStorageService storageService: IStorageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService); + + this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); + } + + override createContentArea(parent: HTMLElement): HTMLElement { + this.element = parent; + this.element.tabIndex = 0; + + // Restore focused action if needed + this._register(addDisposableListener(this.element, EventType.FOCUS, () => { + if (this.focusedActionIndex !== -1) { + this.focusActionLink(); + } + })); + + // Track focus + const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + CONTEXT_BANNER_FOCUSED.bindTo(scopedContextKeyService).set(true); + + return this.element; + } + + private close(item: IBannerItem): void { + // Hide banner + this.setVisibility(false); + + // Remove from document + clearNode(this.element); + + // Remember choice + if (typeof item.onClose === 'function') { + item.onClose(); + } + + this.item = undefined; + } + + private focusActionLink(): void { + const length = this.item?.actions?.length ?? 0; + + if (this.focusedActionIndex < length) { + const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex]; + if (actionLink instanceof HTMLElement) { + this.actionBar?.setFocusable(false); + actionLink.focus(); + } + } else { + this.actionBar?.focus(0); + } + } + + private getAriaLabel(item: IBannerItem): string | undefined { + if (item.ariaLabel) { + return item.ariaLabel; + } + if (typeof item.message === 'string') { + return item.message; + } + + return undefined; + } + + private getBannerMessage(message: MarkdownString | string): HTMLElement { + if (typeof message === 'string') { + const element = $('span'); + element.innerText = message; + return element; + } + + return this.markdownRenderer.render(message).element; + } + + private setVisibility(visible: boolean): void { + if (visible !== this.visible) { + this.visible = visible; + this.focusedActionIndex = -1; + + this.layoutService.setBannerHidden(!visible); + this._onDidChangeSize.fire(undefined); + } + } + + focus(): void { + this.focusedActionIndex = -1; + this.element.focus(); + } + + focusNextAction(): void { + const length = this.item?.actions?.length ?? 0; + this.focusedActionIndex = this.focusedActionIndex < length ? this.focusedActionIndex + 1 : 0; + + this.focusActionLink(); + } + + focusPreviousAction(): void { + const length = this.item?.actions?.length ?? 0; + this.focusedActionIndex = this.focusedActionIndex > 0 ? this.focusedActionIndex - 1 : length; + + this.focusActionLink(); + } + + hide(id: string): void { + if (this.item?.id !== id) { + return; + } + + this.setVisibility(false); + } + + show(item: IBannerItem): void { + if (item.id === this.item?.id) { + this.setVisibility(true); + return; + } + + // Clear previous item + clearNode(this.element); + + // Banner aria label + const ariaLabel = this.getAriaLabel(item); + if (ariaLabel) { + this.element.setAttribute('aria-label', ariaLabel); + } + + // Icon + const iconContainer = append(this.element, $('div.icon-container')); + iconContainer.setAttribute('aria-hidden', 'true'); + iconContainer.appendChild($(`div${item.icon.cssSelector}`)); + + // Message + const messageContainer = append(this.element, $('div.message-container')); + messageContainer.setAttribute('aria-hidden', 'true'); + messageContainer.appendChild(this.getBannerMessage(item.message)); + + // Message Actions + if (item.actions) { + this.messageActionsContainer = append(this.element, $('div.message-actions-container')); + + for (const action of item.actions) { + const actionLink = this._register(this.instantiationService.createInstance(Link, action, {})); + actionLink.el.tabIndex = -1; + actionLink.el.setAttribute('role', 'button'); + this.messageActionsContainer.appendChild(actionLink.el); + } + } + + // Action + const actionBarContainer = append(this.element, $('div.action-container')); + this.actionBar = this._register(new ActionBar(actionBarContainer)); + const closeAction = this._register(new Action('banner.close', 'Close Banner', bannerCloseIcon.classNames, true, () => this.close(item))); + this.actionBar.push(closeAction, { icon: true, label: false }); + this.actionBar.setFocusable(false); + + this.setVisibility(true); + this.item = item; + } + + toJSON(): object { + return { + type: Parts.BANNER_PART + }; + } +} + +registerSingleton(IBannerService, BannerPart); + + +// Keybindings + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.banner.focusBanner', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + when: CONTEXT_BANNER_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const bannerService = accessor.get(IBannerService); + bannerService.focus(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.banner.focusNextAction', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.RightArrow, + secondary: [KeyCode.DownArrow], + when: CONTEXT_BANNER_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const bannerService = accessor.get(IBannerService); + bannerService.focusNextAction(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.banner.focusPreviousAction', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.LeftArrow, + secondary: [KeyCode.UpArrow], + when: CONTEXT_BANNER_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const bannerService = accessor.get(IBannerService); + bannerService.focusPreviousAction(); + } +}); + + +// Actions + +class FocusBannerAction extends Action2 { + + static readonly ID = 'workbench.action.focusBanner'; + static readonly LABEL = localize('focusBanner', "Focus Banner"); + + constructor() { + super({ + id: FocusBannerAction.ID, + title: { value: FocusBannerAction.LABEL, original: 'Focus Banner' }, + category: CATEGORIES.View, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.focusPart(Parts.BANNER_PART); + } +} + +registerAction2(FocusBannerAction); diff --git a/lib/vscode/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/lib/vscode/src/vs/workbench/browser/parts/banner/media/bannerpart.css new file mode 100644 index 000000000000..0b348bafbda2 --- /dev/null +++ b/lib/vscode/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .part.banner { + box-sizing: border-box; + cursor: default; + width: 100%; + height: 100%; + font-size: 12px; + display: flex; + overflow: visible; +} + +.monaco-workbench .part.banner .icon-container { + display: flex; + align-items: center; + padding: 0 6px 0 10px; +} + +.monaco-workbench .part.banner .message-container { + line-height: 26px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.monaco-workbench .part.banner .message-container p { + margin-block-start: 0; + margin-block-end: 0; +} + +.monaco-workbench .part.banner .message-actions-container { + flex-grow: 1; + flex-shrink: 0; + line-height: 26px; +} + +.monaco-workbench .part.banner .message-actions-container a { + padding: 3px; + margin-left: 12px; + text-decoration: underline; +} + +.monaco-workbench .part.banner .message-actions-container a:hover { + text-decoration: none; +} + +.monaco-workbench .part.banner .action-container { + padding: 0 10px 0 6px; +} diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/binaryEditor.ts index edfc611887e4..3608f7181b0b 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -6,7 +6,8 @@ import 'vs/css!./media/binaryeditor'; import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -19,9 +20,10 @@ import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/ import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ByteSize } from 'vs/platform/files/common/files'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export interface IOpenCallbacks { - openInternal: (input: EditorInput, options: EditorOptions | undefined) => Promise; + openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; } /* @@ -70,7 +72,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { parent.appendChild(this.scrollbar.getDomNode()); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); const model = await input.resolve(); @@ -88,7 +90,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { this.inputDisposable.value = this.renderInput(input, options, model); } - private renderInput(input: EditorInput, options: EditorOptions | undefined, model: BinaryEditorModel): IDisposable { + private renderInput(input: EditorInput, options: IEditorOptions | undefined, model: BinaryEditorModel): IDisposable { const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); clearNode(binaryContainer); diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index f0957c16ec5b..f24ca6352bb1 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -24,7 +24,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService, WorkbenchDataTree, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry'; +import { ColorIdentifier, ColorTransform } from 'vs/platform/theme/common/colorRegistry'; import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceLabel } from 'vs/workbench/browser/labels'; @@ -146,7 +146,7 @@ export interface IBreadcrumbsControlOptions { showFileIcons: boolean; showSymbolIcons: boolean; showDecorationColors: boolean; - breadcrumbsBackground: ColorIdentifier | ColorFunction; + breadcrumbsBackground: ColorIdentifier | ColorTransform; showPlaceholder: boolean; } @@ -219,6 +219,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget)); + this.hide(); } dispose(): void { @@ -312,6 +313,7 @@ export class BreadcrumbsControl { this._breadcrumbsDisposables.add(model); this._breadcrumbsDisposables.add(listener); this._breadcrumbsDisposables.add(configListener); + this._breadcrumbsDisposables.add(toDisposable(() => this._widget.setItems([]))); const updateScrollbarSizing = () => { const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default'; diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 54d768672041..5b579ff10f1f 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -5,17 +5,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; -import { EditorInput, IEditorInputSerializer, SideBySideEditorInput, IEditorInputFactoryRegistry, TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, MultipleEditorGroupsContext, ActiveEditorDirtyContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { + IEditorInputFactoryRegistry, TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorExtensions, EditorGroupEditorsCountContext, + ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, MultipleEditorGroupsContext, ActiveEditorDirtyContext +} from 'vs/workbench/common/editor'; +import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { DiffEditorInput, DiffEditorInputSerializer } from 'vs/workbench/common/editor/diffEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; -import { UntitledHintContribution } from 'vs/workbench/browser/parts/editor/untitledHint'; import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/binaryDiffEditor'; import { ChangeEncodingAction, ChangeEOLAction, ChangeModeAction, EditorStatus } from 'vs/workbench/browser/parts/editor/editorStatus'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; @@ -34,37 +36,33 @@ import { JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction, - QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction + QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ReOpenInTextEditorAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_PINNED_EDITOR_COMMAND_ID, CLOSE_SAVED_EDITORS_COMMAND_ID, GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, KEEP_EDITOR_COMMAND_ID, PIN_EDITOR_COMMAND_ID, SHOW_EDITORS_IN_GROUP, SPLIT_EDITOR_DOWN, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, - TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup + TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup as registerEditorCommands } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/codeeditor'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { toLocalResource } from 'vs/base/common/resources'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess, AllEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { FileAccess } from 'vs/base/common/network'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; + +//#region Editor Registrations -// Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextResourceEditor, @@ -73,11 +71,10 @@ Registry.as(EditorExtensions.Editors).registerEditor( ), [ new SyncDescriptor(UntitledTextEditorInput), - new SyncDescriptor(ResourceEditorInput) + new SyncDescriptor(TextResourceEditorInput) ] ); -// Register Text Diff Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextDiffEditor, @@ -89,7 +86,6 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); -// Register Binary Resource Diff Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( BinaryResourceDiffEditor, @@ -112,186 +108,24 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); -interface ISerializedUntitledTextEditorInput { - resourceJSON: UriComponents; - modeId: string | undefined; - encoding: string | undefined; -} - -// Register Editor Input Serializer -class UntitledTextEditorInputSerializer implements IEditorInputSerializer { - - constructor( - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IPathService private readonly pathService: IPathService - ) { } - - canSerialize(editorInput: EditorInput): boolean { - return this.filesConfigurationService.isHotExitEnabled && !editorInput.isDisposed(); - } - - serialize(editorInput: EditorInput): string | undefined { - if (!this.filesConfigurationService.isHotExitEnabled || editorInput.isDisposed()) { - return undefined; - } - - const untitledTextEditorInput = editorInput as UntitledTextEditorInput; - - let resource = untitledTextEditorInput.resource; - if (untitledTextEditorInput.model.hasAssociatedFilePath) { - resource = toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); // untitled with associated file path use the local schema - } - - // Mode: only remember mode if it is either specific (not text) - // or if the mode was explicitly set by the user. We want to preserve - // this information across restarts and not set the mode unless - // this is the case. - let modeId: string | undefined; - const modeIdCandidate = untitledTextEditorInput.getMode(); - if (modeIdCandidate !== PLAINTEXT_MODE_ID) { - modeId = modeIdCandidate; - } else if (untitledTextEditorInput.model.hasModeSetExplicitly) { - modeId = modeIdCandidate; - } - - const serialized: ISerializedUntitledTextEditorInput = { - resourceJSON: resource.toJSON(), - modeId, - encoding: untitledTextEditorInput.getEncoding() - }; - - return JSON.stringify(serialized); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledTextEditorInput { - return instantiationService.invokeFunction(accessor => { - const deserialized: ISerializedUntitledTextEditorInput = JSON.parse(serializedEditorInput); - const resource = URI.revive(deserialized.resourceJSON); - const mode = deserialized.modeId; - const encoding = deserialized.encoding; - - return accessor.get(IEditorService).createEditorInput({ resource, mode, encoding, forceUntitled: true }) as UntitledTextEditorInput; - }); - } -} - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(UntitledTextEditorInput.ID, UntitledTextEditorInputSerializer); - -// Register SideBySide/DiffEditor Input Serializer -interface ISerializedSideBySideEditorInput { - name: string; - description: string | undefined; - - primarySerialized: string; - secondarySerialized: string; - - primaryTypeId: string; - secondaryTypeId: string; -} - -export abstract class AbstractSideBySideEditorInputSerializer implements IEditorInputSerializer { - - private getInputSerializers(secondaryEditorInputTypeId: string, primaryEditorInputTypeId: string): [IEditorInputSerializer | undefined, IEditorInputSerializer | undefined] { - const registry = Registry.as(EditorExtensions.EditorInputFactories); - - return [registry.getEditorInputSerializer(secondaryEditorInputTypeId), registry.getEditorInputSerializer(primaryEditorInputTypeId)]; - } - - canSerialize(editorInput: EditorInput): boolean { - const input = editorInput as SideBySideEditorInput | DiffEditorInput; - - if (input.primary && input.secondary) { - const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); - - return !!(secondaryInputSerializer?.canSerialize(input.secondary) && primaryInputSerializer?.canSerialize(input.primary)); - } - - return false; - } - - serialize(editorInput: EditorInput): string | undefined { - const input = editorInput as SideBySideEditorInput | DiffEditorInput; - - if (input.primary && input.secondary) { - const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); - if (primaryInputSerializer && secondaryInputSerializer) { - const primarySerialized = primaryInputSerializer.serialize(input.primary); - const secondarySerialized = secondaryInputSerializer.serialize(input.secondary); - - if (primarySerialized && secondarySerialized) { - const serializedEditorInput: ISerializedSideBySideEditorInput = { - name: input.getName(), - description: input.getDescription(), - primarySerialized: primarySerialized, - secondarySerialized: secondarySerialized, - primaryTypeId: input.primary.typeId, - secondaryTypeId: input.secondary.typeId - }; - - return JSON.stringify(serializedEditorInput); - } - } - } - - return undefined; - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { - const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput); - - const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(deserialized.secondaryTypeId, deserialized.primaryTypeId); - if (primaryInputSerializer && secondaryInputSerializer) { - const primaryInput = primaryInputSerializer.deserialize(instantiationService, deserialized.primarySerialized); - const secondaryInput = secondaryInputSerializer.deserialize(instantiationService, deserialized.secondarySerialized); - - if (primaryInput && secondaryInput) { - return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput); - } - } - - return undefined; - } - - protected abstract createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput; -} - -class SideBySideEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { - - protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { - return new SideBySideEditorInput(name, description, secondaryInput, primaryInput); - } -} - -class DiffEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { - - protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { - return instantiationService.createInstance(DiffEditorInput, name, description, secondaryInput, primaryInput, undefined); - } -} - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(SideBySideEditorInput.ID, SideBySideEditorInputSerializer); Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(DiffEditorInput.ID, DiffEditorInputSerializer); -// Register Editor Contributions -registerEditorContribution(OpenWorkspaceButtonContribution.ID, OpenWorkspaceButtonContribution); +//#endregion -// Register Editor Status -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatus, LifecyclePhase.Ready); +//#region Workbench Contributions -// Register Editor Auto Save Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorAutoSave, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatus, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(UntitledTextEditorWorkingCopyEditorHandler, LifecyclePhase.Ready); -// Register Untitled Hint -registerEditorContribution(UntitledHintContribution.ID, UntitledHintContribution); +registerEditorContribution(OpenWorkspaceButtonContribution.ID, OpenWorkspaceButtonContribution); -// Register Status Actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeModeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_M) }), 'Change Language Mode', undefined, ContextKeyExpr.not('notebookEditorFocused')); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEOLAction), 'Change End of Line Sequence'); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEncodingAction), 'Change File Encoding'); +//#endregion + +//#region Quick Access -// Register Editor Quick Access const quickAccessRegistry = Registry.as(QuickAccessExtensions.Quickaccess); const editorPickerContextKey = 'inEditorsPicker'; const editorPickerContext = ContextKeyExpr.and(inQuickPickContext, ContextKeyExpr.has(editorPickerContextKey)); @@ -320,7 +154,17 @@ quickAccessRegistry.registerQuickAccessProvider({ helpEntries: [{ description: localize('allEditorsByMostRecentlyUsedQuickAccess', "Show All Opened Editors By Most Recently Used"), needsEditor: false }] }); -// Register Editor Actions +//#endregion + +//#region Actions & Commands + +// Editor Status +const registry = Registry.as(ActionExtensions.WorkbenchActions); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeModeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_M) }), 'Change Language Mode', undefined, ContextKeyExpr.not('notebookEditorFocused')); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEOLAction), 'Change End of Line Sequence'); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ChangeEncodingAction), 'Change File Encoding'); + +// Editor Management registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenNextEditor, { primary: KeyMod.CtrlCmd | KeyCode.PageDown, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET] } }), 'View: Open Next Editor', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenPreviousEditor, { primary: KeyMod.CtrlCmd | KeyCode.PageUp, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET] } }), 'View: Open Previous Editor', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenNextEditorInGroup, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.PageDown), mac: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow) } }), 'View: Open Next Editor in Group', CATEGORIES.View.value); @@ -400,16 +244,11 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(EditorLayoutTwoByTwoG registry.registerWorkbenchAction(SyncActionDescriptor.from(EditorLayoutTwoRowsRightAction), 'View: Two Rows Right Editor Layout', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(EditorLayoutTwoColumnsBottomAction), 'View: Two Columns Bottom Editor Layout', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ReopenResourcesAction), 'View: Reopen Editor With...', CATEGORIES.View.value, ActiveEditorAvailableEditorIdsContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorTypeAction), 'View: Toggle Editor Type', CATEGORIES.View.value, ActiveEditorAvailableEditorIdsContext); - -// Register Quick Editor Actions including built in quick navigate support for some - +registry.registerWorkbenchAction(SyncActionDescriptor.from(ReOpenInTextEditorAction), 'View: Reopen Editor With Text Editor', CATEGORIES.View.value, ActiveEditorAvailableEditorIdsContext); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousRecentlyUsedEditorAction), 'View: Quick Open Previous Recently Used Editor', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessLeastRecentlyUsedEditorAction), 'View: Quick Open Least Recently Used Editor', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousRecentlyUsedEditorInGroupAction, { primary: KeyMod.CtrlCmd | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyCode.Tab } }), 'View: Quick Open Previous Recently Used Editor in Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessLeastRecentlyUsedEditorInGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Tab } }), 'View: Quick Open Least Recently Used Editor in Group', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousEditorFromHistoryAction), 'Quick Open Previous Editor from History'); const quickAccessNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker'; @@ -432,10 +271,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Tab } }); -// Editor Commands -setup(); +registerEditorCommands(); -// Touch Bar +//#endregion Workbench Actions + +//#region Menus + +// macOS: Touchbar if (isMacintosh) { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, icon: { dark: FileAccess.asFileUri('vs/workbench/browser/parts/editor/media/back-tb.png', require) } }, @@ -605,7 +447,6 @@ const previousChangeIcon = registerIcon('diff-editor-previous-change', Codicon.a const nextChangeIcon = registerIcon('diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); const toggleWhitespace = registerIcon('diff-editor-toggle-whitespace', Codicon.whitespace, localize('toggleWhitespace', 'Icon for the toggle whitespace action in the diff editor.')); - // Diff Editor Title Menu: Previous Change appendEditorToolItem( { @@ -1026,3 +867,5 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { submenu: MenuId.MenubarSwitchGroupMenu, order: 2 }); + +//#endregion diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editor.ts index 6984779b870f..0761fee8bf6d 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editor.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editor.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorInput } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Dimension } from 'vs/base/browser/dom'; @@ -13,6 +14,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; export interface IEditorPartCreationOptions { restorePreviousState: boolean; @@ -129,15 +132,20 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito relayout(): void; } -export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: EditorOptions): EditorOptions { +export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: IEditorOptions): ITextEditorOptions { const activeGroupCodeEditor = group.activeEditorPane ? getIEditor(group.activeEditorPane.getControl()) : undefined; if (activeGroupCodeEditor) { if (!expectedActiveEditor || expectedActiveEditor.matches(group.activeEditor)) { - return TextEditorOptions.fromEditor(activeGroupCodeEditor, presetOptions); + const textOptions: ITextEditorOptions = { + ...presetOptions, + viewState: withNullAsUndefined(activeGroupCodeEditor.saveViewState()) + }; + + return textOptions; } } - return presetOptions || new EditorOptions(); + return presetOptions || Object.create(null); } /** diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorActions.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorActions.ts index 0cb09967252b..e901b3805879 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -5,7 +5,8 @@ import { localize } from 'vs/nls'; import { Action } from 'vs/base/common/actions'; -import { IEditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder, EditorInputCapabilities, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -24,6 +25,8 @@ import { Codicon } from 'vs/base/common/codicons'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { EditorOverride } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/services/editor/common/editorOverrideService'; export class ExecuteCommandAction extends Action { @@ -592,7 +595,7 @@ abstract class BaseCloseAllAction extends Action { // Auto-save on focus change: assume to Save unless the editor is untitled // because bringing up a dialog would save in this case anyway. - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.isUntitled()) { + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled)) { dirtyEditorsToAutoSave.add(editor); } @@ -1925,10 +1928,12 @@ export class ReopenResourcesAction extends Action { } } -export class ToggleEditorTypeAction extends Action { +export class ReOpenInTextEditorAction extends Action { - static readonly ID = 'workbench.action.toggleEditorType'; - static readonly LABEL = localize('workbench.action.toggleEditorType', "Toggle Editor Type"); + static readonly ID = 'workbench.action.reopenTextEditor'; + static readonly LABEL = localize('workbench.action.reopenTextEditor', "Reopen Editor With Text Editor"); + + private readonly fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); constructor( id: string, @@ -1952,12 +1957,17 @@ export class ToggleEditorTypeAction extends Action { const options = activeEditorPane.options; const group = activeEditorPane.group; - const overrides = this.editorService.getEditorOverrides(activeEditorResource, options, group); - const firstNonActiveOverride = overrides.find(([_, entry]) => !entry.active); - if (!firstNonActiveOverride) { + if (this.fileEditorInputFactory.isFileEditorInput(this.editorService.activeEditor)) { return; } - await firstNonActiveOverride[0].open(activeEditorPane.input, { ...options, override: firstNonActiveOverride[1].id }, group)?.override; + // Replace the current editor with the text editor + await this.editorService.replaceEditors([ + { + editor: activeEditorPane.input, + replacement: activeEditorPane.input, + options: { ...options, override: DEFAULT_EDITOR_ASSOCIATION.id }, + } + ], group); } } diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index 8bddd352872b..09ffc4af2a25 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -7,7 +7,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService, AutoSaveMode, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier, ISaveOptions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -90,7 +90,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution } private maybeTriggerAutoSave(reason: SaveReason, editorIdentifier?: IEditorIdentifier): void { - if (editorIdentifier && (editorIdentifier.editor.isReadonly() || editorIdentifier.editor.isUntitled())) { + if (editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Readonly) || editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Untitled)) { return; // no auto save for readonly or untitled editors } diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorCommands.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorCommands.ts index 625742980224..f30102c65b81 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { isObject, isString, isUndefined, isNumber, withNullAsUndefined } from 'vs/base/common/types'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane, ActiveEditorStickyContext, EditorsOrder, viewColumnToEditorGroup, EditorGroupColumn } from 'vs/workbench/common/editor'; +import { TextCompareEditorVisibleContext, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane, ActiveEditorStickyContext, EditorsOrder, viewColumnToEditorGroup, EditorGroupColumn, EditorInputCapabilities, isEditorIdentifier } from 'vs/workbench/common/editor'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; @@ -452,7 +452,7 @@ function registerOpenEditorAPICommands(): void { } }); - CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, leftResource: UriComponents, rightResource: UriComponents, label?: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { + CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, originalResource: UriComponents, modifiedResource: UriComponents, label?: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { const editorService = accessor.get(IEditorService); const editorGroupService = accessor.get(IEditorGroupsService); @@ -460,8 +460,8 @@ function registerOpenEditorAPICommands(): void { const [options, column] = mixinContext(context, optionsArg, columnArg); await editorService.openEditor({ - leftResource: URI.revive(leftResource), - rightResource: URI.revive(rightResource), + originalInput: { resource: URI.revive(originalResource) }, + modifiedInput: { resource: URI.revive(modifiedResource) }, label, options }, viewColumnToEditorGroup(editorGroupService, column)); @@ -487,7 +487,7 @@ function registerOpenEditorAPICommands(): void { group = editorGroupsService.getGroup(viewColumnToEditorGroup(editorGroupsService, columnArg)) ?? editorGroupsService.activeGroup; } - return editorService.openEditor({ resource: URI.revive(resource), options: { ...optionsArg, override: id } }, group); + return editorService.openEditor({ resource: URI.revive(resource), options: { ...optionsArg, pinned: true, override: id } }, group); }); } @@ -631,13 +631,14 @@ export function splitEditor(editorGroupService: IEditorGroupsService, direction: editorToCopy = withNullAsUndefined(sourceGroup.activeEditor); } - // Copy the editor to the new group, else move the editor to the new group - if (editorToCopy && (editorToCopy as EditorInput).canSplit()) { + // Copy the editor to the new group, else create an empty group + if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { sourceGroup.copyEditor(editorToCopy, newGroup); - // Focus - newGroup.focus(); } + // Focus + newGroup.focus(); + } function registerSplitEditorCommands() { @@ -1045,15 +1046,12 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon } function isEditorGroup(thing: unknown): thing is IEditorGroup { - const group = thing as IEditorGroup; - - return group && typeof group.id === 'number' && Array.isArray(group.editors); -} - -function isEditorIdentifier(thing: unknown): thing is IEditorIdentifier { - const identifier = thing as IEditorIdentifier; + const group = thing as IEditorGroup | undefined; + if (!group) { + return false; + } - return identifier && typeof identifier.groupId === 'number'; + return typeof group.id === 'number' && Array.isArray(group.editors); } export function setup(): void { diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorControl.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorControl.ts index 10819e7d186d..b49d6dd9e0f6 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorExtensions, EditorInput, EditorOptions, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { Dimension, show, hide } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, IEditorDescriptor } from 'vs/workbench/browser/editor'; @@ -15,6 +16,9 @@ import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progre import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export interface IOpenEditorResult { readonly editorPane: EditorPane; @@ -42,31 +46,64 @@ export class EditorControl extends Disposable { private readonly activeEditorPaneDisposables = this._register(new DisposableStore()); private dimension: Dimension | undefined; private readonly editorOperation = this._register(new LongRunningOperation(this.editorProgressService)); + private readonly editorsRegistry = Registry.as(EditorExtensions.Editors); constructor( private parent: HTMLElement, private groupView: IEditorGroupView, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IEditorProgressService private readonly editorProgressService: IEditorProgressService + @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, ) { super(); + + this.registerListeners(); } - async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { + private registerListeners(): void { + this._register(this.workspaceTrustService.onDidChangeTrust(() => this.onDidChangeWorkspaceTrust())); + } - // Editor pane - const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editor); - if (!descriptor) { - throw new Error(`No editor descriptor found for input id ${editor.typeId}`); + private onDidChangeWorkspaceTrust() { + + // If the active editor pane requires workspace trust + // we need to re-open it anytime trust changes to + // account for it. + // For that we explicitly call into the group-view + // to handle errors properly. + const editor = this._activeEditorPane?.input; + const options = this._activeEditorPane?.options; + if (editor?.hasCapability(EditorInputCapabilities.RequiresTrust)) { + this.groupView.openEditor(editor, options); } + } + + async openEditor(editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise { + + // Editor descriptor + const descriptor = this.getEditorDescriptor(editor); + + // Editor pane const editorPane = this.doShowEditorPane(descriptor); - // Set input + // Apply input to pane const editorChanged = await this.doSetInput(editorPane, editor, options, context); return { editorPane, editorChanged }; } + private getEditorDescriptor(editor: EditorInput): IEditorDescriptor { + if (editor.hasCapability(EditorInputCapabilities.RequiresTrust) && !this.workspaceTrustService.isWorkpaceTrusted()) { + // Workspace trust: if an editor signals it needs workspace trust + // but the current workspace is untrusted, we fallback to a generic + // editor descriptor to indicate this an do NOT load the registered + // editor. + return WorkspaceTrustRequiredEditor.DESCRIPTOR; + } + + return assertIsDefined(this.editorsRegistry.getEditor(editor)); + } + private doShowEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if the currently active editor pane can handle the input @@ -108,7 +145,6 @@ export class EditorControl extends Disposable { if (!editorPane.getContainer()) { const editorPaneContainer = document.createElement('div'); editorPaneContainer.classList.add('editor-instance'); - editorPaneContainer.setAttribute('data-editor-id', descriptor.getId()); editorPane.create(editorPaneContainer); } @@ -147,7 +183,7 @@ export class EditorControl extends Disposable { this._onDidChangeSizeConstraints.fire(undefined); } - private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { + private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext): Promise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 2d813d3e68e3..10cecdf94a43 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -10,7 +10,7 @@ import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions } f import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { IEditorIdentifier, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { GroupDirection, IEditorGroupsService, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; @@ -18,12 +18,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { RunOnceScheduler } from 'vs/base/common/async'; import { DataTransfers } from 'vs/base/browser/dnd'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; import { ByteSize } from 'vs/platform/files/common/files'; @@ -55,7 +55,7 @@ class DropOverlay extends Themable { @IInstantiationService private instantiationService: IInstantiationService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: IEditorService, - @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { super(themeService); @@ -281,10 +281,10 @@ class DropOverlay extends Themable { } // Open in target group - const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ + const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, { pinned: true, // always pin dropped editor sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state - })); + }); const copyEditor = this.isCopyOperation(event, draggedEditor); if (!copyEditor) { @@ -313,7 +313,7 @@ class DropOverlay extends Themable { // Skip for very large files because this operation is unbuffered if (file.size > DropOverlay.MAX_FILE_UPLOAD_SIZE) { - this.notificationService.warn(localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again.")); + this.dialogService.show(Severity.Warning, localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again."), [localize('ok', 'OK')]); continue; } @@ -332,18 +332,16 @@ class DropOverlay extends Themable { proposedFilePath = joinPath(defaultFilePath, name); } - // Open as untitled file with the provided contents - const untitledEditor = this.editorService.createEditorInput({ - resource: proposedFilePath, - forceUntitled: true, - contents: VSBuffer.wrap(new Uint8Array(event.target.result)).toString() - }); - if (!targetGroup) { targetGroup = ensureTargetGroup(); } - await targetGroup.openEditor(untitledEditor); + // Open as untitled text file with the provided contents + await this.editorService.openEditor({ + resource: proposedFilePath, + forceUntitled: true, + contents: VSBuffer.wrap(new Uint8Array(event.target.result)).toString() + }, targetGroup.id); } }; } @@ -354,16 +352,12 @@ class DropOverlay extends Themable { // Check for URI transfer else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true /* open workspace instead of file if dropped */ }); - dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => { - if (targetGroup) { - targetGroup.focus(); - } - }); + dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => targetGroup?.focus()); } } private isCopyOperation(e: DragEvent, draggedEditor?: IEditorIdentifier): boolean { - if (draggedEditor?.editor instanceof EditorInput && !draggedEditor.editor.canSplit()) { + if (draggedEditor?.editor.hasCapability(EditorInputCapabilities.Singleton)) { return false; } diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorGroupView.ts index b30b4a700101..fe11ee21bad4 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -5,7 +5,9 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroupModel, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, ActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, ActiveEditorStickyContext, ActiveEditorPinnedContext, EditorResourceAccessor, IEditorMoveEvent } from 'vs/workbench/common/editor'; +import { GroupIdentifier, CloseDirection, IEditorCloseEvent, ActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, ActiveEditorStickyContext, ActiveEditorPinnedContext, EditorResourceAccessor, IEditorMoveEvent, EditorInputCapabilities, IEditorOpenEvent } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor, asCSSUrl } from 'vs/base/browser/dom'; @@ -45,7 +47,7 @@ import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; import { extname, isEqual } from 'vs/base/common/resources'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOpenContext, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IDialogService, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { ILogService } from 'vs/platform/log/common/log'; import { Codicon } from 'vs/base/common/codicons'; @@ -100,6 +102,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private readonly _onWillMoveEditor = this._register(new Emitter()); readonly onWillMoveEditor = this._onWillMoveEditor.event; + private readonly _onWillOpenEditor = this._register(new Emitter()); + readonly onWillOpenEditor = this._onWillOpenEditor.event; + //#endregion private readonly model: EditorGroupModel; @@ -286,7 +291,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (this.isEmpty) { EventHelper.stop(e); - this.openEditor(this.editorService.createEditorInput({ forceUntitled: true }), EditorOptions.create({ pinned: true })); + this.openEditor(this.editorService.createEditorInput({ forceUntitled: true }), { pinned: true }); } })); @@ -456,11 +461,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Determine editor options - let options: EditorOptions; + let options: IEditorOptions; if (from instanceof EditorGroupView) { options = getActiveTextEditorOptions(from); // if we copy from another group, ensure to copy its active editor viewstate } else { - options = new EditorOptions(); + options = Object.create(null); } const activeEditor = this.model.activeEditor; @@ -499,7 +504,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._register(this.model.onDidCloseEditor(editor => this.handleOnDidCloseEditor(editor))); this._register(this.model.onWillDisposeEditor(editor => this.onWillDisposeEditor(editor))); this._register(this.model.onDidChangeEditorDirty(editor => this.onDidChangeEditorDirty(editor))); - this._register(this.model.onDidEditorLabelChange(editor => this.onDidEditorLabelChange(editor))); + this._register(this.model.onDidChangeEditorLabel(editor => this.onDidChangeEditorLabel(editor))); + this._register(this.model.onDidChangeEditorCapabilities(editor => this.onDidChangeEditorCapabilities(editor))); // Option Changes this._register(this.accessor.onDidChangeEditorPartOptions(e => this.onDidChangeEditorPartOptions(e))); @@ -687,7 +693,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_DIRTY, editor }); } - private onDidEditorLabelChange(editor: EditorInput): void { + private onDidChangeEditorLabel(editor: EditorInput): void { // Forward to title control this.titleAreaControl.updateEditorLabel(editor); @@ -696,6 +702,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_LABEL, editor }); } + private onDidChangeEditorCapabilities(editor: EditorInput): void { + + // Forward to title control + this.titleAreaControl.updateEditorCapabilities(editor); + + // Event + this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_CAPABILITIES, editor }); + } + private onDidVisibilityChange(visible: boolean): void { // Forward to editor control @@ -899,26 +914,18 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditor() - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { - - // Guard against invalid inputs - if (!editor) { - return undefined; - } - - // Proceed with opening - return this.doOpenEditor(editor, options); - } - - private async doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options?: IEditorOptions): Promise { // Guard against invalid inputs. Disposed inputs // should never open because they emit no events // e.g. to indicate dirty changes. - if (editor.isDisposed()) { + if (!editor || editor.isDisposed()) { return; } + // Fire the event letting everyone know we are about to open an editor + this._onWillOpenEditor.fire({ editor, groupId: this.id }); + // Determine options const openEditorOptions: IEditorOpenOptions = { index: options ? options.index : undefined, @@ -991,7 +998,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return showEditorResult; } - private doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: EditorOptions): Promise { + private doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: IEditorOptions): Promise { // Show in editor control if the active editor changed let openEditorPromise: Promise; @@ -1024,11 +1031,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return openEditorPromise; } - private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: EditorOptions): Promise { + private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: IEditorOptions): Promise { // Report error only if we are not told to ignore errors that occur from opening an editor if (!isPromiseCanceledError(error) && (!options || !options.ignoreError)) { + // Always log the error to figure out what is going on + this.logService.error(error); + // Since it is more likely that errors fail to open when restoring them e.g. // because files got deleted or moved meanwhile, we do not show any notifications // if we are still restoring editors. @@ -1093,11 +1103,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { Event.once(handle.onDidClose)(() => actions.primary && dispose(actions.primary)); } } - - // Restoring: just log errors to console - else { - this.logService.error(error); - } } // Event @@ -1114,7 +1119,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditors() - async openEditors(editors: { editor: EditorInput, options?: EditorOptions }[]): Promise { + async openEditors(editors: { editor: EditorInput, options?: IEditorOptions }[]): Promise { if (!editors.length) { return null; } @@ -1129,7 +1134,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Open the other ones inactive const startingIndex = this.getIndexOfEditor(editor) + 1; await Promises.settled(editors.map(async ({ editor, options }, index) => { - const adjustedEditorOptions = options || new EditorOptions(); + const adjustedEditorOptions = options || Object.create(null); adjustedEditorOptions.inactive = true; adjustedEditorOptions.pinned = true; adjustedEditorOptions.index = startingIndex + index; @@ -1147,7 +1152,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region moveEditor() - moveEditor(editor: EditorInput, target: IEditorGroupView, options?: EditorOptions): void { + moveEditor(editor: EditorInput, target: IEditorGroupView, options?: IEditorOptions): void { // Move within same group if (this === target) { @@ -1196,11 +1201,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // When moving/copying an editor, try to preserve as much view state as possible // by checking for the editor to be a text editor and creating the options accordingly // if so - const options = getActiveTextEditorOptions(this, editor, EditorOptions.create({ + const options = getActiveTextEditorOptions(this, editor, { ...openOptions, pinned: true, // always pin moved editor sticky: !keepCopy && this.model.isSticky(editor) // preserve sticky state only if editor is moved (https://github.com/microsoft/vscode/issues/99035) - })); + }); // Indicate will move event if (!keepCopy) { @@ -1224,7 +1229,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region copyEditor() - copyEditor(editor: EditorInput, target: IEditorGroupView, options?: EditorOptions): void { + copyEditor(editor: EditorInput, target: IEditorGroupView, options?: IEditorOptions): void { // Move within same group because we do not support to show the same editor // multiple times in the same group @@ -1322,16 +1327,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { activation = EditorActivation.PRESERVE; } - const options = EditorOptions.create({ preserveFocus, activation }); - - // When closing an editor due to an error we can end up in a loop where we continue closing - // editors that fail to open (e.g. when the file no longer exists). We do not want to show - // repeated errors in this case to the user. As such, if we open the next editor and we are - // in a scope of a previous editor failing, we silence the input errors until the editor is - // opened by setting ignoreError: true. - if (fromError) { - options.ignoreError = true; - } + const options: IEditorOptions = { + preserveFocus, + activation, + // When closing an editor due to an error we can end up in a loop where we continue closing + // editors that fail to open (e.g. when the file no longer exists). We do not want to show + // repeated errors in this case to the user. As such, if we open the next editor and we are + // in a scope of a previous editor failing, we silence the input errors until the editor is + // opened by setting ignoreError: true. + ignoreError: fromError + }; this.openEditor(nextActiveEditor, options); } @@ -1448,7 +1453,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { let confirmation: ConfirmResult; let saveReason = SaveReason.EXPLICIT; let autoSave = false; - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.isUntitled() && !options?.skipAutoSave) { + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled) && !options?.skipAutoSave) { autoSave = true; confirmation = ConfirmResult.SAVE; saveReason = SaveReason.FOCUS_CHANGE; @@ -1654,7 +1659,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (options) { options.index = index; } else { - options = EditorOptions.create({ index }); + options = { index }; } options.inactive = !isActiveEditor; @@ -1673,7 +1678,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { for (const { editor, replacement, forceReplaceDirty, options } of inactiveReplacements) { // Open inactive editor - await this.doOpenEditor(replacement, options); + await this.openEditor(replacement, options); // Close replaced inactive editor unless they match if (!editor.matches(replacement)) { @@ -1694,7 +1699,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (activeReplacement) { // Open replacement as active editor - const openEditorResult = this.doOpenEditor(activeReplacement.replacement, activeReplacement.options); + const openEditorResult = this.openEditor(activeReplacement.replacement, activeReplacement.options); // Close replaced active editor unless they match if (!activeReplacement.editor.matches(activeReplacement.replacement)) { @@ -1796,7 +1801,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { export interface EditorReplacement extends IEditorReplacement { readonly editor: EditorInput; readonly replacement: EditorInput; - readonly options?: EditorOptions; + readonly options?: IEditorOptions; } registerThemingParticipant((theme, collector) => { diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorPane.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorPane.ts index 081a0a8ae31d..b07b6a1c1f79 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Composite } from 'vs/workbench/browser/composite'; -import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -20,6 +21,7 @@ import { joinPath, IExtUri, isEqual } from 'vs/base/common/resources'; import { indexOfPath } from 'vs/base/common/extpath'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -56,8 +58,8 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _input: EditorInput | undefined; get input(): EditorInput | undefined { return this._input; } - protected _options: EditorOptions | undefined; - get options(): EditorOptions | undefined { return this._options; } + protected _options: IEditorOptions | undefined; + get options(): IEditorOptions | undefined { return this._options; } private _group: IEditorGroup | undefined; get group(): IEditorGroup | undefined { return this._group; } @@ -102,7 +104,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { * The provided cancellation token should be used to test if the operation * was cancelled. */ - async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this._input = input; this._options = options; } @@ -129,7 +131,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { * Sets the given options to the editor. Clients should apply the options * to the current input. */ - setOptions(options: EditorOptions | undefined): void { + setOptions(options: IEditorOptions | undefined): void { this._options = options; } @@ -193,7 +195,7 @@ export class EditorMemento implements IEditorMemento { private editorDisposables: Map | undefined; constructor( - public readonly id: string, + readonly id: string, private key: string, private memento: MementoObject, private limit: number, diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorPart.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorPart.ts index 1383de7ab678..fddbc8a13c11 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -211,11 +211,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro private whenRestoredResolve: (() => void) | undefined; readonly whenRestored = new Promise(resolve => (this.whenRestoredResolve = resolve)); - private restored = false; - - isRestored(): boolean { - return this.restored; - } get hasRestorableState(): boolean { return !!this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY]; @@ -845,7 +840,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro // Signal restored Promises.settled(this.groups.map(group => group.whenRestored)).finally(() => { - this.restored = true; this.whenRestoredResolve?.(); }); diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index a2e88453c0dc..7f8a6094a07f 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -68,7 +68,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro return super.provide(picker, token); } - protected getPicks(filter: string): Array { + protected _getPicks(filter: string): Array { const query = prepareQuery(filter); // Filtering diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorStatus.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorStatus.ts index 4c2e352c4550..cd87db7f04d3 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { Action, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { IFileEditorInput, EditorResourceAccessor, SideBySideEditorInput, IEditorPane, IEditorInput, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IFileEditorInput, EditorResourceAccessor, IEditorPane, IEditorInput, SideBySideEditor, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { Disposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; @@ -53,6 +53,7 @@ import { IMarker, IMarkerService, MarkerSeverity, IMarkerData } from 'vs/platfor import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private primary: IEncodingSupport, private secondary: IEncodingSupport) { } @@ -369,7 +370,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); } - if (this.editorService.activeEditor?.isReadonly()) { + if (this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } @@ -404,13 +405,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.tabFocusModeElement.value) { const text = localize('tabFocusModeEnabled', "Tab Moves Focus"); this.tabFocusModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.tabFocusMode', "Accessibility Mode"), text, ariaLabel: text, tooltip: localize('disableTabMode', "Disable Accessibility Mode"), command: 'editor.action.toggleTabFocusMode', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.tabFocusMode', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); + }, 'status.editor.tabFocusMode', StatusbarAlignment.RIGHT, 100.7); } } else { this.tabFocusModeElement.clear(); @@ -422,13 +424,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.columnSelectionModeElement.value) { const text = localize('columnSelectionModeEnabled', "Column Selection"); this.columnSelectionModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.columnSelectionMode', "Column Selection Mode"), text, ariaLabel: text, tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"), command: 'editor.action.toggleColumnSelection', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.columnSelectionMode', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + }, 'status.editor.columnSelectionMode', StatusbarAlignment.RIGHT, 100.8); } } else { this.columnSelectionModeElement.clear(); @@ -440,12 +443,13 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.screenRedearModeElement.value) { const text = localize('screenReaderDetected', "Screen Reader Optimized"); this.screenRedearModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.screenReaderMode', "Screen Reader Mode"), text, ariaLabel: text, command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); + }, 'status.editor.screenReaderMode', StatusbarAlignment.RIGHT, 100.6); } } else { this.screenRedearModeElement.clear(); @@ -459,13 +463,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.selection', "Editor Selection"), text, ariaLabel: text, tooltip: localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; - this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); + this.updateElement(this.selectionElement, props, 'status.editor.selection', StatusbarAlignment.RIGHT, 100.5); } private updateIndentationElement(text: string | undefined): void { @@ -475,13 +480,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.indentation', "Editor Indentation"), text, ariaLabel: text, tooltip: localize('selectIndentation', "Select Indentation"), command: 'changeEditorIndentation' }; - this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); + this.updateElement(this.indentationElement, props, 'status.editor.indentation', StatusbarAlignment.RIGHT, 100.4); } private updateEncodingElement(text: string | undefined): void { @@ -491,13 +497,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.encoding', "Editor Encoding"), text, ariaLabel: text, tooltip: localize('selectEncoding', "Select Encoding"), command: 'workbench.action.editor.changeEncoding' }; - this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); + this.updateElement(this.encodingElement, props, 'status.editor.encoding', StatusbarAlignment.RIGHT, 100.3); } private updateEOLElement(text: string | undefined): void { @@ -507,13 +514,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.eol', "Editor End of Line"), text, ariaLabel: text, tooltip: localize('selectEOL', "Select End of Line Sequence"), command: 'workbench.action.editor.changeEOL' }; - this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); + this.updateElement(this.eolElement, props, 'status.editor.eol', StatusbarAlignment.RIGHT, 100.2); } private updateModeElement(text: string | undefined): void { @@ -523,13 +531,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.mode', "Editor Language"), text, ariaLabel: text, tooltip: localize('selectLanguageMode', "Select Language Mode"), command: 'workbench.action.editor.changeLanguageMode' }; - this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); + this.updateElement(this.modeElement, props, 'status.editor.mode', StatusbarAlignment.RIGHT, 100.1); } private updateMetadataElement(text: string | undefined): void { @@ -539,17 +548,18 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.info', "File Information"), text, ariaLabel: text, tooltip: localize('fileInfo', "File Information") }; - this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); + this.updateElement(this.metadataElement, props, 'status.editor.info', StatusbarAlignment.RIGHT, 100); } - private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { + private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: number) { if (!element.value) { - element.value = this.statusbarService.addEntry(props, id, name, alignment, priority); + element.value = this.statusbarService.addEntry(props, id, alignment, priority); } else { element.value.update(props); } @@ -923,9 +933,9 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { const line = splitLines(this.currentMarker.message)[0]; const text = `${this.getType(this.currentMarker)} ${line}`; if (!this.statusBarEntryAccessor.value) { - this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ name: localize('currentProblem', "Current Problem"), text: '', ariaLabel: '' }, 'statusbar.currentProblem', StatusbarAlignment.LEFT); } - this.statusBarEntryAccessor.value.update({ text, ariaLabel: text }); + this.statusBarEntryAccessor.value.update({ name: localize('currentProblem', "Current Problem"), text, ariaLabel: text }); } else { this.statusBarEntryAccessor.clear(); } @@ -1270,7 +1280,7 @@ export class ChangeEOLAction extends Action { return; } - if (this.editorService.activeEditor?.isReadonly()) { + if (this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { await this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); return; } @@ -1287,7 +1297,7 @@ export class ChangeEOLAction extends Action { const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); if (eol) { const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); - if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.isReadonly()) { + if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { textModel = activeCodeEditor.getModel(); textModel.pushStackElement(); textModel.pushEOL(eol.eol); @@ -1353,7 +1363,7 @@ export class ChangeEncodingAction extends Action { let action: IQuickPickItem | undefined; if (encodingSupport instanceof UntitledTextEditorInput) { action = saveWithEncodingPick; - } else if (activeEditorPane.input.isReadonly()) { + } else if (activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly)) { action = reopenWithEncodingPick; } else { action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/editorsObserver.ts index 1abe2fd4004a..108a035f888d 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, EditorExtensions, IEditorPartOptionsChangeEvent, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, EditorExtensions, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/media/back-tb.png b/lib/vscode/src/vs/workbench/browser/parts/editor/media/back-tb.png index c9d6f935ae4b..d279d15d18bb 100644 Binary files a/lib/vscode/src/vs/workbench/browser/parts/editor/media/back-tb.png and b/lib/vscode/src/vs/workbench/browser/parts/editor/media/back-tb.png differ diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/media/forward-tb.png b/lib/vscode/src/vs/workbench/browser/parts/editor/media/forward-tb.png index 69c52268ab80..ff7bb68bb680 100644 Binary files a/lib/vscode/src/vs/workbench/browser/parts/editor/media/forward-tb.png and b/lib/vscode/src/vs/workbench/browser/parts/editor/media/forward-tb.png differ diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css b/lib/vscode/src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css new file mode 100644 index 000000000000..e345faf8f2fd --- /dev/null +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/media/workspacetrusteditor.css @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workspace-trust-required-editor:focus { + outline: none !important; +} + +.monaco-workspace-trust-required-editor { + padding: 5px 0 0 10px; + box-sizing: border-box; +} + +.monaco-workspace-trust-required-editor .embedded-link, +.monaco-workspace-trust-required-editor .embedded-link:hover { + cursor: pointer; + text-decoration: underline; + margin-left: 5px; +} diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 8d3ae15eecb6..edd17f5975fc 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -50,7 +50,7 @@ export class NoTabsTitleControl extends TitleControl { this._register(addDisposableListener(this.editorLabel.element, EventType.CLICK, e => this.onTitleLabelClick(e))); // Breadcrumbs - this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent, showPlaceholder: false }); + this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: Color.transparent.toString(), showPlaceholder: false }); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); this._register(toDisposable(() => titleContainer.classList.remove('breadcrumbs'))); // important to remove because the container is a shared dom node @@ -114,7 +114,7 @@ export class NoTabsTitleControl extends TitleControl { private onTitleTap(e: GestureEvent): void { // We only want to open the quick access picker when - // the tap occured over the editor label, so we need + // the tap occurred over the editor label, so we need // to check on the target // (https://github.com/microsoft/vscode/issues/107543) const target = e.initialTarget; @@ -167,6 +167,10 @@ export class NoTabsTitleControl extends TitleControl { this.ifEditorIsActive(editor, () => this.redraw()); } + updateEditorCapabilities(editor: IEditorInput): void { + this.ifEditorIsActive(editor, () => this.redraw()); + } + updateEditorLabels(): void { if (this.group.activeEditor) { this.updateEditorLabel(this.group.activeEditor); // we only have the active one to update diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 5a857f4cc0aa..bf1508b3dde9 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -5,7 +5,9 @@ import { Dimension, $, clearNode } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane, IEditorOpenContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorControl, IEditorPane, IEditorOpenContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,6 +20,7 @@ import { SplitView, Sizing, Orientation } from 'vs/base/browser/ui/splitview/spl import { Event, Relay, Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined } from 'vs/base/common/types'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export class SideBySideEditor extends EditorPane { @@ -94,14 +97,14 @@ export class SideBySideEditor extends EditorPane { this.updateStyles(); } - override async setInput(newInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const oldInput = this.input as SideBySideEditorInput; - await super.setInput(newInput, options, context, token); + override async setInput(input: SideBySideEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + const oldInput = this.input; + await super.setInput(input, options, context, token); - return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, context, token); + return this.updateInput(oldInput, input, options, context, token); } - override setOptions(options: EditorOptions | undefined): void { + override setOptions(options: IEditorOptions | undefined): void { if (this.primaryEditorPane) { this.primaryEditorPane.setOptions(options); } @@ -162,7 +165,7 @@ export class SideBySideEditor extends EditorPane { return this.secondaryEditorPane; } - private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + private async updateInput(oldInput: EditorInput | undefined, newInput: SideBySideEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); @@ -181,11 +184,23 @@ export class SideBySideEditor extends EditorPane { ]); } - private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const secondaryEditor = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); - const primaryEditor = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); + private async setNewInput(newInput: SideBySideEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + this.secondaryEditorPane = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); + this.primaryEditorPane = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); - return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, context, token); + this.layout(this.dimension); + + this._onDidChangeSizeConstraints.input = Event.any( + Event.map(this.secondaryEditorPane.onDidChangeSizeConstraints, () => undefined), + Event.map(this.primaryEditorPane.onDidChangeSizeConstraints, () => undefined) + ); + + this.onDidCreateEditors.fire(undefined); + + await Promise.all([ + this.secondaryEditorPane.setInput(newInput.secondary, undefined, context, token), + this.primaryEditorPane.setInput(newInput.primary, options, context, token)] + ); } private doCreateEditor(editorInput: EditorInput, container: HTMLElement): EditorPane { @@ -201,23 +216,6 @@ export class SideBySideEditor extends EditorPane { return editor; } - private async onEditorsCreated(secondary: EditorPane, primary: EditorPane, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - this.secondaryEditorPane = secondary; - this.primaryEditorPane = primary; - - this._onDidChangeSizeConstraints.input = Event.any( - Event.map(secondary.onDidChangeSizeConstraints, () => undefined), - Event.map(primary.onDidChangeSizeConstraints, () => undefined) - ); - - this.onDidCreateEditors.fire(undefined); - - await Promise.all([ - this.secondaryEditorPane.setInput(secondaryInput, undefined, context, token), - this.primaryEditorPane.setInput(primaryInput, options, context, token)] - ); - } - override updateStyles(): void { super.updateStyles(); diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 5ca4311be8b7..ae94f70f7a2f 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/tabstitlecontrol'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; -import { EditorResourceAccessor, GroupIdentifier, IEditorInput, Verbosity, EditorCommandsContextActionRunner, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, GroupIdentifier, IEditorInput, Verbosity, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -19,7 +19,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { EditorCommandsContextActionRunner, ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IDisposable, dispose, DisposableStore, combinedDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -410,14 +410,8 @@ export class TabsTitleControl extends TitleControl { } closeEditors(editors: IEditorInput[]): void { - // Cleanup closed editors this.handleClosedEditors(); - - // Update Breadcrumbs when last editor closed - if (this.group.count === 0) { - this.breadcrumbsControl?.update(); - } } private handleClosedEditors(): void { @@ -455,6 +449,8 @@ export class TabsTitleControl extends TitleControl { this.tabActionBars = []; this.clearEditorActionsToolbar(); + + this.breadcrumbsControl?.update(); } } @@ -513,6 +509,10 @@ export class TabsTitleControl extends TitleControl { this.layout(this.dimensions); } + updateEditorCapabilities(editor: IEditorInput): void { + this.updateEditorLabel(editor); + } + private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); updateEditorLabel(editor: IEditorInput): void { @@ -806,7 +806,7 @@ export class TabsTitleControl extends TitleControl { } // Apply some datatransfer types to allow for dragging the element outside of the application - this.doFillResourceDataTransfers(editor, e); + this.doFillResourceDataTransfers([editor], e); // Fixes https://github.com/microsoft/vscode/issues/18733 tab.classList.add('dragged'); diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index e9a4d85f130e..4e5aee431f97 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -5,11 +5,12 @@ import { localize } from 'vs/nls'; import { deepClone } from 'vs/base/common/objects'; -import { isFunction, isObject, isArray, assertIsDefined, withUndefinedAsNull } from 'vs/base/common/types'; +import { isObject, isArray, assertIsDefined, withUndefinedAsNull } from 'vs/base/common/types'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -21,13 +22,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/common/editorCommon'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isEqual } from 'vs/base/common/resources'; import { multibyteAwareBtoa } from 'vs/base/browser/dom'; @@ -43,6 +44,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan private diffNavigator: DiffNavigator | undefined; private readonly diffNavigatorDisposables = this._register(new DisposableStore()); + private readonly inputListener = this._register(new MutableDisposable()); + override get scopedContextKeyService(): IContextKeyService | undefined { const control = this.getControl(); if (!control) { @@ -68,21 +71,29 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService); // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); + } + + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input instanceof DiffEditorInput && (this.input.originalInput.resource?.scheme === scheme || this.input.modifiedInput.resource?.scheme === scheme)) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: DiffEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } } - private onDidFileSystemProviderChange(scheme: string): void { + private updateReadonly(input: DiffEditorInput): void { const control = this.getControl(); - const input = this.input; - - if (control && input instanceof DiffEditorInput) { - if (input.originalInput.resource?.scheme === scheme || input.modifiedInput.resource?.scheme === scheme) { - control.updateOptions({ - readOnly: input.modifiedInput.isReadonly(), - originalEditable: !input.originalInput.isReadonly() - }); - } + if (control) { + control.updateOptions({ + readOnly: input.modifiedInput.hasCapability(EditorInputCapabilities.Readonly), + originalEditable: !input.originalInput.hasCapability(EditorInputCapabilities.Readonly) + }); } } @@ -106,7 +117,10 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: DiffEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -125,8 +139,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return undefined; } - // Assert Model Instance - if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) { + // Fallback to open as binary if not text + if (!(resolvedModel instanceof TextDiffEditorModel)) { + this.openAsBinary(input, options); return undefined; } @@ -135,10 +150,10 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan const resolvedDiffEditorModel = resolvedModel as TextDiffEditorModel; diffEditor.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); - // Apply Options from TextOptions + /// Apply options to editor if any let optionsGotApplied = false; - if (options && isFunction((options).apply)) { - optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); + if (options) { + optionsGotApplied = applyTextEditorOptions(options, diffEditor, ScrollType.Immediate); } // Otherwise restore View State unless disabled via settings @@ -165,7 +180,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } catch (error) { // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. - if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) { + if (this.isFileBinaryError(error)) { + this.openAsBinary(input, options); return; } @@ -173,62 +189,51 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } } - private restoreTextDiffEditorViewState(editor: EditorInput, control: IDiffEditor): boolean { - if (editor instanceof DiffEditorInput) { - const resource = this.toDiffEditorViewStateResource(editor); - if (resource) { - const viewState = this.loadTextEditorViewState(resource); - if (viewState) { - control.restoreViewState(viewState); + private restoreTextDiffEditorViewState(editor: DiffEditorInput, control: IDiffEditor): boolean { + const resource = this.toDiffEditorViewStateResource(editor); + if (resource) { + const viewState = this.loadTextEditorViewState(resource); + if (viewState) { + control.restoreViewState(viewState); - return true; - } + return true; } } return false; } - private openAsBinary(input: EditorInput, options: EditorOptions | undefined): boolean { - if (input instanceof DiffEditorInput) { - const originalInput = input.originalInput; - const modifiedInput = input.modifiedInput; + private openAsBinary(input: DiffEditorInput, options: ITextEditorOptions | undefined): void { + const originalInput = input.originalInput; + const modifiedInput = input.modifiedInput; - const binaryDiffInput = this.instantiationService.createInstance(DiffEditorInput, input.getName(), input.getDescription(), originalInput, modifiedInput, true); + const binaryDiffInput = this.instantiationService.createInstance(DiffEditorInput, input.getName(), input.getDescription(), originalInput, modifiedInput, true); - // Forward binary flag to input if supported - const fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); - if (fileEditorInputFactory.isFileEditorInput(originalInput)) { - originalInput.setForceOpenAsBinary(); - } + // Forward binary flag to input if supported + const fileEditorInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileEditorInputFactory(); + if (fileEditorInputFactory.isFileEditorInput(originalInput)) { + originalInput.setForceOpenAsBinary(); + } - if (fileEditorInputFactory.isFileEditorInput(modifiedInput)) { - modifiedInput.setForceOpenAsBinary(); - } + if (fileEditorInputFactory.isFileEditorInput(modifiedInput)) { + modifiedInput.setForceOpenAsBinary(); + } - // Make sure to not steal away the currently active group - // because we are triggering another openEditor() call - // and do not control the initial intent that resulted - // in us now opening as binary. - const preservingOptions: IEditorOptions = { + // Replace this editor with the binary one + this.editorService.replaceEditors([{ + editor: input, + replacement: binaryDiffInput, + options: { + ...options, + // Make sure to not steal away the currently active group + // because we are triggering another openEditor() call + // and do not control the initial intent that resulted + // in us now opening as binary. activation: EditorActivation.PRESERVE, pinned: this.group?.isPinned(input), sticky: this.group?.isSticky(input) - }; - - if (options) { - options.overwrite(preservingOptions); - } else { - options = EditorOptions.create(preservingOptions); } - - // Replace this editor with the binary one - this.editorService.replaceEditors([{ editor: input, replacement: binaryDiffInput, options }], this.group || ACTIVE_GROUP); - - return true; - } - - return false; + }], this.group || ACTIVE_GROUP); } protected override computeConfiguration(configuration: IEditorConfiguration): ICodeEditorOptions { @@ -255,8 +260,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan protected override getConfigurationOverrides(): ICodeEditorOptions { const options: IDiffEditorOptions = super.getConfigurationOverrides(); - options.readOnly = this.input instanceof DiffEditorInput && this.input.modifiedInput.isReadonly(); - options.originalEditable = this.input instanceof DiffEditorInput && !this.input.originalInput.isReadonly(); + options.readOnly = this.input instanceof DiffEditorInput && this.input.modifiedInput.hasCapability(EditorInputCapabilities.Readonly); + options.originalEditable = this.input instanceof DiffEditorInput && !this.input.originalInput.hasCapability(EditorInputCapabilities.Readonly); options.lineDecorationsWidth = '2ch'; return options; @@ -276,6 +281,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan override clearInput(): void { + // Clear input listener + this.inputListener.clear(); + // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/textEditor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/textEditor.ts index bf11b188d4f1..eea7340bdb92 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -7,10 +7,12 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { distinct, deepClone } from 'vs/base/common/objects'; import { Event } from 'vs/base/common/event'; -import { isObject, assertIsDefined, withNullAsUndefined, isFunction } from 'vs/base/common/types'; +import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, IEditorOpenContext, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorMemento, ITextEditorPane, IEditorCloseEvent, IEditorInput, IEditorOpenContext, EditorResourceAccessor, SideBySideEditor, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorViewState, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; @@ -19,7 +21,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -27,6 +29,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IExtUri } from 'vs/base/common/resources'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export interface IEditorConfiguration { editor: object; @@ -44,15 +47,11 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa private editorControl: IEditor | undefined; private editorContainer: HTMLElement | undefined; private hasPendingConfigurationChange: boolean | undefined; - private lastAppliedEditorOptions?: IEditorOptions; + private lastAppliedEditorOptions?: ICodeEditorOptions; private editorMemento: IEditorMemento; private readonly groupListener = this._register(new MutableDisposable()); - private _instantiationService: IInstantiationService; - protected get instantiationService(): IInstantiationService { return this._instantiationService; } - protected set instantiationService(value: IInstantiationService) { this._instantiationService = value; } - override get scopedContextKeyService(): IContextKeyService | undefined { return isCodeEditor(this.editorControl) ? this.editorControl.invokeWithinContext(accessor => accessor.get(IContextKeyService)) : undefined; } @@ -60,7 +59,7 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa constructor( id: string, @ITelemetryService telemetryService: ITelemetryService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService protected instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService, @IThemeService themeService: IThemeService, @@ -69,8 +68,6 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa ) { super(id, telemetryService, themeService, storageService); - this._instantiationService = instantiationService; - this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => { @@ -105,10 +102,10 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa } } - protected computeConfiguration(configuration: IEditorConfiguration): IEditorOptions { + protected computeConfiguration(configuration: IEditorConfiguration): ICodeEditorOptions { // Specific editor options always overwrite user configuration - const editorConfiguration: IEditorOptions = isObject(configuration.editor) ? deepClone(configuration.editor) : Object.create(null); + const editorConfiguration: ICodeEditorOptions = isObject(configuration.editor) ? deepClone(configuration.editor) : Object.create(null); Object.assign(editorConfiguration, this.getConfigurationOverrides()); // ARIA label @@ -121,12 +118,12 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } - protected getConfigurationOverrides(): IEditorOptions { + protected getConfigurationOverrides(): ICodeEditorOptions { return { overviewRulerLanes: 3, lineNumbersMinChars: 3, fixedOverflowWidgets: true, - readOnly: this.input?.isReadonly(), + readOnly: this.input?.hasCapability(EditorInputCapabilities.Readonly), // render problems even in readonly editors // https://github.com/microsoft/vscode/issues/89057 renderValidationDecorations: 'on' @@ -153,13 +150,13 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa * * The passed in configuration object should be passed to the editor control when creating it. */ - protected createEditorControl(parent: HTMLElement, configuration: IEditorOptions): IEditor { + protected createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { // Use a getter for the instantiation service since some subclasses might use scoped instantiation services return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: EditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); // Update editor options after having set the input. We do this because there can be @@ -171,11 +168,9 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa editorContainer.setAttribute('aria-label', this.computeAriaLabel()); } - override setOptions(options: EditorOptions | undefined): void { - const textOptions = options as TextEditorOptions; - if (textOptions && isFunction(textOptions.apply)) { - const textEditor = assertIsDefined(this.getControl()); - textOptions.apply(textEditor, ScrollType.Smooth); + override setOptions(options: ITextEditorOptions | undefined): void { + if (options) { + applyTextEditorOptions(options, assertIsDefined(this.getControl()), ScrollType.Smooth); } } diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 5105d227a4af..562945e369ca 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { assertIsDefined, isFunction, withNullAsUndefined } from 'vs/base/common/types'; +import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor, getCodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; -import { TextEditorOptions, EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { AbstractTextResourceEditorInput, TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -23,8 +25,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ModelConstants } from 'vs/editor/common/model'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * An editor implementation that is capable of showing the contents of resource inputs. Uses @@ -53,7 +56,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return localize('textEditor', "Text Editor"); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); @@ -77,11 +80,10 @@ export class AbstractTextResourceEditor extends BaseTextEditor { const textEditorModel = resolvedModel.textEditorModel; textEditor.setModel(textEditorModel); - // Apply Options from TextOptions + // Apply options to editor if any let optionsGotApplied = false; - const textOptions = options; - if (textOptions && isFunction(textOptions.apply)) { - optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); + if (options) { + optionsGotApplied = applyTextEditorOptions(options, textEditor, ScrollType.Immediate); } // Otherwise restore View State unless disabled via settings @@ -97,8 +99,8 @@ export class AbstractTextResourceEditor extends BaseTextEditor { textEditor.updateOptions({ readOnly: resolvedModel.isReadonly() }); } - private restoreTextResourceEditorViewState(editor: EditorInput, control: IEditor) { - if (editor instanceof UntitledTextEditorInput || editor instanceof ResourceEditorInput) { + private restoreTextResourceEditorViewState(editor: AbstractTextResourceEditorInput, control: IEditor) { + if (editor instanceof UntitledTextEditorInput || editor instanceof TextResourceEditorInput) { const viewState = this.loadTextEditorViewState(editor.resource); if (viewState) { control.restoreViewState(viewState); @@ -144,7 +146,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { } private saveTextResourceEditorViewState(input: EditorInput | undefined): void { - if (!(input instanceof UntitledTextEditorInput) && !(input instanceof ResourceEditorInput)) { + if (!(input instanceof UntitledTextEditorInput) && !(input instanceof TextResourceEditorInput)) { return; // only enabled for untitled and resource inputs } @@ -180,7 +182,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService); } - protected override createEditorControl(parent: HTMLElement, configuration: IEditorOptions): IEditor { + protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { const control = super.createEditorControl(parent, configuration); // Install a listener for paste to update this editors diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/titleControl.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/titleControl.ts index e2d0ed9a806c..3de99faa1196 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -4,16 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/titlecontrol'; +import { localize } from 'vs/nls'; import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, SubmenuAction } from 'vs/base/common/actions'; +import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, SubmenuAction, ActionRunner } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -26,18 +25,17 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; -import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillEditorsDragData, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IEditorGroupsAccessor, IEditorGroupTitleHeight, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext } from 'vs/workbench/common/editor'; +import { IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext, EditorsOrder } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types'; import { isFirefox } from 'vs/base/browser/browser'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { isPromiseCanceledError } from 'vs/base/common/errors'; export interface IToolbarActions { @@ -59,6 +57,19 @@ export interface ITitleControlDimensions { available: Dimension; } +export class EditorCommandsContextActionRunner extends ActionRunner { + + constructor( + private context: IEditorCommandsContext + ) { + super(); + } + + override run(action: IAction): Promise { + return super.run(action, this.context); + } +} + export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -254,11 +265,16 @@ export abstract class TitleControl extends Themable { e.dataTransfer.effectAllowed = 'copyMove'; } - // If tabs are disabled, treat dragging as if an editor tab was dragged + // Drag all tabs of the group if tabs are enabled let hasDataTransfer = false; - if (!this.accessor.partOptions.showTabs) { + if (this.accessor.partOptions.showTabs) { + hasDataTransfer = this.doFillResourceDataTransfers(this.group.getEditors(EditorsOrder.SEQUENTIAL), e); + } + + // Otherwise only drag the active editor + else { if (this.group.activeEditor) { - hasDataTransfer = this.doFillResourceDataTransfers(this.group.activeEditor, e); + hasDataTransfer = this.doFillResourceDataTransfers([this.group.activeEditor], e); } } @@ -284,29 +300,14 @@ export abstract class TitleControl extends Themable { })); } - protected doFillResourceDataTransfers(editor: IEditorInput, e: DragEvent): boolean { - const resource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); - if (!resource) { - return false; - } + protected doFillResourceDataTransfers(editors: readonly IEditorInput[], e: DragEvent): boolean { + if (editors.length) { + this.instantiationService.invokeFunction(fillEditorsDragData, editors.map(editor => ({ editor, groupId: this.group.id })), e); - const editorOptions: ITextEditorOptions = { - viewState: (() => { - if (this.group.activeEditor === editor) { - const activeControl = this.group.activeEditorPane?.getControl(); - if (isCodeEditor(activeControl)) { - return withNullAsUndefined(activeControl.saveViewState()); - } - } - - return undefined; - })(), - sticky: this.group.isSticky(editor) - }; - - this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], () => editorOptions, e); + return true; + } - return true; + return false; } protected onContextMenu(editor: IEditorInput, e: Event, node: HTMLElement): void { @@ -380,6 +381,8 @@ export abstract class TitleControl extends Themable { abstract updateEditorLabel(editor: IEditorInput): void; + abstract updateEditorCapabilities(editor: IEditorInput): void; + abstract updateEditorLabels(): void; abstract updateEditorDirty(editor: IEditorInput): void; diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts b/lib/vscode/src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts new file mode 100644 index 000000000000..a08710ea570a --- /dev/null +++ b/lib/vscode/src/vs/workbench/browser/parts/editor/workspaceTrustRequiredEditor.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/workspacetrusteditor'; +import { localize } from 'vs/nls'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Dimension, size, clearNode, append, addDisposableListener, EventType, $ } from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { EditorDescriptor } from 'vs/workbench/browser/editor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; + +export class WorkspaceTrustRequiredEditor extends EditorPane { + + static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; + static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); + static readonly DESCRIPTOR = EditorDescriptor.create(WorkspaceTrustRequiredEditor, WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL); + + private container: HTMLElement | undefined; + private scrollbar: DomScrollableElement | undefined; + private inputDisposable = this._register(new MutableDisposable()); + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @ICommandService private readonly commandService: ICommandService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IStorageService storageService: IStorageService + ) { + super(WorkspaceTrustRequiredEditor.ID, telemetryService, themeService, storageService); + } + + override getTitle(): string { + return WorkspaceTrustRequiredEditor.LABEL; + } + + protected createEditor(parent: HTMLElement): void { + + // Container + this.container = document.createElement('div'); + this.container.className = 'monaco-workspace-trust-required-editor'; + this.container.style.outline = 'none'; + this.container.tabIndex = 0; // enable focus support from the editor part (do not remove) + + // Custom Scrollbars + this.scrollbar = this._register(new DomScrollableElement(this.container, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto })); + parent.appendChild(this.scrollbar.getDomNode()); + } + + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + // Check for cancellation + if (token.isCancellationRequested) { + return; + } + + // Render Input + this.inputDisposable.value = this.renderInput(); + } + + private renderInput(): IDisposable { + const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); + + clearNode(container); + + const disposables = new DisposableStore(); + + const label = container.appendChild(document.createElement('p')); + label.textContent = isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? + localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : + localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."); + + const link = append(label, $('a.embedded-link')); + link.setAttribute('role', 'button'); + link.textContent = localize('manageTrust', "Manage Workspace Trust"); + + disposables.add(addDisposableListener(link, EventType.CLICK, async () => { + await this.commandService.executeCommand('workbench.trust.manage'); + })); + + scrollbar.scanDomNode(); + + return disposables; + } + + override clearInput(): void { + if (this.container) { + clearNode(this.container); + } + + this.inputDisposable.clear(); + + super.clearInput(); + } + + layout(dimension: Dimension): void { + + // Pass on to Container + const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); + size(container, dimension.width, dimension.height); + scrollbar.scanDomNode(); + } + + override focus(): void { + const container = assertIsDefined(this.container); + + container.focus(); + } + + override dispose(): void { + this.container?.remove(); + + super.dispose(); + } +} diff --git a/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index a7af76e8d881..27fc5cdb9634 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -122,7 +122,7 @@ export class ConfigureNotificationAction extends Action { constructor( id: string, label: string, - public readonly configurationActions: readonly IAction[] + readonly configurationActions: readonly IAction[] ) { super(id, label, ThemeIcon.asClassName(configureIcon)); } diff --git a/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 228208bd1805..063cf045525c 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -71,6 +71,7 @@ export class NotificationsStatus extends Disposable { // Show the bell with a dot if there are unread or in-progress notifications const statusProperties: IStatusbarEntry = { + name: localize('status.notifications', "Notifications"), text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-dot)' : '$(bell)'}`, ariaLabel: localize('status.notifications', "Notifications"), command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, @@ -82,7 +83,6 @@ export class NotificationsStatus extends Disposable { this.notificationsCenterStatusItem = this.statusbarService.addEntry( statusProperties, 'status.notifications', - localize('status.notifications', "Notifications"), StatusbarAlignment.RIGHT, -Number.MAX_VALUE /* towards the far end of the right hand side */ ); @@ -180,9 +180,12 @@ export class NotificationsStatus extends Disposable { let statusMessageEntry: IStatusbarEntryAccessor; let showHandle: any = setTimeout(() => { statusMessageEntry = this.statusbarService.addEntry( - { text: message, ariaLabel: message }, + { + name: localize('status.message', "Status Message"), + text: message, + ariaLabel: message + }, 'status.message', - localize('status.message', "Status Message"), StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */ ); diff --git a/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 1a316c061d04..fc96d2263efb 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; -import { clearNode, addDisposableListener, EventType, EventHelper, $ } from 'vs/base/browser/dom'; +import { clearNode, addDisposableListener, EventType, EventHelper, $, EventLike } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -24,6 +24,9 @@ import { Severity } from 'vs/platform/notification/common/notification'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; +import { DomEmitter } from 'vs/base/browser/event'; +import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch'; +import { Event } from 'vs/base/common/event'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -151,10 +154,17 @@ class NotificationMessageRenderer { const anchor = $('a', { href: node.href, title: title, }, node.label); if (actionHandler) { - actionHandler.toDispose.add(addDisposableListener(anchor, EventType.CLICK, e => { + const onPointer = (e: EventLike) => { EventHelper.stop(e, true); actionHandler.callback(node.href); - })); + }; + + const onClick = actionHandler.toDispose.add(new DomEmitter(anchor, 'click')).event; + + actionHandler.toDispose.add(Gesture.addTarget(anchor)); + const onTap = actionHandler.toDispose.add(new DomEmitter(anchor, GestureEventType.Tap)).event; + + Event.any(onClick, onTap)(onPointer, null, actionHandler.toDispose); } messageContainer.appendChild(anchor); diff --git a/lib/vscode/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/lib/vscode/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 20fe201cd090..f2dbea288b41 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; +import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator, toAction } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, ThemeColor } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -42,13 +42,38 @@ import { syncing } from 'vs/platform/theme/common/iconRegistry'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { hash } from 'vs/base/common/hash'; +import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; + +interface IStatusbarEntryPriority { + + /** + * The main priority of the entry that + * defines the order of appearance. + * + * May not be unique across all entries. + */ + primary: number; + + /** + * The secondary priority of the entry + * is used in case the main priority + * matches another one's priority. + * + * Should be unique across all entries. + */ + secondary: number; +} interface IPendingStatusbarEntry { id: string; - name: string; entry: IStatusbarEntry; alignment: StatusbarAlignment; - priority: number; + priority: IStatusbarEntryPriority; accessor?: IStatusbarEntryAccessor; } @@ -56,7 +81,7 @@ interface IStatusbarViewModelEntry { id: string; name: string; alignment: StatusbarAlignment; - priority: number; + priority: IStatusbarEntryPriority; container: HTMLElement; labelContainer: HTMLElement; } @@ -294,14 +319,16 @@ class StatusbarViewModel extends Disposable { this._entries.sort((entryA, entryB) => { if (entryA.alignment === entryB.alignment) { - if (entryA.priority !== entryB.priority) { - return entryB.priority - entryA.priority; // higher priority towards the left + if (entryA.priority.primary !== entryB.priority.primary) { + return entryB.priority.primary - entryA.priority.primary; // higher priority towards the left (primary) } - const indexA = mapEntryToIndex.get(entryA); - const indexB = mapEntryToIndex.get(entryB); + if (entryA.priority.secondary !== entryB.priority.secondary) { + return entryB.priority.secondary - entryA.priority.secondary; // higher priority towards the left (secondary) + } - return indexA! - indexB!; // otherwise maintain stable order (both values known to be in map) + // otherwise maintain stable order (both values known to be in map) + return mapEntryToIndex.get(entryA)! - mapEntryToIndex.get(entryB)!; } if (entryA.alignment === StatusbarAlignment.LEFT) { @@ -404,6 +431,8 @@ export class StatusbarPart extends Part implements IStatusbarService { private leftItemsContainer: HTMLElement | undefined; private rightItemsContainer: HTMLElement | undefined; + private hoverDelegate: IHoverDelegate; + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -412,30 +441,42 @@ export class StatusbarPart extends Part implements IStatusbarService { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); this.registerListeners(); + + this.hoverDelegate = { + showHover: (options: IHoverDelegateOptions) => hoverService.showHover(options), + delay: configurationService.getValue('workbench.hover.delay'), + placement: 'element' + }; } private registerListeners(): void { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } - addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number = 0): IStatusbarEntryAccessor { + addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, primaryPriority = 0): IStatusbarEntryAccessor { + const priority: IStatusbarEntryPriority = { + primary: primaryPriority, + secondary: hash(id) // derive from identifier to accomplish uniqueness + }; // As long as we have not been created into a container yet, record all entries // that are pending so that they can get created at a later point if (!this.element) { - return this.doAddPendingEntry(entry, id, name, alignment, priority); + return this.doAddPendingEntry(entry, id, alignment, priority); } // Otherwise add to view - return this.doAddEntry(entry, id, name, alignment, priority); + return this.doAddEntry(entry, id, alignment, priority); } - private doAddPendingEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number): IStatusbarEntryAccessor { - const pendingEntry: IPendingStatusbarEntry = { entry, id, name, alignment, priority }; + private doAddPendingEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { + const pendingEntry: IPendingStatusbarEntry = { entry, id, alignment, priority }; this.pendingEntries.push(pendingEntry); const accessor: IStatusbarEntryAccessor = { @@ -459,17 +500,25 @@ export class StatusbarPart extends Part implements IStatusbarService { return accessor; } - private doAddEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number): IStatusbarEntryAccessor { + private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { // Create item const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined])); - const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry); + const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate); // Append to parent this.appendOneStatusbarEntry(itemContainer, alignment, priority); // Add to view model - const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer, labelContainer: item.labelContainer }; + const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry { + readonly id = id; + readonly alignment = alignment; + readonly priority = priority; + readonly container = itemContainer; + readonly labelContainer = item.labelContainer; + + get name() { return item.name; } + }; const viewModelEntryDispose = this.viewModel.add(viewModelEntry); return { @@ -553,7 +602,7 @@ export class StatusbarPart extends Part implements IStatusbarService { while (this.pendingEntries.length) { const pending = this.pendingEntries.shift(); if (pending) { - pending.accessor = this.addEntry(pending.entry, pending.id, pending.name, pending.alignment, pending.priority); + pending.accessor = this.addEntry(pending.entry, pending.id, pending.alignment, pending.priority.primary); } } } @@ -571,7 +620,7 @@ export class StatusbarPart extends Part implements IStatusbarService { }); } - private appendOneStatusbarEntry(itemContainer: HTMLElement, alignment: StatusbarAlignment, priority: number): void { + private appendOneStatusbarEntry(itemContainer: HTMLElement, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): void { const entries = this.viewModel.getEntries(alignment); if (alignment === StatusbarAlignment.RIGHT) { @@ -584,9 +633,20 @@ export class StatusbarPart extends Part implements IStatusbarService { // and then insert the item before that one let appended = false; for (const entry of entries) { + + // pick a priority that ideally is not the same + // by falling back to secondary priority + let existingEntryPriority = entry.priority.primary; + let newEntryPriority = priority.primary; + if (existingEntryPriority === newEntryPriority) { + existingEntryPriority = entry.priority.secondary; + newEntryPriority = priority.secondary; + } + + // insert according to priority if ( - alignment === StatusbarAlignment.LEFT && entry.priority < priority || - alignment === StatusbarAlignment.RIGHT && entry.priority > priority // reversing due to flex: row-reverse + alignment === StatusbarAlignment.LEFT && existingEntryPriority < newEntryPriority || + alignment === StatusbarAlignment.RIGHT && existingEntryPriority > newEntryPriority // reversing due to flex: row-reverse ) { target.insertBefore(itemContainer, entry.container); appended = true; @@ -625,7 +685,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const actions: IAction[] = []; // Provide an action to hide the status bar at last - actions.push(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, localize('hideStatusBar', "Hide Status Bar"))); + actions.push(toAction({ id: ToggleStatusbarVisibilityAction.ID, label: localize('hideStatusBar', "Hide Status Bar"), run: () => this.instantiationService.invokeFunction(accessor => new ToggleStatusbarVisibilityAction().run(accessor)) })); actions.push(new Separator()); // Show an entry per known status entry @@ -776,7 +836,10 @@ class StatusbarEntryItem extends Disposable { readonly labelContainer: HTMLElement; private readonly label: StatusBarCodiconLabel; + private customHover: IDisposable | undefined; + private entry: IStatusbarEntry | undefined = undefined; + get name(): string { return assertIsDefined(this.entry).name; } private readonly foregroundListener = this._register(new MutableDisposable()); private readonly backgroundListener = this._register(new MutableDisposable()); @@ -787,6 +850,7 @@ class StatusbarEntryItem extends Disposable { constructor( private container: HTMLElement, entry: IStatusbarEntry, + private readonly customHoverDelegate: IHoverDelegate, @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -835,8 +899,15 @@ class StatusbarEntryItem extends Disposable { } // Update: Tooltip (on the container, because label can be disabled) - if (!this.entry || entry.tooltip !== this.entry.tooltip) { - if (entry.tooltip) { + if (!this.entry || !isEqualTooltip(this.entry, entry)) { + if (this.customHover) { + this.customHover.dispose(); + this.customHover = undefined; + } + if (isMarkdownString(entry.tooltip)) { + this.container.removeAttribute('title'); + this.customHover = setupCustomHover(this.customHoverDelegate, this.container, { markdown: entry.tooltip, markdownNotSupportedFallback: undefined }); + } else if (entry.tooltip) { this.container.title = entry.tooltip; } else { this.container.title = ''; @@ -947,7 +1018,22 @@ class StatusbarEntryItem extends Disposable { dispose(this.backgroundListener); dispose(this.commandMouseListener); dispose(this.commandKeyboardListener); + if (this.customHover) { + this.customHover.dispose(); + } + } +} + +function isEqualTooltip(e1: IStatusbarEntry, e2: IStatusbarEntry) { + const t1 = e1.tooltip; + const t2 = e2.tooltip; + if (t1 === undefined) { + return t2 === undefined; + } + if (isMarkdownString(t1)) { + return isMarkdownString(t2) && markdownStringEqual(t1, t2); } + return t1 === t2; } registerThemingParticipant((theme, collector) => { diff --git a/lib/vscode/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/lib/vscode/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index ac94cca192e5..87dbc5d32048 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IRecentlyOpened, isRecentFolder, IRecent, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { RunOnceScheduler } from 'vs/base/common/async'; import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_BORDER, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; @@ -36,11 +36,93 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { IsMacNativeContext, IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; export type IOpenRecentAction = IAction & { uri: URI, remoteAuthority?: string }; +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarFileMenu, + title: { + value: 'File', + original: 'File', + mnemonicTitle: localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"), + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarEditMenu, + title: { + value: 'Edit', + original: 'Edit', + mnemonicTitle: localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarSelectionMenu, + title: { + value: 'Selection', + original: 'Selection', + mnemonicTitle: localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarViewMenu, + title: { + value: 'View', + original: 'View', + mnemonicTitle: localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarGoMenu, + title: { + value: 'Go', + original: 'Go', + mnemonicTitle: localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go") + }, + order: 5 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarTerminalMenu, + title: { + value: 'Terminal', + original: 'Terminal', + mnemonicTitle: localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal") + }, + order: 7, + when: ContextKeyExpr.has('terminalProcessSupported') +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarHelpMenu, + title: { + value: 'Help', + original: 'Help', + mnemonicTitle: localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") + }, + order: 8 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, { + submenu: MenuId.MenubarPreferencesMenu, + title: { + value: 'Preferences', + original: 'Preferences', + mnemonicTitle: localize('mPreferences', "Preferences") + }, + when: IsMacNativeContext, + order: 9 +}); + export abstract class MenubarControl extends Disposable { protected keys = [ @@ -52,28 +134,10 @@ export abstract class MenubarControl extends Disposable { ]; protected menus: { - 'File': IMenu; - 'Edit': IMenu; - 'Selection': IMenu; - 'View': IMenu; - 'Go': IMenu; - 'Run': IMenu; - 'Terminal': IMenu; - 'Window'?: IMenu; - 'Help': IMenu; [index: string]: IMenu | undefined; - }; - - protected topLevelTitles: { [menu: string]: string } = { - 'File': localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"), - 'Edit': localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit"), - 'Selection': localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"), - 'View': localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"), - 'Go': localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"), - 'Run': localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run"), - 'Terminal': localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), - 'Help': localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") - }; + } = {}; + + protected topLevelTitles: { [menu: string]: string } = {}; protected recentlyOpened: IRecentlyOpened = { files: [], workspaces: [] }; @@ -100,17 +164,30 @@ export abstract class MenubarControl extends Disposable { super(); - this.menus = { - 'File': this._register(this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService)), - 'Edit': this._register(this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService)), - 'Selection': this._register(this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService)), - 'View': this._register(this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService)), - 'Go': this._register(this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService)), - 'Run': this._register(this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService)), - 'Terminal': this._register(this.menuService.createMenu(MenuId.MenubarTerminalMenu, this.contextKeyService)), - 'Help': this._register(this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService)) + const mainMenu = this._register(this.menuService.createMenu(MenuId.MenubarMainMenu, this.contextKeyService)); + const mainMenuDisposables = this._register(new DisposableStore()); + + const setupMenu = () => { + mainMenuDisposables.clear(); + this.menus = {}; + this.topLevelTitles = {}; + + const [, mainMenuActions] = mainMenu.getActions()[0]; + for (const mainMenuAction of mainMenuActions) { + if (mainMenuAction instanceof SubmenuItemAction && typeof mainMenuAction.item.title !== 'string') { + this.menus[mainMenuAction.item.title.original] = mainMenuDisposables.add(this.menuService.createMenu(mainMenuAction.item.submenu, this.contextKeyService)); + this.topLevelTitles[mainMenuAction.item.title.original] = mainMenuAction.item.title.mnemonicTitle ?? mainMenuAction.item.title.value; + } + } }; + setupMenu(); + + mainMenu.onDidChange(() => { + setupMenu(); + this.doUpdateMenubar(true); + }); + this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200)); this.notifyUserOfCustomMenubarAccessibility(); @@ -551,6 +628,7 @@ export class CustomMenubarControl extends MenubarControl { this._onVisibilityChange.fire(visible); } + private reinstallDisposables = this._register(new DisposableStore()); private setupCustomMenubar(firstTime: boolean): void { // If there is no container, we cannot setup the menubar if (!this.container) { @@ -558,14 +636,19 @@ export class CustomMenubarControl extends MenubarControl { } if (firstTime) { - this.menubar = this._register(new MenuBar(this.container, this.getMenuBarOptions())); + // Reset and create new menubar + if (this.menubar) { + this.reinstallDisposables.clear(); + } + + this.menubar = this.reinstallDisposables.add(new MenuBar(this.container, this.getMenuBarOptions())); this.accessibilityService.alwaysUnderlineAccessKeys().then(val => { this.alwaysOnMnemonics = val; this.menubar?.update(this.getMenuBarOptions()); }); - this._register(this.menubar.onFocusStateChange(focused => { + this.reinstallDisposables.add(this.menubar.onFocusStateChange(focused => { this._onFocusStateChange.fire(focused); // When the menubar loses focus, update it to clear any pending updates @@ -575,18 +658,18 @@ export class CustomMenubarControl extends MenubarControl { } })); - this._register(this.menubar.onVisibilityChange(e => this.onDidVisibilityChange(e))); + this.reinstallDisposables.add(this.menubar.onVisibilityChange(e => this.onDidVisibilityChange(e))); // Before we focus the menubar, stop updates to it so that focus-related context keys will work - this._register(addDisposableListener(this.container, EventType.FOCUS_IN, () => { + this.reinstallDisposables.add(addDisposableListener(this.container, EventType.FOCUS_IN, () => { this.focusInsideMenubar = true; })); - this._register(addDisposableListener(this.container, EventType.FOCUS_OUT, () => { + this.reinstallDisposables.add(addDisposableListener(this.container, EventType.FOCUS_OUT, () => { this.focusInsideMenubar = false; })); - this._register(attachMenuStyler(this.menubar, this.themeService)); + this.reinstallDisposables.add(attachMenuStyler(this.menubar, this.themeService)); } else { this.menubar?.update(this.getMenuBarOptions()); } @@ -654,7 +737,7 @@ export class CustomMenubarControl extends MenubarControl { for (const title of Object.keys(this.topLevelTitles)) { const menu = this.menus[title]; if (firstTime && menu) { - this._register(menu.onDidChange(() => { + this.reinstallDisposables.add(menu.onDidChange(() => { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, title); @@ -666,7 +749,7 @@ export class CustomMenubarControl extends MenubarControl { // For the file menu, we need to update if the web nav menu updates as well if (menu === this.menus.File) { - this._register(this.webNavigationMenu.onDidChange(() => { + this.reinstallDisposables.add(this.webNavigationMenu.onDidChange(() => { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, title); diff --git a/lib/vscode/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/lib/vscode/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 8572b7f405fd..d00e11c292c1 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -166,6 +166,7 @@ export class TitlebarPart extends Part implements ITitleService { if (activeEditor) { this.activeEditorListeners.add(activeEditor.onDidChangeDirty(() => this.titleUpdater.schedule())); this.activeEditorListeners.add(activeEditor.onDidChangeLabel(() => this.titleUpdater.schedule())); + this.activeEditorListeners.add(activeEditor.onDidChangeCapabilities(() => this.titleUpdater.schedule())); } } diff --git a/lib/vscode/src/vs/workbench/browser/parts/views/media/views.css b/lib/vscode/src/vs/workbench/browser/parts/views/media/views.css index d82afb63ebdc..09e6c7c8b1f8 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/views/media/views.css +++ b/lib/vscode/src/vs/workbench/browser/parts/views/media/views.css @@ -143,12 +143,17 @@ padding-right: 6px; width: 16px; height: 22px; + display: flex; + align-items: center; + justify-content: center; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { - margin-top: 3px; +/* makes spinning icons square */ +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon.codicon-modifier-spin { + padding-left: 6px; + margin-left: -6px; } .customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { diff --git a/lib/vscode/src/vs/workbench/browser/parts/views/treeView.ts b/lib/vscode/src/vs/workbench/browser/parts/views/treeView.ts index 075cf857ad18..29cb607489a2 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/views/treeView.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/views/treeView.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MenuId, IMenuService, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; +import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -38,7 +38,9 @@ import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMat import { isString } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; +import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { IDragAndDropData } from 'vs/base/browser/dnd'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; @@ -87,6 +89,7 @@ export class TreeViewPane extends ViewPane { if (options.titleDescription !== this.treeView.description) { this.updateTitleDescription(this.treeView.description); } + this.updateTreeVisibility(); } @@ -237,6 +240,13 @@ export class TreeView extends Disposable implements ITreeView { get viewLocation(): ViewContainerLocation { return this.viewDescriptorService.getViewLocationById(this.id)!; } + private _dragAndDropController: ITreeViewDragAndDropController | undefined; + get dragAndDropController(): ITreeViewDragAndDropController | undefined { + return this._dragAndDropController; + } + set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) { + this._dragAndDropController = dnd; + } private _dataProvider: ITreeViewDataProvider | undefined; get dataProvider(): ITreeViewDataProvider | undefined { @@ -503,6 +513,7 @@ export class TreeView extends Disposable implements ITreeView { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; }, multipleSelectionSupport: this.canSelectMany, + dnd: this.dragAndDropController ? this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.dragAndDropController) : undefined, overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } @@ -1183,3 +1194,32 @@ export class CustomTreeView extends TreeView { } } } + +export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { + constructor(private dndController: ITreeViewDragAndDropController, @ILabelService private readonly labelService: ILabelService) { } + + onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true }; + } + + getDragURI(element: ITreeItem): string | null { + return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle; + } + + getDragLabel?(elements: ITreeItem[]): string | undefined { + if (elements.length > 1) { + return String(elements.length); + } + const element = elements[0]; + return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined); + } + + async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): Promise { + if (data instanceof ElementsDragAndDropData) { + const elements = data.elements; + if (targetNode) { + await this.dndController.onDrop(elements, targetNode); + } + } + } +} diff --git a/lib/vscode/src/vs/workbench/browser/parts/views/viewPane.ts b/lib/vscode/src/vs/workbench/browser/parts/views/viewPane.ts index 7b20423c227f..655410652bfd 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/views/viewPane.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; +import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -40,7 +40,7 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; -import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; export interface IViewPaneOptions extends IPaneOptions { id: string; @@ -445,6 +445,10 @@ export abstract class ViewPane extends Pane implements IView { this.scrollableElement.scanDomNode(); } + onDidScrollRoot() { + // noop + } + getProgressIndicator() { if (this.progressBar === undefined) { // Progress bar @@ -575,13 +579,12 @@ export abstract class ViewPane extends Pane implements IView { if (typeof node === 'string') { append(p, document.createTextNode(node)); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(p, link.el); disposables.add(link); - disposables.add(attachLinkStyler(link, this.themeService)); if (precondition && node.href.startsWith('command:')) { - const updateEnablement = () => link.style({ disabled: !this.contextKeyService.contextMatchesRules(precondition) }); + const updateEnablement = () => link.enabled = this.contextKeyService.contextMatchesRules(precondition); updateEnablement(); const keys = new Set(); diff --git a/lib/vscode/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/lib/vscode/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 255e8eab1de7..0ed15426be7b 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/lib/vscode/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -35,7 +35,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -407,6 +407,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { options.orientation = this.orientation; this.paneview = this._register(new PaneView(parent, this.options)); this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane))); + this._register(this.paneview.onDidScroll(_ => this.onDidScrollPane())); this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); this._menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.paneview.element, this.viewContainer)); @@ -1064,6 +1065,12 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return true; } + private onDidScrollPane() { + for (const pane of this.panes) { + pane.onDidScrollRoot(); + } + } + override dispose(): void { super.dispose(); this.paneItems.forEach(i => i.disposable.dispose()); diff --git a/lib/vscode/src/vs/workbench/browser/style.ts b/lib/vscode/src/vs/workbench/browser/style.ts index c83278bda46a..6971905465ea 100644 --- a/lib/vscode/src/vs/workbench/browser/style.ts +++ b/lib/vscode/src/vs/workbench/browser/style.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/style'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground, toolbarHoverBackground, toolbarActiveBackground, toolbarHoverOutline } from 'vs/platform/theme/common/colorRegistry'; +import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground, toolbarHoverBackground, toolbarActiveBackground, toolbarHoverOutline, listFocusHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform'; import { createMetaElement } from 'vs/base/browser/dom'; @@ -61,6 +61,16 @@ registerThemingParticipant((theme, collector) => { `); } + // List highlight w/ focus + const listHighlightFocusForegroundColor = theme.getColor(listFocusHighlightForeground); + if (listHighlightFocusForegroundColor) { + collector.addRule(` + .monaco-workbench .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { + color: ${listHighlightFocusForegroundColor}; + } + `); + } + // Scrollbars const scrollbarShadowColor = theme.getColor(scrollbarShadow); if (scrollbarShadowColor) { diff --git a/lib/vscode/src/vs/workbench/browser/web.main.ts b/lib/vscode/src/vs/workbench/browser/web.main.ts index 61d5dfe34302..5e03179dd48a 100644 --- a/lib/vscode/src/vs/workbench/browser/web.main.ts +++ b/lib/vscode/src/vs/workbench/browser/web.main.ts @@ -39,7 +39,6 @@ import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogger } from 'vs/platform/log/common/fileLog'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; -import { initialize } from 'vs/server/browser/client'; import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; @@ -64,6 +63,8 @@ import { ITimerService } from 'vs/workbench/services/timer/browser/timerService' import { WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { initialize } from './client'; class BrowserMain extends Disposable { @@ -96,7 +97,9 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); + // NOTE@coder: initialize our additions await initialize(services.serviceCollection); + // Window this._register(instantiationService.createInstance(BrowserWindow)); @@ -108,16 +111,22 @@ class BrowserMain extends Disposable { const commandService = accessor.get(ICommandService); const lifecycleService = accessor.get(ILifecycleService); const timerService = accessor.get(ITimerService); + const openerService = accessor.get(IOpenerService); + const productService = accessor.get(IProductService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, env: { + uriScheme: productService.urlProtocol, async retrievePerformanceMarks() { await timerService.whenReady(); return timerService.getPerformanceMarks(); + }, + async openUri(uri: URI): Promise { + return openerService.open(uri, {}); } }, shutdown: () => lifecycleService.shutdown() @@ -178,7 +187,7 @@ class BrowserMain extends Disposable { serviceCollection.set(IFileService, fileService); await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, logService, logsPath); - // IURIIdentityService + // URI Identity const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); @@ -205,7 +214,7 @@ class BrowserMain extends Disposable { ]); // Workspace Trust Service - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, environmentService, storageService, uriIdentityService, configurationService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, storageService, uriIdentityService, environmentService, configurationService, remoteAuthorityResolverService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/lib/vscode/src/vs/workbench/browser/window.ts b/lib/vscode/src/vs/workbench/browser/window.ts index fa5f82cd07af..05b9f806f35b 100644 --- a/lib/vscode/src/vs/workbench/browser/window.ts +++ b/lib/vscode/src/vs/workbench/browser/window.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpener } from 'vs/base/browser/dom'; -import { domEvent } from 'vs/base/browser/event'; +import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpenerWithSuccess, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { DomEmitter } from 'vs/base/browser/event'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -50,7 +50,13 @@ export class BrowserWindow extends Disposable { // Layout const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; - this._register(addDisposableListener(viewport, EventType.RESIZE, () => this.onWindowResize())); + this._register(addDisposableListener(viewport, EventType.RESIZE, () => { + this.onWindowResize(); + if (isIOS) { + // Sometimes the keyboard appearing scrolls the whole workbench out of view, as a workaround scroll back into view #121206 + window.scrollTo(0, 0); + } + })); // Prevent the back/forward gestures in macOS this._register(addDisposableListener(this.layoutService.container, EventType.WHEEL, e => e.preventDefault(), { passive: false })); @@ -74,7 +80,6 @@ export class BrowserWindow extends Disposable { private onWindowResize(): void { this.logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); - this.layoutService.layout(); } @@ -84,8 +89,8 @@ export class BrowserWindow extends Disposable { // when shutdown has happened to not show the dialog e.g. // when navigation takes a longer time. Event.toPromise(Event.any( - Event.once(domEvent(document.body, EventType.KEY_DOWN, true)), - Event.once(domEvent(document.body, EventType.MOUSE_DOWN, true)) + Event.once(new DomEmitter(document.body, EventType.KEY_DOWN, true).event), + Event.once(new DomEmitter(document.body, EventType.MOUSE_DOWN, true).event) )).then(async () => { // Delay the dialog in case the user interacted @@ -139,7 +144,7 @@ export class BrowserWindow extends Disposable { this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { - const opened = windowOpenNoOpener(href); + const opened = windowOpenNoOpenerWithSuccess(href); if (!opened) { const showResult = await this.dialogService.show(Severity.Warning, localize('unableToOpenExternal', "The browser interrupted the opening of a new tab or window. Press 'Open' to open it anyway."), [localize('open', "Open"), localize('learnMore', "Learn More"), localize('cancel', "Cancel")], { cancelId: 2, detail: href }); diff --git a/lib/vscode/src/vs/workbench/browser/workbench.ts b/lib/vscode/src/vs/workbench/browser/workbench.ts index 2074e0c7776a..ffa6372c5302 100644 --- a/lib/vscode/src/vs/workbench/browser/workbench.ts +++ b/lib/vscode/src/vs/workbench/browser/workbench.ts @@ -275,9 +275,7 @@ export class Workbench extends Layout { } private restoreFontInfo(storageService: IStorageService, configurationService: IConfigurationService): void { - - // Restore (native: use storage service, web: use browser specific local storage) - const storedFontInfoRaw = isNative ? storageService.get('editorFontInfo', StorageScope.GLOBAL) : window.localStorage.getItem('vscode.editorFontInfo'); + const storedFontInfoRaw = storageService.get('editorFontInfo', StorageScope.GLOBAL); if (storedFontInfoRaw) { try { const storedFontInfo = JSON.parse(storedFontInfoRaw); @@ -295,17 +293,7 @@ export class Workbench extends Layout { private storeFontInfo(storageService: IStorageService): void { const serializedFontInfo = serializeFontInfo(); if (serializedFontInfo) { - const serializedFontInfoRaw = JSON.stringify(serializedFontInfo); - - // Font info is very specific to the machine the workbench runs - // on. As such, in the web, we prefer to store this info in - // local storage and not global storage because it would not make - // much sense to synchronize to other machines. - if (isNative) { - storageService.store('editorFontInfo', serializedFontInfoRaw, StorageScope.GLOBAL, StorageTarget.MACHINE); - } else { - window.localStorage.setItem('vscode.editorFontInfo', serializedFontInfoRaw); - } + storageService.store('editorFontInfo', JSON.stringify(serializedFontInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); } } @@ -340,6 +328,7 @@ export class Workbench extends Layout { // Create Parts [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, + { id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] }, { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892 { id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } }, diff --git a/lib/vscode/src/vs/workbench/common/editor.ts b/lib/vscode/src/vs/workbench/common/editor.ts index d33a1daabce8..c2ac8757b109 100644 --- a/lib/vscode/src/vs/workbench/common/editor.ts +++ b/lib/vscode/src/vs/workbench/common/editor.ts @@ -4,24 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Event, Emitter } from 'vs/base/common/event'; -import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; +import { Event } from 'vs/base/common/event'; +import { assertIsDefined, isUndefinedOrNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceEditorInput, IResourceEditorInput, EditorActivation, EditorOpenContext, ITextEditorSelection, TextEditorSelectionRevealType, EditorOverride } from 'vs/platform/editor/common/editor'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IEditor, IEditorViewState, IDiffEditor } from 'vs/editor/common/editorCommon'; +import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceEditorInput, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEncodingSupport, IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl, IComposite } from 'vs/workbench/common/composite'; -import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; -import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; +import { coalesce } from 'vs/base/common/arrays'; import { ACTIVE_GROUP, IResourceEditorInputType, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IRange } from 'vs/editor/common/core/range'; import { IExtUri } from 'vs/base/common/resources'; // Static values for editor contributions @@ -68,6 +66,31 @@ export const TEXT_DIFF_EDITOR_ID = 'workbench.editors.textDiffEditor'; */ export const BINARY_DIFF_EDITOR_ID = 'workbench.editors.binaryResourceDiffEditor'; +export interface IEditorDescriptor { + + /** + * The unique type identifier of the editor. All instances + * of the same `IEditorPane` should have the same type + * identifier. + */ + readonly typeId: string; + + /** + * The display name of the editor. + */ + readonly name: string; + + /** + * Instantiates the editor pane using the provided services. + */ + instantiate(instantiationService: IInstantiationService): T; + + /** + * Whether the descriptor is for the provided editor pane. + */ + describes(editorPane: T): boolean; +} + /** * The editor pane is the container for workbench editors. */ @@ -190,7 +213,7 @@ export interface IFileEditorInputFactory { /** * Creates new new editor input capable of showing files. */ - createFileEditorInput(resource: URI, preferredResource: URI | undefined, preferredName: string | undefined, preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; + createFileEditorInput(resource: URI, preferredResource: URI | undefined, preferredName: string | undefined, preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, preferredContents: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; /** * Check if the provided object is a file editor input. @@ -198,22 +221,6 @@ export interface IFileEditorInputFactory { isFileEditorInput(obj: unknown): obj is IFileEditorInput; } -/** - * @deprecated obsolete - * - * TODO@bpasero remove this API and users once the generic backup restorer has been removed - */ -export interface ICustomEditorInputFactory { - /** - * @deprecated obsolete - */ - createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise; - /** - * @deprecated obsolete - */ - canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean; -} - export interface IEditorInputFactoryRegistry { /** @@ -226,20 +233,6 @@ export interface IEditorInputFactoryRegistry { */ getFileEditorInputFactory(): IFileEditorInputFactory; - /** - * Registers the custom editor input factory to use for custom inputs. - * - * @deprecated obsolete - */ - registerCustomEditorInputFactory(scheme: string, factory: ICustomEditorInputFactory): void; - - /** - * Returns the custom editor input factory to use for custom inputs. - * - * @deprecated obsolete - */ - getCustomEditorInputFactory(scheme: string): ICustomEditorInputFactory | undefined; - /** * Registers a editor input serializer for the given editor input to the registry. * An editor input serializer is capable of serializing and deserializing editor @@ -279,10 +272,10 @@ export interface IEditorInputSerializer { * Returns an editor input from the provided serialized form of the editor input. This form matches * the value returned from the serialize() method. */ - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined; + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): IEditorInput | undefined; } -export interface IUntitledTextResourceEditorInput extends IBaseResourceEditorInput { +export interface IUntitledTextResourceEditorInput extends IBaseTextResourceEditorInput { /** * Optional resource. If the resource is not provided a new untitled file is created (e.g. Untitled-1). @@ -291,34 +284,19 @@ export interface IUntitledTextResourceEditorInput extends IBaseResourceEditorInp * the untitled editor. */ readonly resource?: URI; - - /** - * Optional language of the untitled resource. - */ - readonly mode?: string; - - /** - * Optional contents of the untitled resource. - */ - readonly contents?: string; - - /** - * Optional encoding of the untitled resource. - */ - readonly encoding?: string; } export interface IResourceDiffEditorInput extends IBaseResourceEditorInput { /** - * The left hand side URI to open inside a diff editor. + * The left hand side editor to open inside a diff editor. */ - readonly leftResource: URI; + readonly originalInput: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; /** - * The right hand side URI to open inside a diff editor. + * The right hand side editor to open inside a diff editor. */ - readonly rightResource: URI; + readonly modifiedInput: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; } export const enum Verbosity { @@ -393,10 +371,39 @@ export interface IRevertOptions { } export interface IMoveResult { - editor: EditorInput | IResourceEditorInputType; + editor: IEditorInput | IResourceEditorInputType; options?: IEditorOptions; } +export const enum EditorInputCapabilities { + + /** + * Signals no specific capability for the input. + */ + None = 0, + + /** + * Signals that the input is readonly. + */ + Readonly = 1 << 1, + + /** + * Signals that the input is untitled. + */ + Untitled = 1 << 2, + + /** + * Signals that the input can only be shown in one group + * and not be split into multiple groups. + */ + Singleton = 1 << 3, + + /** + * Signals that the input requires workspace trust. + */ + RequiresTrust = 1 << 4, +} + export interface IEditorInput extends IDisposable { /** @@ -415,7 +422,12 @@ export interface IEditorInput extends IDisposable { readonly onDidChangeLabel: Event; /** - * Unique type identifier for this inpput. Every editor input of the + * Triggered when this input changes its capabilities. + */ + readonly onDidChangeCapabilities: Event; + + /** + * Unique type identifier for this input. Every editor input of the * same class should share the same type identifier. The type identifier * is used for example for serialising/deserialising editor inputs * via the serialisers of the `IEditorInputFactoryRegistry`. @@ -435,6 +447,16 @@ export interface IEditorInput extends IDisposable { */ readonly resource: URI | undefined; + /** + * The capabilities of the input. + */ + readonly capabilities: EditorInputCapabilities; + + /** + * Figure out if the input has the provided capability. + */ + hasCapability(capability: EditorInputCapabilities): boolean; + /** * Returns the display name of this input. */ @@ -462,16 +484,6 @@ export interface IEditorInput extends IDisposable { */ resolve(): Promise; - /** - * Returns if this input is readonly or not. - */ - isReadonly(): boolean; - - /** - * Returns if the input is an untitled editor or not. - */ - isUntitled(): boolean; - /** * Returns if this input is dirty or not. */ @@ -523,9 +535,18 @@ export interface IEditorInput extends IDisposable { rename(group: GroupIdentifier, target: URI): IMoveResult | undefined; /** - * Subclasses can set this to false if it does not make sense to split the editor input. + * Returns a copy of the current editor input. Used when we can't just reuse the input */ - canSplit(): boolean; + copy(): IEditorInput; + + /** + * Returns a representation of this typed editor input as untyped + * resource editor input that e.g. can be used to serialize the + * editor input into a form that it can be restored. + * + * May return `undefined` if a untyped representatin is not supported. + */ + asResourceEditorInput(groupId: GroupIdentifier): IBaseResourceEditorInput | undefined; /** * Returns if the other object matches this input. @@ -536,130 +557,6 @@ export interface IEditorInput extends IDisposable { * Returns if this editor is disposed. */ isDisposed(): boolean; - - /** - * Returns a copy of the current editor input. Used when we can't just reuse the input - */ - copy(): IEditorInput; -} - -/** - * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. - * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. - */ -export abstract class EditorInput extends Disposable implements IEditorInput { - - protected readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - protected readonly _onDidChangeLabel = this._register(new Emitter()); - readonly onDidChangeLabel = this._onDidChangeLabel.event; - - private readonly _onWillDispose = this._register(new Emitter()); - readonly onWillDispose = this._onWillDispose.event; - - private disposed: boolean = false; - - abstract get typeId(): string; - - abstract get resource(): URI | undefined; - - getName(): string { - return `Editor ${this.typeId}`; - } - - getDescription(verbosity?: Verbosity): string | undefined { - return undefined; - } - - getTitle(verbosity?: Verbosity): string { - return this.getName(); - } - - getAriaLabel(): string { - return this.getTitle(Verbosity.SHORT); - } - - /** - * Returns the preferred editor for this input. A list of candidate editors is passed in that whee registered - * for the input. This allows subclasses to decide late which editor to use for the input on a case by case basis. - */ - getPreferredEditorId(candidates: string[]): string | undefined { - return firstOrDefault(candidates); - } - - /** - * Returns a descriptor suitable for telemetry events. - * - * Subclasses should extend if they can contribute. - */ - getTelemetryDescriptor(): { [key: string]: unknown } { - /* __GDPR__FRAGMENT__ - "EditorTelemetryDescriptor" : { - "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - return { typeId: this.typeId }; - } - - isReadonly(): boolean { - return true; - } - - isUntitled(): boolean { - return false; - } - - isDirty(): boolean { - return false; - } - - isSaving(): boolean { - return false; - } - - async resolve(): Promise { - return null; - } - - async save(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this; - } - - async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this; - } - - async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { } - - rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { - return undefined; - } - - canSplit(): boolean { - return true; - } - - matches(otherInput: unknown): boolean { - return this === otherInput; - } - - copy(): IEditorInput { - return this; - } - - isDisposed(): boolean { - return this.disposed; - } - - override dispose(): void { - if (!this.disposed) { - this.disposed = true; - this._onWillDispose.fire(); - } - - super.dispose(); - } } export interface IEditorInputWithPreferredResource { @@ -685,9 +582,34 @@ export interface IEditorInputWithPreferredResource { } export function isEditorInputWithPreferredResource(obj: unknown): obj is IEditorInputWithPreferredResource { - const editorInputWithPreferredResource = obj as IEditorInputWithPreferredResource; + const editorInputWithPreferredResource = obj as IEditorInputWithPreferredResource | undefined; + if (!editorInputWithPreferredResource) { + return false; + } - return editorInputWithPreferredResource && !!editorInputWithPreferredResource.preferredResource; + return URI.isUri(editorInputWithPreferredResource.preferredResource); +} + +export interface ISideBySideEditorInput extends IEditorInput { + + /** + * The primary editor input is shown on the right hand side. + */ + primary: IEditorInput; + + /** + * The secondary editor input is shown on the left hand side. + */ + secondary: IEditorInput; +} + +function isSideBySideEditorInput(obj: unknown): obj is ISideBySideEditorInput { + const sideBySideEditorInput = obj as ISideBySideEditorInput | undefined; + if (!sideBySideEditorInput) { + return false; + } + + return !!sideBySideEditorInput.primary && !!sideBySideEditorInput.secondary; } /** @@ -737,6 +659,11 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeS */ setPreferredMode(mode: string): void; + /** + * Sets the preferred contents to use for this file input. + */ + setPreferredContents(contents: string): void; + /** * Forces this file input to open as binary instead of text. */ @@ -748,173 +675,9 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeS isResolved(): boolean; } -/** - * Side by side editor inputs that have a primary and secondary side. - */ -export class SideBySideEditorInput extends EditorInput { - - static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; - - override get typeId(): string { - return SideBySideEditorInput.ID; - } - - constructor( - protected readonly name: string | undefined, - protected readonly description: string | undefined, - private readonly _secondary: EditorInput, - private readonly _primary: EditorInput - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - - // When the primary or secondary input gets disposed, dispose this diff editor input - const onceSecondaryDisposed = Event.once(this.secondary.onWillDispose); - this._register(onceSecondaryDisposed(() => { - if (!this.isDisposed()) { - this.dispose(); - } - })); - - const oncePrimaryDisposed = Event.once(this.primary.onWillDispose); - this._register(oncePrimaryDisposed(() => { - if (!this.isDisposed()) { - this.dispose(); - } - })); - - // Reemit some events from the primary side to the outside - this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire())); - } - - /** - * Use `EditorResourceAccessor` utility method to access the resources - * of both sides of the diff editor. - */ - get resource(): URI | undefined { - return undefined; - } - - get primary(): EditorInput { - return this._primary; - } - - get secondary(): EditorInput { - return this._secondary; - } - - override getName(): string { - if (!this.name) { - return localize('sideBySideLabels', "{0} - {1}", this._secondary.getName(), this._primary.getName()); - } - - return this.name; - } - - override getDescription(): string | undefined { - return this.description; - } - - override isReadonly(): boolean { - return this.primary.isReadonly(); - } - - override isUntitled(): boolean { - return this.primary.isUntitled(); - } - - override isDirty(): boolean { - return this.primary.isDirty(); - } - - override isSaving(): boolean { - return this.primary.isSaving(); - } - - override save(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this.primary.save(group, options); - } - - override saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this.primary.saveAs(group, options); - } - - override revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - return this.primary.revert(group, options); - } - - override getTelemetryDescriptor(): { [key: string]: unknown } { - const descriptor = this.primary.getTelemetryDescriptor(); - - return Object.assign(descriptor, super.getTelemetryDescriptor()); - } - - override matches(otherInput: unknown): boolean { - if (otherInput === this) { - return true; - } - - if (otherInput instanceof SideBySideEditorInput) { - return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary); - } - - return false; - } -} - -/** - * The editor model is the heavyweight counterpart of editor input. Depending on the editor input, it - * resolves from a file system retrieve content and may allow for saving it back or reverting it. - * Editor models are typically cached for some while because they are expensive to construct. - */ -export class EditorModel extends Disposable implements IEditorModel { - - private readonly _onWillDispose = this._register(new Emitter()); - readonly onWillDispose = this._onWillDispose.event; - - private disposed = false; - private resolved = false; - - /** - * Causes this model to resolve returning a promise when loading is completed. - */ - async resolve(): Promise { - this.resolved = true; - } - - /** - * Returns whether this model was loaded or not. - */ - isResolved(): boolean { - return this.resolved; - } - - /** - * Find out if this model has been disposed. - */ - isDisposed(): boolean { - return this.disposed; - } - - /** - * Subclasses should implement to free resources that have been claimed through loading. - */ - override dispose(): void { - this.disposed = true; - this._onWillDispose.fire(); - - super.dispose(); - } -} - export interface IEditorInputWithOptions { editor: IEditorInput; - options?: IEditorOptions | ITextEditorOptions; + options?: IEditorOptions; } export interface IEditorInputWithOptionsAndGroup extends IEditorInputWithOptions { @@ -927,289 +690,6 @@ export function isEditorInputWithOptions(obj: unknown): obj is IEditorInputWithO return !!editorInputWithOptions && !!editorInputWithOptions.editor; } -/** - * The editor options is the base class of options that can be passed in when opening an editor. - */ -export class EditorOptions implements IEditorOptions { - - /** - * Helper to create EditorOptions inline. - */ - static create(settings: IEditorOptions): EditorOptions { - const options = new EditorOptions(); - options.overwrite(settings); - - return options; - } - - /** - * Tells the editor to not receive keyboard focus when the editor is being opened. - * - * Will also not activate the group the editor opens in unless the group is already - * the active one. This behaviour can be overridden via the `activation` option. - */ - preserveFocus: boolean | undefined; - - /** - * This option is only relevant if an editor is opened into a group that is not active - * already and allows to control if the inactive group should become active, restored - * or preserved. - * - * By default, the editor group will become active unless `preserveFocus` or `inactive` - * is specified. - */ - activation: EditorActivation | undefined; - - /** - * Tells the editor to reload the editor input in the editor even if it is identical to the one - * already showing. By default, the editor will not reload the input if it is identical to the - * one showing. - */ - forceReload: boolean | undefined; - - /** - * Will reveal the editor if it is already opened and visible in any of the opened editor groups. - */ - revealIfVisible: boolean | undefined; - - /** - * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. - */ - revealIfOpened: boolean | undefined; - - /** - * An editor that is pinned remains in the editor stack even when another editor is being opened. - * An editor that is not pinned will always get replaced by another editor that is not pinned. - */ - pinned: boolean | undefined; - - /** - * An editor that is sticky moves to the beginning of the editors list within the group and will remain - * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. - */ - sticky: boolean | undefined; - - /** - * The index in the document stack where to insert the editor into when opening. - */ - index: number | undefined; - - /** - * An active editor that is opened will show its contents directly. Set to true to open an editor - * in the background without loading its contents. - * - * Will also not activate the group the editor opens in unless the group is already - * the active one. This behaviour can be overridden via the `activation` option. - */ - inactive: boolean | undefined; - - /** - * Will not show an error in case opening the editor fails and thus allows to show a custom error - * message as needed. By default, an error will be presented as notification if opening was not possible. - */ - ignoreError: boolean | undefined; - - /** - * Allows to override the editor that should be used to display the input: - * - `undefined`: let the editor decide for itself - * - `string`: specific override by id - * - `EditorOverride`: specific override handling - */ - override: string | EditorOverride | undefined; - - /** - * A optional hint to signal in which context the editor opens. - * - * If configured to be `EditorOpenContext.USER`, this hint can be - * used in various places to control the experience. For example, - * if the editor to open fails with an error, a notification could - * inform about this in a modal dialog. If the editor opened through - * some background task, the notification would show in the background, - * not as a modal dialog. - */ - context: EditorOpenContext | undefined; - - /** - * Overwrites option values from the provided bag. - */ - overwrite(options: IEditorOptions): EditorOptions { - if (typeof options.forceReload === 'boolean') { - this.forceReload = options.forceReload; - } - - if (typeof options.revealIfVisible === 'boolean') { - this.revealIfVisible = options.revealIfVisible; - } - - if (typeof options.revealIfOpened === 'boolean') { - this.revealIfOpened = options.revealIfOpened; - } - - if (typeof options.preserveFocus === 'boolean') { - this.preserveFocus = options.preserveFocus; - } - - if (typeof options.activation === 'number') { - this.activation = options.activation; - } - - if (typeof options.pinned === 'boolean') { - this.pinned = options.pinned; - } - - if (typeof options.sticky === 'boolean') { - this.sticky = options.sticky; - } - - if (typeof options.inactive === 'boolean') { - this.inactive = options.inactive; - } - - if (typeof options.ignoreError === 'boolean') { - this.ignoreError = options.ignoreError; - } - - if (typeof options.index === 'number') { - this.index = options.index; - } - - if (options.override !== undefined) { - this.override = options.override; - } - - if (typeof options.context === 'number') { - this.context = options.context; - } - - return this; - } -} - -/** - * Base Text Editor Options. - */ -export class TextEditorOptions extends EditorOptions implements ITextEditorOptions { - - /** - * Text editor selection. - */ - selection: ITextEditorSelection | undefined; - - /** - * Text editor view state. - */ - editorViewState: IEditorViewState | undefined; - - /** - * Option to control the text editor selection reveal type. - */ - selectionRevealType: TextEditorSelectionRevealType | undefined; - - static from(input?: IBaseResourceEditorInput): TextEditorOptions | undefined { - if (!input?.options) { - return undefined; - } - - return TextEditorOptions.create(input.options); - } - - /** - * Helper to convert options bag to real class - */ - static override create(options: ITextEditorOptions = Object.create(null)): TextEditorOptions { - const textEditorOptions = new TextEditorOptions(); - textEditorOptions.overwrite(options); - - return textEditorOptions; - } - - /** - * Overwrites option values from the provided bag. - */ - override overwrite(options: ITextEditorOptions): TextEditorOptions { - super.overwrite(options); - - if (options.selection) { - this.selection = { - startLineNumber: options.selection.startLineNumber, - startColumn: options.selection.startColumn, - endLineNumber: options.selection.endLineNumber ?? options.selection.startLineNumber, - endColumn: options.selection.endColumn ?? options.selection.startColumn - }; - } - - if (options.viewState) { - this.editorViewState = options.viewState as IEditorViewState; - } - - if (typeof options.selectionRevealType !== 'undefined') { - this.selectionRevealType = options.selectionRevealType; - } - - return this; - } - - /** - * Returns if this options object has objects defined for the editor. - */ - hasOptionsDefined(): boolean { - return !!this.editorViewState || !!this.selectionRevealType || !!this.selection; - } - - /** - * Create a TextEditorOptions inline to be used when the editor is opening. - */ - static fromEditor(editor: IEditor, settings?: IEditorOptions): TextEditorOptions { - const options = TextEditorOptions.create(settings); - - // View state - options.editorViewState = withNullAsUndefined(editor.saveViewState()); - - return options; - } - - /** - * Apply the view state or selection to the given editor. - * - * @return if something was applied - */ - apply(editor: IEditor, scrollType: ScrollType): boolean { - let gotApplied = false; - - // First try viewstate - if (this.editorViewState) { - editor.restoreViewState(this.editorViewState); - gotApplied = true; - } - - // Otherwise check for selection - else if (this.selection) { - const range: IRange = { - startLineNumber: this.selection.startLineNumber, - startColumn: this.selection.startColumn, - endLineNumber: this.selection.endLineNumber ?? this.selection.startLineNumber, - endColumn: this.selection.endColumn ?? this.selection.startColumn - }; - - editor.setSelection(range); - - if (this.selectionRevealType === TextEditorSelectionRevealType.NearTop) { - editor.revealRangeNearTop(range, scrollType); - } else if (this.selectionRevealType === TextEditorSelectionRevealType.NearTopIfOutsideViewport) { - editor.revealRangeNearTopIfOutsideViewport(range, scrollType); - } else if (this.selectionRevealType === TextEditorSelectionRevealType.CenterIfOutsideViewport) { - editor.revealRangeInCenterIfOutsideViewport(range, scrollType); - } else { - editor.revealRangeInCenter(range, scrollType); - } - - gotApplied = true; - } - - return gotApplied; - } -} - /** * Context passed into `EditorPane#setInput` to give additional * context information around why the editor was opened. @@ -1231,6 +711,15 @@ export interface IEditorIdentifier { editor: IEditorInput; } +export function isEditorIdentifier(thing: unknown): thing is IEditorIdentifier { + const identifier = thing as IEditorIdentifier | undefined; + if (!identifier) { + return false; + } + + return typeof identifier.groupId === 'number' && !isUndefinedOrNull(identifier.editor); +} + /** * The editor commands context is used for editor commands (e.g. in the editor title) * and we must ensure that the context is serializable because it potentially travels @@ -1241,19 +730,6 @@ export interface IEditorCommandsContext { editorIndex?: number; } -export class EditorCommandsContextActionRunner extends ActionRunner { - - constructor( - private context: IEditorCommandsContext - ) { - super(); - } - - override run(action: IAction): Promise { - return super.run(action, this.context); - } -} - export interface IEditorCloseEvent extends IEditorIdentifier { replaced: boolean; index: number; @@ -1264,6 +740,8 @@ export interface IEditorMoveEvent extends IEditorIdentifier { target: GroupIdentifier; } +export interface IEditorOpenEvent extends IEditorIdentifier { } + export type GroupIdentifier = number; export interface IWorkbenchEditorConfiguration { @@ -1366,7 +844,7 @@ class EditorResourceAccessorImpl { } // Optionally support side-by-side editors - if (options?.supportSideBySide && editor instanceof SideBySideEditorInput) { + if (options?.supportSideBySide && isSideBySideEditorInput(editor)) { if (options?.supportSideBySide === SideBySideEditor.BOTH) { return { primary: this.getOriginalUri(editor.primary, { filterByScheme: options.filterByScheme }), @@ -1408,7 +886,7 @@ class EditorResourceAccessorImpl { } // Optionally support side-by-side editors - if (options?.supportSideBySide && editor instanceof SideBySideEditorInput) { + if (options?.supportSideBySide && isSideBySideEditorInput(editor)) { if (options?.supportSideBySide === SideBySideEditor.BOTH) { return { primary: this.getCanonicalUri(editor.primary, { filterByScheme: options.filterByScheme }), @@ -1475,7 +953,6 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private instantiationService: IInstantiationService | undefined; private fileEditorInputFactory: IFileEditorInputFactory | undefined; - private readonly customEditorInputFactoryInstances: Map = new Map(); private readonly editorInputSerializerConstructors: Map> = new Map(); private readonly editorInputSerializerInstances: Map = new Map(); @@ -1529,14 +1006,6 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { getEditorInputSerializer(arg1: string | IEditorInput): IEditorInputSerializer | undefined { return this.editorInputSerializerInstances.get(typeof arg1 === 'string' ? arg1 : arg1.typeId); } - - registerCustomEditorInputFactory(scheme: string, factory: ICustomEditorInputFactory): void { - this.customEditorInputFactoryInstances.set(scheme, factory); - } - - getCustomEditorInputFactory(scheme: string): ICustomEditorInputFactory | undefined { - return this.customEditorInputFactoryInstances.get(scheme); - } } Registry.add(EditorExtensions.EditorInputFactories, new EditorInputFactoryRegistry()); diff --git a/lib/vscode/src/vs/workbench/common/editor/binaryEditorModel.ts b/lib/vscode/src/vs/workbench/common/editor/binaryEditorModel.ts index 98bbf9360ba3..7e4e6a7ae46b 100644 --- a/lib/vscode/src/vs/workbench/common/editor/binaryEditorModel.ts +++ b/lib/vscode/src/vs/workbench/common/editor/binaryEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { MIME_BINARY } from 'vs/base/common/mime'; @@ -12,20 +12,18 @@ import { MIME_BINARY } from 'vs/base/common/mime'; * An editor model that just represents a resource that can be loaded. */ export class BinaryEditorModel extends EditorModel { + + private readonly mime = MIME_BINARY; + private size: number | undefined; private etag: string | undefined; - private readonly mime: string; constructor( - public readonly resource: URI, + readonly resource: URI, private readonly name: string, @IFileService private readonly fileService: IFileService ) { super(); - - this.resource = resource; - this.name = name; - this.mime = MIME_BINARY; } /** diff --git a/lib/vscode/src/vs/workbench/common/editor/diffEditorInput.ts b/lib/vscode/src/vs/workbench/common/editor/diffEditorInput.ts index 93c0ed19cd2b..83aab2ce17e8 100644 --- a/lib/vscode/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/lib/vscode/src/vs/workbench/common/editor/diffEditorInput.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, BINARY_DIFF_EDITOR_ID, Verbosity } from 'vs/workbench/common/editor'; +import { AbstractSideBySideEditorInputSerializer, SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { TEXT_DIFF_EDITOR_ID, BINARY_DIFF_EDITOR_ID, Verbosity, IEditorDescriptor, IEditorPane, GroupIdentifier, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; @@ -14,6 +17,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -32,8 +36,8 @@ export class DiffEditorInput extends SideBySideEditorInput { constructor( name: string | undefined, description: string | undefined, - public readonly originalInput: EditorInput, - public readonly modifiedInput: EditorInput, + readonly originalInput: EditorInput, + readonly modifiedInput: EditorInput, private readonly forceOpenAsBinary: boolean | undefined, @ILabelService private readonly labelService: ILabelService, @IFileService private readonly fileService: IFileService @@ -58,7 +62,7 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.name; } - override getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { if (typeof this.description !== 'string') { // Pass the description of the modified side in case both original @@ -104,8 +108,12 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.cachedModel; } - override getPreferredEditorId(candidates: string[]): string { - return this.forceOpenAsBinary ? BINARY_DIFF_EDITOR_ID : TEXT_DIFF_EDITOR_ID; + override prefersEditor>(editors: T[]): T | undefined { + if (this.forceOpenAsBinary) { + return editors.find(editor => editor.typeId === BINARY_DIFF_EDITOR_ID); + } + + return editors.find(editor => editor.typeId === TEXT_DIFF_EDITOR_ID); } private async createModel(): Promise { @@ -125,6 +133,22 @@ export class DiffEditorInput extends SideBySideEditorInput { return new DiffEditorModel(withNullAsUndefined(originalEditorModel), withNullAsUndefined(modifiedEditorModel)); } + override asResourceEditorInput(groupId: GroupIdentifier): IResourceDiffEditorInput | undefined { + const originalResourceEditorInput = this.secondary.asResourceEditorInput(groupId); + const modifiedResourceEditorInput = this.primary.asResourceEditorInput(groupId); + + if (originalResourceEditorInput && modifiedResourceEditorInput) { + return { + label: this.name, + description: this.description, + originalInput: originalResourceEditorInput, + modifiedInput: modifiedResourceEditorInput + }; + } + + return undefined; + } + override matches(otherInput: unknown): boolean { if (!super.matches(otherInput)) { return false; @@ -146,3 +170,10 @@ export class DiffEditorInput extends SideBySideEditorInput { super.dispose(); } } + +export class DiffEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { + + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + return instantiationService.createInstance(DiffEditorInput, name, description, secondaryInput, primaryInput, undefined); + } +} diff --git a/lib/vscode/src/vs/workbench/common/editor/diffEditorModel.ts b/lib/vscode/src/vs/workbench/common/editor/diffEditorModel.ts index 9de37bf7e02a..cddd3bbb0fb5 100644 --- a/lib/vscode/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/lib/vscode/src/vs/workbench/common/editor/diffEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IEditorModel } from 'vs/platform/editor/common/editor'; /** diff --git a/lib/vscode/src/vs/workbench/common/editor/editorGroupModel.ts b/lib/vscode/src/vs/workbench/common/editor/editorGroupModel.ts index 85076d88d213..ec7c3b4ca397 100644 --- a/lib/vscode/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/lib/vscode/src/vs/workbench/common/editor/editorGroupModel.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, IEditorInput, EditorsOrder, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, IEditorInput, EditorsOrder, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -80,7 +82,10 @@ export class EditorGroupModel extends Disposable { readonly onDidChangeEditorDirty = this._onDidChangeEditorDirty.event; private readonly _onDidChangeEditorLabel = this._register(new Emitter()); - readonly onDidEditorLabelChange = this._onDidChangeEditorLabel.event; + readonly onDidChangeEditorLabel = this._onDidChangeEditorLabel.event; + + private readonly _onDidChangeEditorCapabilities = this._register(new Emitter()); + readonly onDidChangeEditorCapabilities = this._onDidChangeEditorCapabilities.event; private readonly _onDidMoveEditor = this._register(new Emitter()); readonly onDidMoveEditor = this._onDidMoveEditor.event; @@ -331,6 +336,11 @@ export class EditorGroupModel extends Disposable { this._onDidChangeEditorLabel.fire(editor); })); + // Re-Emit capability changes + listeners.add(editor.onDidChangeCapabilities(() => { + this._onDidChangeEditorCapabilities.fire(editor); + })); + // Clean up dispose listeners once the editor gets closed listeners.add(this.onDidCloseEditor(event => { if (event.editor.matches(editor)) { @@ -810,8 +820,9 @@ export class EditorGroupModel extends Disposable { const editorSerializer = registry.getEditorInputSerializer(e.id); if (editorSerializer) { - editor = editorSerializer.deserialize(this.instantiationService, e.value); - if (editor) { + const deserializedEditor = editorSerializer.deserialize(this.instantiationService, e.value); + if (deserializedEditor instanceof EditorInput) { + editor = deserializedEditor; this.registerEditorListeners(editor); } } diff --git a/lib/vscode/src/vs/workbench/common/editor/editorInput.ts b/lib/vscode/src/vs/workbench/common/editor/editorInput.ts new file mode 100644 index 000000000000..82250013fff8 --- /dev/null +++ b/lib/vscode/src/vs/workbench/common/editor/editorInput.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IBaseResourceEditorInput, IEditorModel } from 'vs/platform/editor/common/editor'; +import { firstOrDefault } from 'vs/base/common/arrays'; +import { IEditorInput, EditorInputCapabilities, Verbosity, GroupIdentifier, ISaveOptions, IRevertOptions, IMoveResult, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; + +/** + * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. + * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. + */ +export abstract class EditorInput extends Disposable implements IEditorInput { + + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + protected readonly _onDidChangeLabel = this._register(new Emitter()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; + + protected readonly _onDidChangeCapabilities = this._register(new Emitter()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + private disposed: boolean = false; + + abstract get typeId(): string; + + abstract get resource(): URI | undefined; + + get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly; + } + + hasCapability(capability: EditorInputCapabilities): boolean { + if (capability === EditorInputCapabilities.None) { + return this.capabilities === EditorInputCapabilities.None; + } + + return (this.capabilities & capability) !== 0; + } + + getName(): string { + return `Editor ${this.typeId}`; + } + + getDescription(verbosity?: Verbosity): string | undefined { + return undefined; + } + + getTitle(verbosity?: Verbosity): string { + return this.getName(); + } + + getAriaLabel(): string { + return this.getTitle(Verbosity.SHORT); + } + + /** + * Returns a descriptor suitable for telemetry events. + * + * Subclasses should extend if they can contribute. + */ + getTelemetryDescriptor(): { [key: string]: unknown } { + /* __GDPR__FRAGMENT__ + "EditorTelemetryDescriptor" : { + "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + return { typeId: this.typeId }; + } + + isDirty(): boolean { + return false; + } + + isSaving(): boolean { + return false; + } + + async resolve(): Promise { + return null; + } + + async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this; + } + + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { } + + rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + return undefined; + } + + copy(): IEditorInput { + return this; + } + + matches(otherInput: unknown): boolean { + return this === otherInput; + } + + /** + * If a input was registered onto multiple editors, this method + * will be asked to return the preferred one to use. + * + * @param editors a list of editor descriptors that are candidates + * for the editor input to open in. + */ + prefersEditor>(editors: T[]): T | undefined { + return firstOrDefault(editors); + } + + asResourceEditorInput(groupId: GroupIdentifier): IBaseResourceEditorInput | undefined { + return undefined; + } + + isDisposed(): boolean { + return this.disposed; + } + + override dispose(): void { + if (!this.disposed) { + this.disposed = true; + this._onWillDispose.fire(); + } + + super.dispose(); + } +} diff --git a/lib/vscode/src/vs/workbench/common/editor/editorModel.ts b/lib/vscode/src/vs/workbench/common/editor/editorModel.ts new file mode 100644 index 000000000000..ae5ba0d1cc34 --- /dev/null +++ b/lib/vscode/src/vs/workbench/common/editor/editorModel.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; + +/** + * The editor model is the heavyweight counterpart of editor input. Depending on the editor input, it + * resolves from a file system retrieve content and may allow for saving it back or reverting it. + * Editor models are typically cached for some while because they are expensive to construct. + */ +export class EditorModel extends Disposable implements IEditorModel { + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + private disposed = false; + private resolved = false; + + /** + * Causes this model to resolve returning a promise when loading is completed. + */ + async resolve(): Promise { + this.resolved = true; + } + + /** + * Returns whether this model was loaded or not. + */ + isResolved(): boolean { + return this.resolved; + } + + /** + * Find out if this model has been disposed. + */ + isDisposed(): boolean { + return this.disposed; + } + + /** + * Subclasses should implement to free resources that have been claimed through loading. + */ + override dispose(): void { + this.disposed = true; + this._onWillDispose.fire(); + + super.dispose(); + } +} diff --git a/lib/vscode/src/vs/workbench/common/editor/editorOptions.ts b/lib/vscode/src/vs/workbench/common/editor/editorOptions.ts new file mode 100644 index 000000000000..a9113b39f742 --- /dev/null +++ b/lib/vscode/src/vs/workbench/common/editor/editorOptions.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange } from 'vs/editor/common/core/range'; +import { IEditor, IEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; + +export function applyTextEditorOptions(options: ITextEditorOptions, editor: IEditor, scrollType: ScrollType): boolean { + + // First try viewstate + if (options.viewState) { + editor.restoreViewState(options.viewState as IEditorViewState); + + return true; + } + + // Otherwise check for selection + else if (options.selection) { + const range: IRange = { + startLineNumber: options.selection.startLineNumber, + startColumn: options.selection.startColumn, + endLineNumber: options.selection.endLineNumber ?? options.selection.startLineNumber, + endColumn: options.selection.endColumn ?? options.selection.startColumn + }; + + editor.setSelection(range); + + if (options.selectionRevealType === TextEditorSelectionRevealType.NearTop) { + editor.revealRangeNearTop(range, scrollType); + } else if (options.selectionRevealType === TextEditorSelectionRevealType.NearTopIfOutsideViewport) { + editor.revealRangeNearTopIfOutsideViewport(range, scrollType); + } else if (options.selectionRevealType === TextEditorSelectionRevealType.CenterIfOutsideViewport) { + editor.revealRangeInCenterIfOutsideViewport(range, scrollType); + } else { + editor.revealRangeInCenter(range, scrollType); + } + + return true; + } + + return false; +} diff --git a/lib/vscode/src/vs/workbench/common/editor/resourceEditorInput.ts b/lib/vscode/src/vs/workbench/common/editor/resourceEditorInput.ts index e897355fbea3..c0564f712efc 100644 --- a/lib/vscode/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/lib/vscode/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -3,132 +3,197 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { Verbosity, IEditorInputWithPreferredResource, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { URI } from 'vs/base/common/uri'; -import { IReference } from 'vs/base/common/lifecycle'; -import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; -import { isEqual } from 'vs/base/common/resources'; +import { dirname, isEqual } from 'vs/base/common/resources'; /** - * A read-only text editor input whos contents are made of the provided resource that points to an existing - * code editor model. + * The base class for all editor inputs that open resources. */ -export class ResourceEditorInput extends AbstractTextResourceEditorInput implements IModeSupport { +export abstract class AbstractResourceEditorInput extends EditorInput implements IEditorInputWithPreferredResource { - static readonly ID: string = 'workbench.editors.resourceEditorInput'; + override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; - override get typeId(): string { - return ResourceEditorInput.ID; + if (this.fileService.canHandleResource(this.resource)) { + if (this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + capabilities |= EditorInputCapabilities.Untitled; + } + + return capabilities; } - private cachedModel: ResourceEditorModel | undefined = undefined; - private modelReference: Promise> | undefined = undefined; + private _preferredResource: URI; + get preferredResource(): URI { return this._preferredResource; } constructor( - resource: URI, - private name: string | undefined, - private description: string | undefined, - private preferredMode: string | undefined, - @ITextModelService private readonly textModelResolverService: ITextModelService, - @ITextFileService textFileService: ITextFileService, - @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService + readonly resource: URI, + preferredResource: URI | undefined, + @ILabelService protected readonly labelService: ILabelService, + @IFileService protected readonly fileService: IFileService ) { - super(resource, undefined, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); - } + super(); + + this._preferredResource = preferredResource || resource; - override getName(): string { - return this.name || super.getName(); + this.registerListeners(); } - setName(name: string): void { - if (this.name !== name) { - this.name = name; + private registerListeners(): void { - this._onDidChangeLabel.fire(); + // Clear our labels on certain label related events + this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + } + + private onLabelEvent(scheme: string): void { + if (scheme === this._preferredResource.scheme) { + this.updateLabel(); } } - override getDescription(): string | undefined { - return this.description; + private updateLabel(): void { + + // Clear any cached labels from before + this._name = undefined; + this._shortDescription = undefined; + this._mediumDescription = undefined; + this._longDescription = undefined; + this._shortTitle = undefined; + this._mediumTitle = undefined; + this._longTitle = undefined; + + // Trigger recompute of label + this._onDidChangeLabel.fire(); } - setDescription(description: string): void { - if (this.description !== description) { - this.description = description; + setPreferredResource(preferredResource: URI): void { + if (!isEqual(preferredResource, this._preferredResource)) { + this._preferredResource = preferredResource; - this._onDidChangeLabel.fire(); + this.updateLabel(); } } - setMode(mode: string): void { - this.setPreferredMode(mode); + private _name: string | undefined = undefined; + override getName(skipDecorate?: boolean): string { + if (typeof this._name !== 'string') { + this._name = this.labelService.getUriBasenameLabel(this._preferredResource); + } + + return skipDecorate ? this._name : this.decorateLabel(this._name); + } - if (this.cachedModel) { - this.cachedModel.setMode(mode); + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortDescription; + case Verbosity.LONG: + return this.longDescription; + case Verbosity.MEDIUM: + default: + return this.mediumDescription; } } - setPreferredMode(mode: string): void { - this.preferredMode = mode; + private _shortDescription: string | undefined = undefined; + private get shortDescription(): string { + if (typeof this._shortDescription !== 'string') { + this._shortDescription = this.labelService.getUriBasenameLabel(dirname(this._preferredResource)); + } + + return this._shortDescription; } - override async resolve(): Promise { - if (!this.modelReference) { - this.modelReference = this.textModelResolverService.createModelReference(this.resource); + private _mediumDescription: string | undefined = undefined; + private get mediumDescription(): string { + if (typeof this._mediumDescription !== 'string') { + this._mediumDescription = this.labelService.getUriLabel(dirname(this._preferredResource), { relative: true }); } - const ref = await this.modelReference; + return this._mediumDescription; + } - // Ensure the resolved model is of expected type - const model = ref.object; - if (!(model instanceof ResourceEditorModel)) { - ref.dispose(); - this.modelReference = undefined; + private _longDescription: string | undefined = undefined; + private get longDescription(): string { + if (typeof this._longDescription !== 'string') { + this._longDescription = this.labelService.getUriLabel(dirname(this._preferredResource)); + } - throw new Error(`Unexpected model for ResourceEditorInput: ${this.resource}`); + return this._longDescription; + } + + private _shortTitle: string | undefined = undefined; + private get shortTitle(): string { + if (typeof this._shortTitle !== 'string') { + this._shortTitle = this.getName(true /* skip decorations */); } - this.cachedModel = model; + return this._shortTitle; + } - // Set mode if we have a preferred mode configured - if (this.preferredMode) { - model.setMode(this.preferredMode); + private _mediumTitle: string | undefined = undefined; + private get mediumTitle(): string { + if (typeof this._mediumTitle !== 'string') { + this._mediumTitle = this.labelService.getUriLabel(this._preferredResource, { relative: true }); } - return model; + return this._mediumTitle; } - override matches(otherInput: unknown): boolean { - if (otherInput === this) { - return true; + private _longTitle: string | undefined = undefined; + private get longTitle(): string { + if (typeof this._longTitle !== 'string') { + this._longTitle = this.labelService.getUriLabel(this._preferredResource); } - if (otherInput instanceof ResourceEditorInput) { - return isEqual(otherInput.resource, this.resource); + return this._longTitle; + } + + override getTitle(verbosity?: Verbosity): string { + switch (verbosity) { + case Verbosity.SHORT: + return this.decorateLabel(this.shortTitle); + case Verbosity.LONG: + return this.decorateLabel(this.longTitle); + default: + case Verbosity.MEDIUM: + return this.decorateLabel(this.mediumTitle); } + } + + private decorateLabel(label: string): string { + const readonly = this.hasCapability(EditorInputCapabilities.Readonly); + const orphaned = this.isOrphaned(); + return decorateFileEditorLabel(label, { orphaned, readonly }); + } + + isOrphaned(): boolean { return false; } +} - override dispose(): void { - if (this.modelReference) { - this.modelReference.then(ref => ref.dispose()); - this.modelReference = undefined; - } +export function decorateFileEditorLabel(label: string, state: { orphaned: boolean, readonly: boolean }): string { + if (state.orphaned && state.readonly) { + return localize('orphanedReadonlyFile', "{0} (deleted, read-only)", label); + } - this.cachedModel = undefined; + if (state.orphaned) { + return localize('orphanedFile', "{0} (deleted)", label); + } - super.dispose(); + if (state.readonly) { + return localize('readonlyFile', "{0} (read-only)", label); } + + return label; } diff --git a/lib/vscode/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/lib/vscode/src/vs/workbench/common/editor/sideBySideEditorInput.ts new file mode 100644 index 000000000000..2eafa406afd3 --- /dev/null +++ b/lib/vscode/src/vs/workbench/common/editor/sideBySideEditorInput.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IEditorInput, EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorInputFactoryRegistry, IEditorInputSerializer, ISideBySideEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; + +/** + * Side by side editor inputs that have a primary and secondary side. + */ +export class SideBySideEditorInput extends EditorInput implements ISideBySideEditorInput { + + static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; + + override get typeId(): string { + return SideBySideEditorInput.ID; + } + + override get capabilities(): EditorInputCapabilities { + + // Use primary capabilities as main capabilities + let capabilities = this._primary.capabilities; + + // Trust: should be considered for both sides + if (this._secondary.hasCapability(EditorInputCapabilities.RequiresTrust)) { + capabilities |= EditorInputCapabilities.RequiresTrust; + } + + // Singleton: should be considered for both sides + if (this._secondary.hasCapability(EditorInputCapabilities.Singleton)) { + capabilities |= EditorInputCapabilities.Singleton; + } + + return capabilities; + } + + get resource(): URI | undefined { + return undefined; // use `EditorResourceAccessor` to obtain one side's resource + } + + get primary(): EditorInput { + return this._primary; + } + + get secondary(): EditorInput { + return this._secondary; + } + + constructor( + protected readonly name: string | undefined, + protected readonly description: string | undefined, + private readonly _secondary: EditorInput, + private readonly _primary: EditorInput + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // When the primary or secondary input gets disposed, dispose this diff editor input + const onceSecondaryDisposed = Event.once(this.secondary.onWillDispose); + this._register(onceSecondaryDisposed(() => { + if (!this.isDisposed()) { + this.dispose(); + } + })); + + const oncePrimaryDisposed = Event.once(this.primary.onWillDispose); + this._register(oncePrimaryDisposed(() => { + if (!this.isDisposed()) { + this.dispose(); + } + })); + + // Re-emit some events from the primary side to the outside + this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire())); + + // Re-emit some events from both sides to the outside + this._register(this.primary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire())); + this._register(this.secondary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire())); + } + + override getName(): string { + if (!this.name) { + return localize('sideBySideLabels', "{0} - {1}", this._secondary.getName(), this._primary.getName()); + } + + return this.name; + } + + override getDescription(): string | undefined { + return this.description; + } + + override getTelemetryDescriptor(): { [key: string]: unknown } { + const descriptor = this.primary.getTelemetryDescriptor(); + + return { ...descriptor, ...super.getTelemetryDescriptor() }; + } + + override isDirty(): boolean { + return this.primary.isDirty(); + } + + override isSaving(): boolean { + return this.primary.isSaving(); + } + + override save(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this.primary.save(group, options); + } + + override saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + return this.primary.saveAs(group, options); + } + + override revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + return this.primary.revert(group, options); + } + + override matches(otherInput: unknown): boolean { + if (super.matches(otherInput)) { + return true; + } + + if (otherInput instanceof SideBySideEditorInput) { + return this.primary.matches(otherInput.primary) && this.secondary.matches(otherInput.secondary); + } + + return false; + } +} + +// Register SideBySide/DiffEditor Input Serializer +interface ISerializedSideBySideEditorInput { + name: string; + description: string | undefined; + + primarySerialized: string; + secondarySerialized: string; + + primaryTypeId: string; + secondaryTypeId: string; +} + +export abstract class AbstractSideBySideEditorInputSerializer implements IEditorInputSerializer { + + private getInputSerializers(secondaryEditorInputTypeId: string, primaryEditorInputTypeId: string): [IEditorInputSerializer | undefined, IEditorInputSerializer | undefined] { + const registry = Registry.as(EditorExtensions.EditorInputFactories); + + return [registry.getEditorInputSerializer(secondaryEditorInputTypeId), registry.getEditorInputSerializer(primaryEditorInputTypeId)]; + } + + canSerialize(editorInput: EditorInput): boolean { + const input = editorInput as SideBySideEditorInput | DiffEditorInput; + + if (input.primary && input.secondary) { + const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); + + return !!(secondaryInputSerializer?.canSerialize(input.secondary) && primaryInputSerializer?.canSerialize(input.primary)); + } + + return false; + } + + serialize(editorInput: EditorInput): string | undefined { + const input = editorInput as SideBySideEditorInput; + + if (input.primary && input.secondary) { + const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(input.secondary.typeId, input.primary.typeId); + if (primaryInputSerializer && secondaryInputSerializer) { + const primarySerialized = primaryInputSerializer.serialize(input.primary); + const secondarySerialized = secondaryInputSerializer.serialize(input.secondary); + + if (primarySerialized && secondarySerialized) { + const serializedEditorInput: ISerializedSideBySideEditorInput = { + name: input.getName(), + description: input.getDescription(), + primarySerialized: primarySerialized, + secondarySerialized: secondarySerialized, + primaryTypeId: input.primary.typeId, + secondaryTypeId: input.secondary.typeId + }; + + return JSON.stringify(serializedEditorInput); + } + } + } + + return undefined; + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { + const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput); + + const [secondaryInputSerializer, primaryInputSerializer] = this.getInputSerializers(deserialized.secondaryTypeId, deserialized.primaryTypeId); + if (primaryInputSerializer && secondaryInputSerializer) { + const primaryInput = primaryInputSerializer.deserialize(instantiationService, deserialized.primarySerialized); + const secondaryInput = secondaryInputSerializer.deserialize(instantiationService, deserialized.secondarySerialized); + + if (primaryInput instanceof EditorInput && secondaryInput instanceof EditorInput) { + return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput); + } + } + + return undefined; + } + + protected abstract createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput; +} + +export class SideBySideEditorInputSerializer extends AbstractSideBySideEditorInputSerializer { + + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + return new SideBySideEditorInput(name, description, secondaryInput, primaryInput); + } +} diff --git a/lib/vscode/src/vs/workbench/common/editor/textEditorModel.ts b/lib/vscode/src/vs/workbench/common/editor/textEditorModel.ts index e7bd48733ce7..bff2d6e78f57 100644 --- a/lib/vscode/src/vs/workbench/common/editor/textEditorModel.ts +++ b/lib/vscode/src/vs/workbench/common/editor/textEditorModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ITextModel, ITextBufferFactory, ITextSnapshot, ModelConstants } from 'vs/editor/common/model'; -import { EditorModel } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; diff --git a/lib/vscode/src/vs/workbench/common/editor/textResourceEditorInput.ts b/lib/vscode/src/vs/workbench/common/editor/textResourceEditorInput.ts index 26d5f3fff5ea..487c19fe3913 100644 --- a/lib/vscode/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/lib/vscode/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -3,237 +3,224 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, Verbosity, GroupIdentifier, IEditorInput, IRevertOptions, IEditorInputWithPreferredResource } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IEditorInput, IRevertOptions, isTextEditorPane } from 'vs/workbench/common/editor'; +import { AbstractResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { URI } from 'vs/base/common/uri'; -import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ITextFileSaveOptions, IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { Schemas } from 'vs/base/common/network'; -import { dirname, isEqual } from 'vs/base/common/resources'; +import { isEqual } from 'vs/base/common/resources'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; +import { IReference } from 'vs/base/common/lifecycle'; +import { IEditorViewState } from 'vs/editor/common/editorCommon'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; /** * The base class for all editor inputs that open in text editors. */ -export abstract class AbstractTextResourceEditorInput extends EditorInput implements IEditorInputWithPreferredResource { - - private _preferredResource: URI; - get preferredResource(): URI { return this._preferredResource; } +export abstract class AbstractTextResourceEditorInput extends AbstractResourceEditorInput { constructor( - public readonly resource: URI, + resource: URI, preferredResource: URI | undefined, @IEditorService protected readonly editorService: IEditorService, - @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @ITextFileService protected readonly textFileService: ITextFileService, - @ILabelService protected readonly labelService: ILabelService, - @IFileService protected readonly fileService: IFileService, - @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService ) { - super(); - - this._preferredResource = preferredResource || resource; - - this.registerListeners(); + super(resource, preferredResource, labelService, fileService); } - protected registerListeners(): void { - - // Clear label memoizer on certain events that have impact - this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); - } + override save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - private onLabelEvent(scheme: string): void { - if (scheme === this._preferredResource.scheme) { - this.updateLabel(); + // If this is neither an `untitled` resource, nor a resource + // we can handle with the file service, we can only "Save As..." + if (this.resource.scheme !== Schemas.untitled && !this.fileService.canHandleResource(this.resource)) { + return this.saveAs(group, options); } - } - - private updateLabel(): void { - // Clear any cached labels from before - this._name = undefined; - this._shortDescription = undefined; - this._mediumDescription = undefined; - this._longDescription = undefined; - this._shortTitle = undefined; - this._mediumTitle = undefined; - this._longTitle = undefined; + // Normal save + return this.doSave(options, false); + } - // Trigger recompute of label - this._onDidChangeLabel.fire(); + override saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSave(options, true); } - setPreferredResource(preferredResource: URI): void { - if (!isEqual(preferredResource, this._preferredResource)) { - this._preferredResource = preferredResource; + private async doSave(options: ITextFileSaveOptions | undefined, saveAs: boolean): Promise { - this.updateLabel(); + // Save / Save As + let target: URI | undefined; + if (saveAs) { + target = await this.textFileService.saveAs(this.resource, undefined, { ...options, suggestedTarget: this.preferredResource }); + } else { + target = await this.textFileService.save(this.resource, options); } - } - private _name: string | undefined = undefined; - override getName(): string { - if (typeof this._name !== 'string') { - this._name = this.labelService.getUriBasenameLabel(this._preferredResource); + if (!target) { + return undefined; // save cancelled } - return this._name; + // If this save operation results in a new editor, either + // because it was saved to disk (e.g. from untitled) or + // through an explicit "Save As", make sure to replace it. + if ( + target.scheme !== this.resource.scheme || + (saveAs && !isEqual(target, this.preferredResource)) + ) { + return this.editorService.createEditorInput({ resource: target }); + } + + return this; } - override getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { - switch (verbosity) { - case Verbosity.SHORT: - return this.shortDescription; - case Verbosity.LONG: - return this.longDescription; - case Verbosity.MEDIUM: - default: - return this.mediumDescription; - } + override async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + await this.textFileService.revert(this.resource, options); } - private _shortDescription: string | undefined = undefined; - private get shortDescription(): string { - if (typeof this._shortDescription !== 'string') { - this._shortDescription = this.labelService.getUriBasenameLabel(dirname(this._preferredResource)); + protected getViewStateFor(group: GroupIdentifier): IEditorViewState | undefined { + for (const editorPane of this.editorService.visibleEditorPanes) { + if (editorPane.group.id === group && this.matches(editorPane.input)) { + if (isTextEditorPane(editorPane)) { + return editorPane.getViewState(); + } + } } - return this._shortDescription; + return undefined; } +} - private _mediumDescription: string | undefined = undefined; - private get mediumDescription(): string { - if (typeof this._mediumDescription !== 'string') { - this._mediumDescription = this.labelService.getUriLabel(dirname(this._preferredResource), { relative: true }); - } +/** + * A read-only text editor input whos contents are made of the provided resource that points to an existing + * code editor model. + */ +export class TextResourceEditorInput extends AbstractTextResourceEditorInput implements IModeSupport { + + static readonly ID: string = 'workbench.editors.resourceEditorInput'; - return this._mediumDescription; + override get typeId(): string { + return TextResourceEditorInput.ID; } - private _longDescription: string | undefined = undefined; - private get longDescription(): string { - if (typeof this._longDescription !== 'string') { - this._longDescription = this.labelService.getUriLabel(dirname(this._preferredResource)); - } + private cachedModel: TextResourceEditorModel | undefined = undefined; + private modelReference: Promise> | undefined = undefined; - return this._longDescription; + constructor( + resource: URI, + private name: string | undefined, + private description: string | undefined, + private preferredMode: string | undefined, + private preferredContents: string | undefined, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextFileService textFileService: ITextFileService, + @IEditorService editorService: IEditorService, + @IFileService fileService: IFileService, + @ILabelService labelService: ILabelService + ) { + super(resource, undefined, editorService, textFileService, labelService, fileService); } - private _shortTitle: string | undefined = undefined; - private get shortTitle(): string { - if (typeof this._shortTitle !== 'string') { - this._shortTitle = this.getName(); - } - - return this._shortTitle; + override getName(): string { + return this.name || super.getName(); } - private _mediumTitle: string | undefined = undefined; - private get mediumTitle(): string { - if (typeof this._mediumTitle !== 'string') { - this._mediumTitle = this.labelService.getUriLabel(this._preferredResource, { relative: true }); + setName(name: string): void { + if (this.name !== name) { + this.name = name; + + this._onDidChangeLabel.fire(); } + } - return this._mediumTitle; + override getDescription(): string | undefined { + return this.description; } - private _longTitle: string | undefined = undefined; - private get longTitle(): string { - if (typeof this._longTitle !== 'string') { - this._longTitle = this.labelService.getUriLabel(this._preferredResource); - } + setDescription(description: string): void { + if (this.description !== description) { + this.description = description; - return this._longTitle; + this._onDidChangeLabel.fire(); + } } - override getTitle(verbosity: Verbosity): string { - switch (verbosity) { - case Verbosity.SHORT: - return this.shortTitle; - case Verbosity.LONG: - return this.longTitle; - default: - case Verbosity.MEDIUM: - return this.mediumTitle; + setMode(mode: string): void { + this.setPreferredMode(mode); + + if (this.cachedModel) { + this.cachedModel.setMode(mode); } } - override isUntitled(): boolean { - // any file: is never untitled as it can be saved - // untitled: is untitled by definition - // any other: is untitled because it cannot be saved, as such we expect a "Save As" dialog - return !this.fileService.canHandleResource(this.resource); + setPreferredMode(mode: string): void { + this.preferredMode = mode; } - override isReadonly(): boolean { - if (this.isUntitled()) { - return false; // untitled is never readonly - } - - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + setPreferredContents(contents: string): void { + this.preferredContents = contents; } - override isSaving(): boolean { - if (this.isUntitled()) { - return false; // untitled is never saving automatically - } + override async resolve(): Promise { - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return true; // a short auto save is configured, treat this as being saved + // Unset preferred contents and mode after resolving + // once to prevent these properties to stick. We still + // want the user to change the language mode in the editor + // and want to show updated contents (if any) in future + // `resolve` calls. + const preferredContents = this.preferredContents; + const preferredMode = this.preferredMode; + this.preferredContents = undefined; + this.preferredMode = undefined; + + if (!this.modelReference) { + this.modelReference = this.textModelResolverService.createModelReference(this.resource); } - return false; - } + const ref = await this.modelReference; - override save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + // Ensure the resolved model is of expected type + const model = ref.object; + if (!(model instanceof TextResourceEditorModel)) { + ref.dispose(); + this.modelReference = undefined; - // If this is neither an `untitled` resource, nor a resource - // we can handle with the file service, we can only "Save As..." - if (this.resource.scheme !== Schemas.untitled && !this.fileService.canHandleResource(this.resource)) { - return this.saveAs(group, options); + throw new Error(`Unexpected model for TextResourceEditorInput: ${this.resource}`); } - // Normal save - return this.doSave(options, false); - } + this.cachedModel = model; - override saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSave(options, true); - } + // Set contents and mode if preferred + if (typeof preferredContents === 'string' || typeof preferredMode === 'string') { + model.updateTextEditorModel(typeof preferredContents === 'string' ? createTextBufferFactory(preferredContents) : undefined, preferredMode); + } - private async doSave(options: ITextFileSaveOptions | undefined, saveAs: boolean): Promise { + return model; + } - // Save / Save As - let target: URI | undefined; - if (saveAs) { - target = await this.textFileService.saveAs(this.resource, undefined, { ...options, suggestedTarget: this.preferredResource }); - } else { - target = await this.textFileService.save(this.resource, options); + override matches(otherInput: unknown): boolean { + if (super.matches(otherInput)) { + return true; } - if (!target) { - return undefined; // save cancelled + if (otherInput instanceof TextResourceEditorInput) { + return isEqual(otherInput.resource, this.resource); } - // If this save operation results in a new editor, either - // because it was saved to disk (e.g. from untitled) or - // through an explicit "Save As", make sure to replace it. - if ( - target.scheme !== this.resource.scheme || - (saveAs && !isEqual(target, this.preferredResource)) - ) { - return this.editorService.createEditorInput({ resource: target }); + return false; + } + + override dispose(): void { + if (this.modelReference) { + this.modelReference.then(ref => ref.dispose()); + this.modelReference = undefined; } - return this; - } + this.cachedModel = undefined; - override async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - await this.textFileService.revert(this.resource, options); + super.dispose(); } } diff --git a/lib/vscode/src/vs/workbench/common/editor/resourceEditorModel.ts b/lib/vscode/src/vs/workbench/common/editor/textResourceEditorModel.ts similarity index 85% rename from lib/vscode/src/vs/workbench/common/editor/resourceEditorModel.ts rename to lib/vscode/src/vs/workbench/common/editor/textResourceEditorModel.ts index 661bbe6496f3..6dab1e29f61b 100644 --- a/lib/vscode/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/lib/vscode/src/vs/workbench/common/editor/textResourceEditorModel.ts @@ -9,9 +9,10 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; /** - * An editor model for in-memory, readonly content that is backed by an existing editor model. + * An editor model for in-memory, readonly text content that + * is backed by an existing editor model. */ -export class ResourceEditorModel extends BaseTextEditorModel { +export class TextResourceEditorModel extends BaseTextEditorModel { constructor( resource: URI, diff --git a/lib/vscode/src/vs/workbench/common/notifications.ts b/lib/vscode/src/vs/workbench/common/notifications.ts index fb465bf963ce..c718fbc18f90 100644 --- a/lib/vscode/src/vs/workbench/common/notifications.ts +++ b/lib/vscode/src/vs/workbench/common/notifications.ts @@ -619,13 +619,17 @@ export class NotificationViewItem extends Disposable implements INotificationVie } updateSeverity(severity: Severity): void { + if (severity === this._severity) { + return; + } + this._severity = severity; this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.SEVERITY }); } updateMessage(input: NotificationMessage): void { const message = NotificationViewItem.parseNotificationMessage(input); - if (!message) { + if (!message || message.raw === this._message.raw) { return; } diff --git a/lib/vscode/src/vs/workbench/common/resources.ts b/lib/vscode/src/vs/workbench/common/resources.ts index 98001944a374..79eb43d845f5 100644 --- a/lib/vscode/src/vs/workbench/common/resources.ts +++ b/lib/vscode/src/vs/workbench/common/resources.ts @@ -76,7 +76,9 @@ export class ResourceContextKey extends Disposable implements IContextKey { if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) { this._contextKeyService.bufferChangeEvents(() => { this._resourceKey.set(value); - // NOTE@coder: Fixes source control context menus (#1104). + // NOTE@coder: this is to get Git context actions to show up + // See issue #1140 / commit 7e4a73ce2d19eee08ceea25113debefeb8ac27e2 + // TODO@oxy: Codespaces has this working alright without this patch - investigate why we need this and remove it. this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null); this._filenameKey.set(value ? basename(value) : null); this._dirnameKey.set(value ? dirname(value).fsPath : null); diff --git a/lib/vscode/src/vs/workbench/common/theme.ts b/lib/vscode/src/vs/workbench/common/theme.ts index 272f767d2e8d..20a7eadb1199 100644 --- a/lib/vscode/src/vs/workbench/common/theme.ts +++ b/lib/vscode/src/vs/workbench/common/theme.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground, listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; @@ -249,14 +249,6 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hc: null }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); -// < --- Resource Viewer --- > - -export const IMAGE_PREVIEW_BORDER = registerColor('imagePreview.border', { - dark: Color.fromHex('#808080').transparent(0.35), - light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder -}, localize('imagePreviewBorder', "Border color for image in image preview.")); - // < --- Panels --- > export const PANEL_BACKGROUND = registerColor('panel.background', { @@ -332,6 +324,25 @@ export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { hc: PANEL_BORDER }, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +// < --- Banner --- > + +export const BANNER_BACKGROUND = registerColor('banner.background', { + dark: listActiveSelectionBackground, + light: listActiveSelectionBackground, + hc: listActiveSelectionBackground +}, localize('banner.background', "Banner background color. The banner is shown under the title bar of the window.")); + +export const BANNER_FOREGROUND = registerColor('banner.foreground', { + dark: listActiveSelectionForeground, + light: listActiveSelectionForeground, + hc: listActiveSelectionForeground +}, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); + +export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { + dark: editorInfoForeground, + light: editorInfoForeground, + hc: editorInfoForeground +}, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); // < --- Status --- > diff --git a/lib/vscode/src/vs/workbench/common/views.ts b/lib/vscode/src/vs/workbench/common/views.ts index e7b8d1d61451..8a3fee85b94a 100644 --- a/lib/vscode/src/vs/workbench/common/views.ts +++ b/lib/vscode/src/vs/workbench/common/views.ts @@ -630,6 +630,8 @@ export interface ITreeView extends IDisposable { dataProvider: ITreeViewDataProvider | undefined; + dragAndDropController?: ITreeViewDragAndDropController; + showCollapseAllAction: boolean; canSelectMany: boolean; @@ -810,7 +812,10 @@ export interface ITreeViewDataProvider { readonly isTreeEmpty?: boolean; onDidChangeEmpty?: Event; getChildren(element?: ITreeItem): Promise; +} +export interface ITreeViewDragAndDropController { + onDrop(elements: ITreeItem[], target: ITreeItem): Promise; } export interface IEditableData { diff --git a/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index 889c25a9193a..62fc63fa5e12 100644 --- a/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -19,7 +19,7 @@ import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bu import { UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; class BulkEdit { @@ -195,7 +195,7 @@ export class BulkEditService implements IBulkEditService { let listener: IDisposable | undefined; try { - listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label), 'veto.blukEditService')); + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label, e.reason), 'veto.blukEditService')); await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { @@ -209,11 +209,13 @@ export class BulkEditService implements IBulkEditService { } } - private async shouldVeto(label: string | undefined): Promise { + private async shouldVeto(label: string | undefined, reason: ShutdownReason): Promise { label = label || localize('fileOperation', "File operation"); + const reasonLabel = reason === ShutdownReason.CLOSE ? localize('closeTheWindow', "Close Window") : reason === ShutdownReason.LOAD ? localize('changeWorkspace', "Change Workspace") : + reason === ShutdownReason.RELOAD ? localize('reloadTheWindow', "Reload Window") : localize('quit', "Quit"); const result = await this._dialogService.confirm({ - message: localize('areYouSureQuiteBulkEdit', "Are you sure you want to quit? '{0}' is in progress.", label), - primaryButton: localize('quit', "Quit") + message: localize('areYouSureQuiteBulkEdit', "Are you sure you want to {0}? '{1}' is in progress.", reasonLabel.toLowerCase(), label), + primaryButton: reasonLabel }); return !result.confirmed; diff --git a/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index b7c76933f5e0..db91d69b5b2b 100644 --- a/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -102,7 +102,7 @@ class ModelEditTask implements IDisposable { class EditorEditTask extends ModelEditTask { - private _editor: ICodeEditor; + private readonly _editor: ICodeEditor; constructor(modelReference: IReference, editor: ICodeEditor) { super(modelReference); @@ -110,10 +110,18 @@ class EditorEditTask extends ModelEditTask { } override getBeforeCursorState(): Selection[] | null { - return this._editor.getSelections(); + return this._canUseEditor() ? this._editor.getSelections() : null; } override apply(): void { + + // Check that the editor is still for the wanted model. It might have changed in the + // meantime and that means we cannot use the editor anymore (instead we perform the edit through the model) + if (!this._canUseEditor()) { + super.apply(); + return; + } + if (this._edits.length > 0) { this._edits = this._edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); this._editor.executeEdits('', this._edits); @@ -124,6 +132,10 @@ class EditorEditTask extends ModelEditTask { } } } + + private _canUseEditor(): boolean { + return this._editor?.getModel()?.uri.toString() === this.model.uri.toString(); + } } export class BulkTextEdits { diff --git a/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 6cf35182be62..5964f380e318 100644 --- a/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/lib/vscode/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -353,8 +353,8 @@ export class BulkEditPane extends ViewPane { } this._editorService.openEditor({ - leftResource, - rightResource: previewUri, + originalInput: { resource: leftResource }, + modifiedInput: { resource: previewUri }, label, description: this._labelService.getUriLabel(dirname(leftResource), { relative: true }), options diff --git a/lib/vscode/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/lib/vscode/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index 96f6d8628af6..a5970bac0a83 100644 --- a/lib/vscode/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/lib/vscode/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -297,6 +297,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { // update: editor and editor highlights const options: IModelDecorationOptions = { + description: 'call-hierarchy-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'call-decoration', overviewRuler: { diff --git a/lib/vscode/src/vs/workbench/contrib/cli/node/cli.contribution.ts b/lib/vscode/src/vs/workbench/contrib/cli/node/cli.contribution.ts deleted file mode 100644 index dcc7623b08ac..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/cli/node/cli.contribution.ts +++ /dev/null @@ -1,193 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as fs from 'fs'; -import * as cp from 'child_process'; -import * as nls from 'vs/nls'; -import * as path from 'vs/base/common/path'; -import * as pfs from 'vs/base/node/pfs'; -import * as extpath from 'vs/base/node/extpath'; -import { promisify } from 'util'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import product from 'vs/platform/product/common/product'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import Severity from 'vs/base/common/severity'; -import { ILogService } from 'vs/platform/log/common/log'; -import { FileAccess } from 'vs/base/common/network'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; - -function ignore(code: string, value: T): (err: any) => Promise { - return err => err.code === code ? Promise.resolve(value) : Promise.reject(err); -} - -let _source: string | null = null; -function getSource(): string { - if (!_source) { - const root = FileAccess.asFileUri('', require).fsPath; - _source = path.resolve(root, '..', 'bin', 'code'); - } - return _source; -} - -function isAvailable(): Promise { - return Promise.resolve(pfs.exists(getSource())); -} - -const category = nls.localize('shellCommand', "Shell Command"); - -class InstallAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.installCommandLine', - title: { - value: nls.localize('install', "Install '{0}' command in PATH", product.applicationName), - original: `Shell Command: Install \'${product.applicationName}\' command in PATH` - }, - category, - f1: true, - precondition: ContextKeyExpr.and(IsMacNativeContext, ContextKeyExpr.equals('remoteName', '')) - }); - } - - run(accessor: ServicesAccessor): Promise { - const productService = accessor.get(IProductService); - const notificationService = accessor.get(INotificationService); - const logService = accessor.get(ILogService); - const dialogService = accessor.get(IDialogService); - const target = `/usr/local/bin/${productService.applicationName}`; - - return isAvailable().then(isAvailable => { - if (!isAvailable) { - const message = nls.localize('not available', "This command is not available"); - notificationService.info(message); - return undefined; - } - - return this.isInstalled(target) - .then(isInstalled => { - if (!isAvailable || isInstalled) { - return Promise.resolve(null); - } else { - return fs.promises.unlink(target) - .then(undefined, ignore('ENOENT', null)) - .then(() => fs.promises.symlink(getSource(), target)) - .then(undefined, err => { - if (err.code === 'EACCES' || err.code === 'ENOENT') { - return new Promise((resolve, reject) => { - const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; - - dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(result => { - switch (result.choice) { - case 0 /* OK */: - const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + target + '\'\\" with administrator privileges"'; - - promisify(cp.exec)(command, {}) - .then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) - .then(() => resolve(), reject); - break; - case 1 /* Cancel */: - reject(new Error(nls.localize('aborted', "Aborted"))); - break; - } - }); - }); - } - - return Promise.reject(err); - }); - } - }) - .then(() => { - logService.trace('cli#install', target); - notificationService.info(nls.localize('successIn', "Shell command '{0}' successfully installed in PATH.", productService.applicationName)); - }); - }); - } - - private async isInstalled(target: string): Promise { - try { - const stat = await fs.promises.lstat(target); - return stat.isSymbolicLink() && getSource() === await extpath.realpath(target); - } catch (err) { - if (err.code === 'ENOENT') { - return false; - } - - throw err; - } - } -} - -class UninstallAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.uninstallCommandLine', - title: { - value: nls.localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName), - original: `Shell Command: Uninstall \'${product.applicationName}\' command from PATH` - }, - category, - f1: true, - precondition: ContextKeyExpr.and(IsMacNativeContext, ContextKeyExpr.equals('remoteName', '')) - }); - } - - run(accessor: ServicesAccessor): Promise { - const productService = accessor.get(IProductService); - const notificationService = accessor.get(INotificationService); - const logService = accessor.get(ILogService); - const dialogService = accessor.get(IDialogService); - const target = `/usr/local/bin/${productService.applicationName}`; - - return isAvailable().then(isAvailable => { - if (!isAvailable) { - const message = nls.localize('not available', "This command is not available"); - notificationService.info(message); - return undefined; - } - - const uninstall = () => { - return fs.promises.unlink(target) - .then(undefined, ignore('ENOENT', null)); - }; - - return uninstall().then(undefined, err => { - if (err.code === 'EACCES') { - return new Promise(async (resolve, reject) => { - const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; - - const { choice } = await dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }); - switch (choice) { - case 0 /* OK */: - const command = 'osascript -e "do shell script \\"rm \'' + target + '\'\\" with administrator privileges"'; - - promisify(cp.exec)(command, {}) - .then(undefined, _ => Promise.reject(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", target)))) - .then(() => resolve(), reject); - break; - case 1 /* Cancel */: - reject(new Error(nls.localize('aborted', "Aborted"))); - break; - } - }); - } - - return Promise.reject(err); - }).then(() => { - logService.trace('cli#uninstall', target); - notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", productService.applicationName)); - }); - }); - } -} - -registerAction2(InstallAction); -registerAction2(UninstallAction); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index 30a3c94fdf19..db38d82e5446 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -18,4 +18,5 @@ import './toggleMultiCursorModifier'; import './toggleRenderControlCharacter'; import './toggleRenderWhitespace'; import './toggleWordWrap'; +import './untitledTextEditorHint'; import './workbenchReferenceSearch'; diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 2362766c7417..c97b25050d28 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -21,7 +21,7 @@ const enum WidgetState { class DiffEditorHelperContribution extends Disposable implements IDiffEditorContribution { - public static ID = 'editor.contrib.diffEditorHelper'; + public static readonly ID = 'editor.contrib.diffEditorHelper'; private _helperWidget: FloatingClickWidget | null; private _helperWidgetListener: IDisposable | null; diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index dc1c0e064cbb..cb02f6275e5e 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -69,7 +69,7 @@ export abstract class SimpleFindWidget extends Widget { this._updateHistoryDelayer = new Delayer(500); this.oninput(this._findInput.domNode, (e) => { - this.foundMatch = this.onInputChanged(); + this.foundMatch = this._onInputChanged(); this.updateButtons(this.foundMatch); this._delayedUpdateHistory(); }); @@ -138,25 +138,25 @@ export abstract class SimpleFindWidget extends Widget { }); this._focusTracker = this._register(dom.trackFocus(this._innerDomNode)); - this._register(this._focusTracker.onDidFocus(this.onFocusTrackerFocus.bind(this))); - this._register(this._focusTracker.onDidBlur(this.onFocusTrackerBlur.bind(this))); + this._register(this._focusTracker.onDidFocus(this._onFocusTrackerFocus.bind(this))); + this._register(this._focusTracker.onDidBlur(this._onFocusTrackerBlur.bind(this))); this._findInputFocusTracker = this._register(dom.trackFocus(this._findInput.domNode)); - this._register(this._findInputFocusTracker.onDidFocus(this.onFindInputFocusTrackerFocus.bind(this))); - this._register(this._findInputFocusTracker.onDidBlur(this.onFindInputFocusTrackerBlur.bind(this))); + this._register(this._findInputFocusTracker.onDidFocus(this._onFindInputFocusTrackerFocus.bind(this))); + this._register(this._findInputFocusTracker.onDidBlur(this._onFindInputFocusTrackerBlur.bind(this))); this._register(dom.addDisposableListener(this._innerDomNode, 'click', (event) => { event.stopPropagation(); })); } - protected abstract onInputChanged(): boolean; + protected abstract _onInputChanged(): boolean; protected abstract find(previous: boolean): void; protected abstract findFirst(): void; - protected abstract onFocusTrackerFocus(): void; - protected abstract onFocusTrackerBlur(): void; - protected abstract onFindInputFocusTrackerFocus(): void; - protected abstract onFindInputFocusTrackerBlur(): void; + protected abstract _onFocusTrackerFocus(): void; + protected abstract _onFocusTrackerBlur(): void; + protected abstract _onFindInputFocusTrackerFocus(): void; + protected abstract _onFindInputFocusTrackerBlur(): void; protected get inputValue() { return this._findInput.getValue(); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 3fd351fc49e1..33143587fca5 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -416,10 +416,17 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const fontStyleLabels = new Array(); function addStyle(key: 'bold' | 'italic' | 'underline') { + let label: HTMLElement | string | undefined; if (semantic && semantic[key]) { - fontStyleLabels.push($('span.tiw-metadata-semantic', undefined, key)); + label = $('span.tiw-metadata-semantic', undefined, key); } else if (tm && tm[key]) { - fontStyleLabels.push(key); + label = key; + } + if (label) { + if (fontStyleLabels.length) { + fontStyleLabels.push(' '); + } + fontStyleLabels.push(label); } } addStyle('bold'); @@ -428,7 +435,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (fontStyleLabels.length) { elements.push($('tr', undefined, $('td.tiw-metadata-key', undefined, 'font style' as string), - $('td.tiw-metadata-value', undefined, fontStyleLabels.join(' ')) + $('td.tiw-metadata-value', undefined, ...fontStyleLabels) )); } return elements; diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts index f5e0bb1571f6..c80090eb8a70 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/inspectKeybindings.ts @@ -3,28 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { Action } from 'vs/base/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; class InspectKeyMap extends EditorAction { constructor() { super({ id: 'workbench.action.inspectKeyMappings', - label: nls.localize('workbench.action.inspectKeyMap', "Developer: Inspect Key Mappings"), + label: localize('workbench.action.inspectKeyMap', "Developer: Inspect Key Mappings"), alias: 'Developer: Inspect Key Mappings', precondition: undefined }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + run(accessor: ServicesAccessor, editor: ICodeEditor): void { const keybindingService = accessor.get(IKeybindingService); const editorService = accessor.get(IEditorService); @@ -34,23 +32,23 @@ class InspectKeyMap extends EditorAction { registerEditorAction(InspectKeyMap); -class InspectKeyMapJSON extends Action { - public static readonly ID = 'workbench.action.inspectKeyMappingsJSON'; - public static readonly LABEL = nls.localize('workbench.action.inspectKeyMapJSON', "Inspect Key Mappings (JSON)"); - - constructor( - id: string, - label: string, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IEditorService private readonly _editorService: IEditorService - ) { - super(id, label); +class InspectKeyMapJSON extends Action2 { + + constructor() { + super({ + id: 'workbench.action.inspectKeyMappingsJSON', + title: { value: localize('workbench.action.inspectKeyMapJSON', "Inspect Key Mappings (JSON)"), original: 'Inspect Key Mappings (JSON)' }, + category: CATEGORIES.Developer, + f1: true + }); } - public override run(): Promise { - return this._editorService.openEditor({ contents: this._keybindingService._dumpDebugInfoJSON(), options: { pinned: true } }); + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const keybindingService = accessor.get(IKeybindingService); + + await editorService.openEditor({ contents: keybindingService._dumpDebugInfoJSON(), options: { pinned: true } }); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(InspectKeyMapJSON), 'Developer: Inspect Key Mappings (JSON)', CATEGORIES.Developer.value); +registerAction2(InspectKeyMapJSON); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index bdb42efacd42..2a9776a9614c 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -233,6 +233,7 @@ class DocumentSymbolsOutline implements IOutline { const ids = this._editor.deltaDecorations([], [{ range: symbol.range, options: { + description: 'document-symbols-outline-range-highlight', className: 'rangeHighlight', isWholeLine: true } diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 73843d65ff63..d2054e83e513 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -17,6 +17,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { @@ -47,11 +48,13 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor - this.editorService.openEditor(this.editorService.activeEditor, { + const editorOptions: ITextEditorOptions = { selection: options.range, pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus - }, SIDE_GROUP); + }; + + this.editorService.openEditor(this.editorService.activeEditor, editorOptions, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 7766be44b3a0..c5772a73b67e 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -28,6 +28,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { isCompositeEditor } from 'vs/editor/browser/editorBrowser'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -73,11 +74,13 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor - this.editorService.openEditor(this.editorService.activeEditor, { + const editorOptions: ITextEditorOptions = { selection: options.range, pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus - }, SIDE_GROUP); + }; + + this.editorService.openEditor(this.editorService.activeEditor, editorOptions, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 5f4725e8f563..825a93c9fc29 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -233,9 +233,10 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { const nestedProgress = new Progress<{ displayName?: string, extensionId?: ExtensionIdentifier }>(provider => { progress.report({ message: localize( - 'formatting', - "Running '{0}' Formatter ([configure](command:workbench.action.openSettings?%5B%22editor.formatOnSave%22%5D)).", - provider.displayName || provider.extensionId && provider.extensionId.value || '???' + { key: 'formatting2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Running '{0}' Formatter ([configure]({1})).", + provider.displayName || provider.extensionId && provider.extensionId.value || '???', + 'command:workbench.action.openSettings?%5B%22editor.formatOnSave%22%5D' ) }); }); @@ -336,9 +337,10 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { private _report(): void { progress.report({ message: localize( - 'codeaction.get', - "Getting code actions from '{0}' ([configure](command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D)).", - [...this._names].map(name => `'${name}'`).join(', ') + { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Getting code actions from '{0}' ([configure]({1})).", + [...this._names].map(name => `'${name}'`).join(', '), + 'command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D' ) }); } diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts index 5164467cd6d7..ea457ff5ca93 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts @@ -3,13 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -17,34 +14,39 @@ import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommand import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleColumnSelectionAction extends Action { - public static readonly ID = 'editor.action.toggleColumnSelection'; - public static readonly LABEL = nls.localize('toggleColumnSelection', "Toggle Column Selection Mode"); +export class ToggleColumnSelectionAction extends Action2 { - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService - ) { - super(id, label); - } + static readonly ID = 'editor.action.toggleColumnSelection'; - private _getCodeEditor(): ICodeEditor | null { - const codeEditor = this._codeEditorService.getFocusedCodeEditor(); - if (codeEditor) { - return codeEditor; - } - return this._codeEditorService.getActiveCodeEditor(); + constructor() { + super({ + id: ToggleColumnSelectionAction.ID, + title: { + value: localize('toggleColumnSelection', "Toggle Column Selection Mode"), + mnemonicTitle: localize({ key: 'miColumnSelection', comment: ['&& denotes a mnemonic'] }, "Column &&Selection Mode"), + original: 'Toggle Column Selection Mode' + }, + f1: true, + toggled: ContextKeyExpr.equals('config.editor.columnSelection', true), + menu: { + id: MenuId.MenubarSelectionMenu, + group: '4_config', + order: 2 + } + }); } - public override async run(): Promise { - const oldValue = this._configurationService.getValue('editor.columnSelection'); - const codeEditor = this._getCodeEditor(); - await this._configurationService.updateValue('editor.columnSelection', !oldValue); - const newValue = this._configurationService.getValue('editor.columnSelection'); - if (!codeEditor || codeEditor !== this._getCodeEditor() || oldValue === newValue || !codeEditor.hasModel()) { + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const codeEditorService = accessor.get(ICodeEditorService); + + const oldValue = configurationService.getValue('editor.columnSelection'); + const codeEditor = this._getCodeEditor(codeEditorService); + await configurationService.updateValue('editor.columnSelection', !oldValue); + const newValue = configurationService.getValue('editor.columnSelection'); + if (!codeEditor || codeEditor !== this._getCodeEditor(codeEditorService) || oldValue === newValue || !codeEditor.hasModel()) { return; } const viewModel = codeEditor._getViewModel(); @@ -76,17 +78,14 @@ export class ToggleColumnSelectionAction extends Action { codeEditor.setSelection(new Selection(fromPosition.lineNumber, fromPosition.column, toPosition.lineNumber, toPosition.column)); } } -} -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleColumnSelectionAction), 'Toggle Column Selection Mode'); + private _getCodeEditor(codeEditorService: ICodeEditorService): ICodeEditor | null { + const codeEditor = codeEditorService.getFocusedCodeEditor(); + if (codeEditor) { + return codeEditor; + } + return codeEditorService.getActiveCodeEditor(); + } +} -MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '4_config', - command: { - id: ToggleColumnSelectionAction.ID, - title: nls.localize({ key: 'miColumnSelection', comment: ['&& denotes a mnemonic'] }, "Column &&Selection Mode"), - toggled: ContextKeyExpr.equals('config.editor.columnSelection', true) - }, - order: 2 -}); +registerAction2(ToggleColumnSelectionAction); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts index 1da1c3be2d58..7dfdbb583c4b 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMinimap.ts @@ -3,41 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleMinimapAction extends Action { - public static readonly ID = 'editor.action.toggleMinimap'; - public static readonly LABEL = nls.localize('toggleMinimap', "Toggle Minimap"); +export class ToggleMinimapAction extends Action2 { - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(id, label); + static readonly ID = 'editor.action.toggleMinimap'; + + constructor() { + super({ + id: ToggleMinimapAction.ID, + title: { + value: localize('toggleMinimap', "Toggle Minimap"), + original: 'Toggle Minimap', + mnemonicTitle: localize({ key: 'miShowMinimap', comment: ['&& denotes a mnemonic'] }, "Show &&Minimap") + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.equals('config.editor.minimap.enabled', true), + menu: { + id: MenuId.MenubarViewMenu, + group: '5_editor', + order: 2 + } + }); } - public override run(): Promise { - const newValue = !this._configurationService.getValue('editor.minimap.enabled'); - return this._configurationService.updateValue('editor.minimap.enabled', newValue); + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const newValue = !configurationService.getValue('editor.minimap.enabled'); + return configurationService.updateValue('editor.minimap.enabled', newValue); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMinimapAction), 'View: Toggle Minimap', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - command: { - id: ToggleMinimapAction.ID, - title: nls.localize({ key: 'miShowMinimap', comment: ['&& denotes a mnemonic'] }, "Show &&Minimap"), - toggled: ContextKeyExpr.equals('config.editor.minimap.enabled', true) - }, - order: 2 -}); +registerAction2(ToggleMinimapAction); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index d271522e1074..fea60d0515ab 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -3,37 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import * as platform from 'vs/base/common/platform'; -import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { isMacintosh } from 'vs/base/common/platform'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleMultiCursorModifierAction extends Action { +export class ToggleMultiCursorModifierAction extends Action2 { - public static readonly ID = 'workbench.action.toggleMultiCursorModifier'; - public static readonly LABEL = nls.localize('toggleLocation', "Toggle Multi-Cursor Modifier"); + static readonly ID = 'workbench.action.toggleMultiCursorModifier'; private static readonly multiCursorModifierConfigurationKey = 'editor.multiCursorModifier'; - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleMultiCursorModifierAction.ID, + title: { value: localize('toggleLocation', "Toggle Multi-Cursor Modifier"), original: 'Toggle Multi-Cursor Modifier' }, + f1: true + }); } - public override run(): Promise { - const editorConf = this.configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); + override run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const editorConf = configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); const newValue: 'ctrlCmd' | 'alt' = (editorConf.multiCursorModifier === 'ctrlCmd' ? 'alt' : 'ctrlCmd'); - return this.configurationService.updateValue(ToggleMultiCursorModifierAction.multiCursorModifierConfigurationKey, newValue); + return configurationService.updateValue(ToggleMultiCursorModifierAction.multiCursorModifierConfigurationKey, newValue); } } @@ -66,14 +66,13 @@ class MultiCursorModifierContextKeyController implements IWorkbenchContribution Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(MultiCursorModifierContextKeyController, LifecyclePhase.Restored); +registerAction2(ToggleMultiCursorModifierAction); -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMultiCursorModifierAction), 'Toggle Multi-Cursor Modifier'); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { group: '4_config', command: { id: ToggleMultiCursorModifierAction.ID, - title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") + title: localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") }, when: multiCursorModifier.isEqualTo('ctrlCmd'), order: 1 @@ -83,9 +82,9 @@ MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { command: { id: ToggleMultiCursorModifierAction.ID, title: ( - platform.isMacintosh - ? nls.localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor") - : nls.localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor") + isMacintosh + ? localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor") + : localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor") ) }, when: multiCursorModifier.isEqualTo('altKey'), diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts index ecf2968c5c19..0990dd2f4547 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts @@ -3,42 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { CATEGORIES, } from 'vs/workbench/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class ToggleRenderControlCharacterAction extends Action { +export class ToggleRenderControlCharacterAction extends Action2 { - public static readonly ID = 'editor.action.toggleRenderControlCharacter'; - public static readonly LABEL = nls.localize('toggleRenderControlCharacters', "Toggle Control Characters"); + static readonly ID = 'editor.action.toggleRenderControlCharacter'; - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(id, label); + constructor() { + super({ + id: ToggleRenderControlCharacterAction.ID, + title: { + value: localize('toggleRenderControlCharacters', "Toggle Control Characters"), + mnemonicTitle: localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Render &&Control Characters"), + original: 'Toggle Control Characters' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.equals('config.editor.renderControlCharacters', true), + menu: { + id: MenuId.MenubarViewMenu, + group: '5_editor', + order: 5 + } + }); } - public override run(): Promise { - let newRenderControlCharacters = !this._configurationService.getValue('editor.renderControlCharacters'); - return this._configurationService.updateValue('editor.renderControlCharacters', newRenderControlCharacters); + override run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const newRenderControlCharacters = !configurationService.getValue('editor.renderControlCharacters'); + return configurationService.updateValue('editor.renderControlCharacters', newRenderControlCharacters); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleRenderControlCharacterAction), 'View: Toggle Control Characters', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - command: { - id: ToggleRenderControlCharacterAction.ID, - title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Render &&Control Characters"), - toggled: ContextKeyExpr.equals('config.editor.renderControlCharacters', true) - }, - order: 5 -}); +registerAction2(ToggleRenderControlCharacterAction); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts index e9835e215b6e..c8bab9f321f1 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts @@ -3,29 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { CATEGORIES, Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; - -export class ToggleRenderWhitespaceAction extends Action { - - public static readonly ID = 'editor.action.toggleRenderWhitespace'; - public static readonly LABEL = nls.localize('toggleRenderWhitespace', "Toggle Render Whitespace"); - - constructor( - id: string, - label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(id, label); +import { CATEGORIES, } from 'vs/workbench/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +class ToggleRenderWhitespaceAction extends Action2 { + + static readonly ID = 'editor.action.toggleRenderWhitespace'; + + constructor() { + super({ + id: ToggleRenderWhitespaceAction.ID, + title: { + value: localize('toggleRenderWhitespace', "Toggle Render Whitespace"), + mnemonicTitle: localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "&&Render Whitespace"), + original: 'Toggle Render Whitespace' + }, + category: CATEGORIES.View, + f1: true, + toggled: ContextKeyExpr.notEquals('config.editor.renderWhitespace', 'none'), + menu: { + id: MenuId.MenubarViewMenu, + group: '5_editor', + order: 4 + } + }); } - public override run(): Promise { - const renderWhitespace = this._configurationService.getValue('editor.renderWhitespace'); + override run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const renderWhitespace = configurationService.getValue('editor.renderWhitespace'); let newRenderWhitespace: string; if (renderWhitespace === 'none') { @@ -34,19 +45,8 @@ export class ToggleRenderWhitespaceAction extends Action { newRenderWhitespace = 'none'; } - return this._configurationService.updateValue('editor.renderWhitespace', newRenderWhitespace); + return configurationService.updateValue('editor.renderWhitespace', newRenderWhitespace); } } -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleRenderWhitespaceAction), 'View: Toggle Render Whitespace', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - command: { - id: ToggleRenderWhitespaceAction.ID, - title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "&&Render Whitespace"), - toggled: ContextKeyExpr.notEquals('config.editor.renderWhitespace', 'none') - }, - order: 4 -}); +registerAction2(ToggleRenderWhitespaceAction); diff --git a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index beb9467303da..7fd09e34fee8 100644 --- a/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -280,7 +280,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '5_editor', command: { id: TOGGLE_WORD_WRAP_ID, - title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), + title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "&&Word Wrap"), toggled: EDITOR_WORD_WRAP, precondition: CAN_TOGGLE_WORD_WRAP }, diff --git a/lib/vscode/src/vs/workbench/browser/parts/editor/untitledHint.ts b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts similarity index 85% rename from lib/vscode/src/vs/workbench/browser/parts/editor/untitledHint.ts rename to lib/vscode/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index 04a3dc37cee0..1e7a864680e4 100644 --- a/lib/vscode/src/vs/workbench/browser/parts/editor/untitledHint.ts +++ b/lib/vscode/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -17,18 +17,19 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; + const $ = dom.$; -const untitledHintSetting = 'workbench.editor.untitled.hint'; -export class UntitledHintContribution implements IEditorContribution { +const untitledTextEditorHintSetting = 'workbench.editor.untitled.hint'; +export class UntitledTextEditorHintContribution implements IEditorContribution { - public static readonly ID = 'editor.contrib.untitledHint'; + public static readonly ID = 'editor.contrib.untitledTextEditorHint'; private toDispose: IDisposable[]; - private untitledHintContentWidget: UntitledHintContentWidget | undefined; + private untitledTextHintContentWidget: UntitledTextEditorHintContentWidget | undefined; private experimentTreatment: 'text' | 'hidden' | undefined; - constructor( private editor: ICodeEditor, @ICommandService private readonly commandService: ICommandService, @@ -40,7 +41,7 @@ export class UntitledHintContribution implements IEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(untitledHintSetting)) { + if (e.affectsConfiguration(untitledTextEditorHintSetting)) { this.update(); } })); @@ -51,24 +52,24 @@ export class UntitledHintContribution implements IEditorContribution { } private update(): void { - this.untitledHintContentWidget?.dispose(); - const configValue = this.configurationService.getValue<'text' | 'hidden' | 'default'>(untitledHintSetting); + this.untitledTextHintContentWidget?.dispose(); + const configValue = this.configurationService.getValue<'text' | 'hidden' | 'default'>(untitledTextEditorHintSetting); const untitledHintMode = configValue === 'default' ? (this.experimentTreatment || 'text') : configValue; const model = this.editor.getModel(); if (model && model.uri.scheme === Schemas.untitled && model.getModeId() === PLAINTEXT_MODE_ID && untitledHintMode === 'text') { - this.untitledHintContentWidget = new UntitledHintContentWidget(this.editor, this.commandService, this.configurationService); + this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.commandService, this.configurationService); } } dispose(): void { dispose(this.toDispose); - this.untitledHintContentWidget?.dispose(); + this.untitledTextHintContentWidget?.dispose(); } } -class UntitledHintContentWidget implements IContentWidget { +class UntitledTextEditorHintContentWidget implements IContentWidget { private static readonly ID = 'editor.widget.untitledHint'; @@ -99,7 +100,7 @@ class UntitledHintContentWidget implements IContentWidget { } getId(): string { - return UntitledHintContentWidget.ID; + return UntitledTextEditorHintContentWidget.ID; } // Select a language to get started. Start typing to dismiss, or don't show this again. @@ -133,7 +134,7 @@ class UntitledHintContentWidget implements IContentWidget { })); this.toDispose.push(dom.addDisposableListener(dontShow, 'click', () => { - this.configurationService.updateValue(untitledHintSetting, 'hidden'); + this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden'); this.dispose(); this.editor.focus(); })); @@ -173,3 +174,5 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .contentWidgets .untitled-hint a { color: ${textLinkForegroundColor}; }`); } }); + +registerEditorContribution(UntitledTextEditorHintContribution.ID, UntitledTextEditorHintContribution); diff --git a/lib/vscode/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/lib/vscode/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 12682d319d9d..8382e6c43187 100644 --- a/lib/vscode/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/lib/vscode/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -29,6 +29,7 @@ export class CommentGlyphWidget { private createDecorationOptions(): ModelDecorationOptions { const decorationOptions: IModelDecorationOptions = { + description: 'comment-glyph-widget', isWholeLine: true, overviewRuler: { color: themeColorFromId(overviewRulerCommentingRangeForeground), diff --git a/lib/vscode/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/lib/vscode/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 42ab2bb49446..c53a055d6ba9 100644 --- a/lib/vscode/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/lib/vscode/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -30,7 +30,7 @@ import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/co import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, resolveColorValue, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; @@ -805,12 +805,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget renderOptions: { after: { contentText: placeholder, - color: `${transparent(editorForeground, 0.4)(this.themeService.getColorTheme())}` + color: `${resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4)}` } } }]; - this._commentReplyComponent?.editor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations); + this._commentReplyComponent?.editor.setDecorations('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); } } diff --git a/lib/vscode/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/lib/vscode/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index ed8b5735426f..46c21f9860ed 100644 --- a/lib/vscode/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -102,6 +102,7 @@ class CommentingRangeDecorator { constructor() { const decorationOptions: IModelDecorationOptions = { + description: 'commenting-range-decorator', isWholeLine: true, linesDecorationsClassName: 'comment-range-glyph comment-diff-added' }; @@ -196,7 +197,7 @@ export class CommentController implements IEditorContribution { })); this.globalToDispose.add(this.editor.onDidChangeModel(e => this.onModelChanged(e))); - this.codeEditorService.registerDecorationType(COMMENTEDITOR_DECORATION_KEY, {}); + this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); this.beginCompute(); } diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts index 7fbc785144a4..dc4c64be579a 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Schemas } from 'vs/base/common/network'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { customEditorInputFactory, CustomEditorInputSerializer } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; +import { CustomEditorInputSerializer, ComplexCustomWorkingCopyEditorHandler as ComplexCustomWorkingCopyEditorHandler } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewEditor } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditor'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { CustomEditorInput } from './customEditorInput'; import { CustomEditorService } from './customEditors'; @@ -32,5 +33,5 @@ Registry.as(EditorExtensions.EditorInputFactories) CustomEditorInputSerializer.ID, CustomEditorInputSerializer); -Registry.as(EditorExtensions.EditorInputFactories) - .registerCustomEditorInputFactory(Schemas.vscodeCustomEditor, customEditorInputFactory); +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(ComplexCustomWorkingCopyEditorHandler, LifecyclePhase.Starting); diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 08f1b94ef060..9c390babf049 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -4,22 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; -import { memoize } from 'vs/base/common/decorators'; import { IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; -import { isEqual } from 'vs/base/common/resources'; +import { dirname, isEqual } from 'vs/base/common/resources'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; +import { decorateFileEditorLabel } from 'vs/workbench/common/editor/resourceEditorInput'; import { defaultCustomEditor } from 'vs/workbench/contrib/customEditor/common/contributedCustomEditors'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { decorateFileEditorLabel } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -32,7 +33,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { resource: URI, viewType: string, group: GroupIdentifier | undefined, - options?: { readonly customClasses?: string }, + options?: { readonly customClasses?: string, readonly oldResource?: URI }, ): IEditorInput { return instantiationService.invokeFunction(accessor => { if (viewType === defaultCustomEditor.id) { @@ -43,11 +44,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { let untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; const id = generateUuid(); const webview = accessor.get(IWebviewService).createWebviewOverlay(id, { customClasses: options?.customClasses }, {}, undefined); - const input = instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, { untitledDocumentData: untitledDocumentData }); - // If we're loading untitled file data we should ensure it's dirty - if (untitledDocumentData) { - input._defaultDirtyState = true; - } + const input = instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, { untitledDocumentData: untitledDocumentData, oldResource: options?.oldResource }); if (typeof group !== 'undefined') { input.updateGroup(group); } @@ -58,6 +55,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public static override readonly typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; + public readonly oldResource?: URI; private _defaultDirtyState: boolean | undefined; private readonly _backupId: string | undefined; @@ -73,7 +71,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { viewType: string, id: string, webview: WebviewOverlay, - options: { startsDirty?: boolean, backupId?: string, untitledDocumentData?: VSBuffer }, + options: { startsDirty?: boolean, backupId?: string, untitledDocumentData?: VSBuffer, readonly oldResource?: URI }, @IWebviewWorkbenchService webviewWorkbenchService: IWebviewWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, @@ -81,20 +79,72 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: IEditorService, @IUndoRedoService private readonly undoRedoService: IUndoRedoService, + @IFileService private readonly fileService: IFileService ) { super(id, viewType, '', webview, webviewWorkbenchService); this._editorResource = resource; + this.oldResource = options.oldResource; this._defaultDirtyState = options.startsDirty; this._backupId = options.backupId; this._untitledDocumentData = options.untitledDocumentData; + + this.registerListeners(); + } + + private registerListeners(): void { + + // Clear our labels on certain label related events + this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + } + + private onLabelEvent(scheme: string): void { + if (scheme === this.resource.scheme) { + this.updateLabel(); + } + } + + private updateLabel(): void { + + // Clear any cached labels from before + this._shortDescription = undefined; + this._mediumDescription = undefined; + this._longDescription = undefined; + this._shortTitle = undefined; + this._mediumTitle = undefined; + this._longTitle = undefined; + + // Trigger recompute of label + this._onDidChangeLabel.fire(); } public override get typeId(): string { return CustomEditorInput.typeId; } - public override canSplit() { - return !!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument; + public override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; + + if (!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument) { + capabilities |= EditorInputCapabilities.Singleton; + } + + if (this._modelRef) { + if (this._modelRef.object.isReadonly()) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + if (this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } + + if (this.resource.scheme === Schemas.untitled) { + capabilities |= EditorInputCapabilities.Untitled; + } + + return capabilities; } override getName(): string { @@ -102,62 +152,99 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return this.decorateLabel(name); } - override matches(other: IEditorInput): boolean { - return this === other || (other instanceof CustomEditorInput - && this.viewType === other.viewType - && isEqual(this.resource, other.resource)); + override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortDescription; + case Verbosity.LONG: + return this.longDescription; + case Verbosity.MEDIUM: + default: + return this.mediumDescription; + } } - override copy(): IEditorInput { - return CustomEditorInput.create(this.instantiationService, this.resource, this.viewType, this.group, this.webview.options); + private _shortDescription: string | undefined = undefined; + private get shortDescription(): string { + if (typeof this._shortDescription !== 'string') { + this._shortDescription = this.labelService.getUriBasenameLabel(dirname(this.resource)); + } + + return this._shortDescription; + } + + private _mediumDescription: string | undefined = undefined; + private get mediumDescription(): string { + if (typeof this._mediumDescription !== 'string') { + this._mediumDescription = this.labelService.getUriLabel(dirname(this.resource), { relative: true }); + } + + return this._mediumDescription; + } + + private _longDescription: string | undefined = undefined; + private get longDescription(): string { + if (typeof this._longDescription !== 'string') { + this._longDescription = this.labelService.getUriLabel(dirname(this.resource)); + } + + return this._longDescription; } - @memoize + private _shortTitle: string | undefined = undefined; private get shortTitle(): string { - return this.getName(); + if (typeof this._shortTitle !== 'string') { + this._shortTitle = this.getName(); + } + + return this._shortTitle; } - @memoize + private _mediumTitle: string | undefined = undefined; private get mediumTitle(): string { - return this.labelService.getUriLabel(this.resource, { relative: true }); + if (typeof this._mediumTitle !== 'string') { + this._mediumTitle = this.labelService.getUriLabel(this.resource, { relative: true }); + } + + return this._mediumTitle; } - @memoize + private _longTitle: string | undefined = undefined; private get longTitle(): string { - return this.labelService.getUriLabel(this.resource); + if (typeof this._longTitle !== 'string') { + this._longTitle = this.labelService.getUriLabel(this.resource); + } + + return this._longTitle; } - public override getTitle(verbosity?: Verbosity): string { + override getTitle(verbosity?: Verbosity): string { switch (verbosity) { case Verbosity.SHORT: return this.decorateLabel(this.shortTitle); + case Verbosity.LONG: + return this.decorateLabel(this.longTitle); default: case Verbosity.MEDIUM: return this.decorateLabel(this.mediumTitle); - case Verbosity.LONG: - return this.decorateLabel(this.longTitle); } } private decorateLabel(label: string): string { + const readonly = this.hasCapability(EditorInputCapabilities.Readonly); const orphaned = !!this._modelRef?.object.isOrphaned(); - const readonly = this._modelRef - ? this._modelRef.object.isEditable() && this._modelRef.object.isOnReadonlyFileSystem() - : false; - - return decorateFileEditorLabel(label, { - orphaned, - readonly - }); + return decorateFileEditorLabel(label, { orphaned, readonly }); } - public override isReadonly(): boolean { - return this._modelRef ? !this._modelRef.object.isEditable() : false; + public override matches(other: IEditorInput): boolean { + return this === other || (other instanceof CustomEditorInput + && this.viewType === other.viewType + && isEqual(this.resource, other.resource)); } - public override isUntitled(): boolean { - return this.resource.scheme === Schemas.untitled; + public override copy(): IEditorInput { + return CustomEditorInput.create(this.instantiationService, this.resource, this.viewType, this.group, this.webview.options); } public override isDirty(): boolean { @@ -221,7 +308,11 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { this._modelRef = this._register(assertIsDefined(await this.customEditorService.models.tryRetain(this.resource, this.viewType))); this._register(this._modelRef.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(this._modelRef.object.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); - + this._register(this._modelRef.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + // If we're loading untitled file data we should ensure it's dirty + if (this._untitledDocumentData) { + this._defaultDirtyState = true; + } if (this.isDirty()) { this._onDidChangeDirty.fire(); } @@ -230,7 +321,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return null; } - override rename(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + public override rename(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { // See if we can keep using the same custom editor provider const editorInfo = this.customEditorService.getCustomEditor(this.viewType); if (editorInfo?.matches(newResource)) { @@ -242,7 +333,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { private doMove(group: GroupIdentifier, newResource: URI): IEditorInput { if (!this._moveHandler) { - return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group); + return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group, { oldResource: this.resource }); } this._moveHandler(newResource); @@ -284,14 +375,23 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return other; } - get backupId(): string | undefined { + public get backupId(): string | undefined { if (this._modelRef) { return this._modelRef.object.backupId; } return this._backupId; } - get untitledDocumentData(): VSBuffer | undefined { + public get untitledDocumentData(): VSBuffer | undefined { return this._untitledDocumentData; } + + public override asResourceEditorInput(groupId: GroupIdentifier): IResourceEditorInput { + return { + resource: this.resource, + options: { + override: this.viewType + } + }; + } } diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 23e5e9f0f664..43e86d2bb275 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -5,13 +5,18 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ICustomEditorInputFactory, IEditorInput } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { IWebviewService, WebviewContentOptions, WebviewContentPurpose, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { SerializedWebviewOptions, DeserializedWebview, reviveWebviewExtensionDescription, SerializedWebview, WebviewEditorInputSerializer, restoreWebviewContentOptions, restoreWebviewOptions } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IWorkingCopyBackupMeta, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; export interface CustomDocumentBackupData extends IWorkingCopyBackupMeta { readonly viewType: string; @@ -36,14 +41,12 @@ interface SerializedCustomEditor extends SerializedWebview { readonly backupId?: string; } - interface DeserializedCustomEditor extends DeserializedWebview { readonly editorResource: URI; readonly dirty: boolean; readonly backupId?: string; } - export class CustomEditorInputSerializer extends WebviewEditorInputSerializer { public static override readonly ID = CustomEditorInput.typeId; @@ -104,41 +107,63 @@ function reviveWebview(webviewService: IWebviewService, data: { id: string, stat return webview; } -export const customEditorInputFactory = new class implements ICustomEditorInputFactory { - public createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { - return instantiationService.invokeFunction(async accessor => { - const webviewService = accessor.get(IWebviewService); - const workingCopyBackupService = accessor.get(IWorkingCopyBackupService); +export class ComplexCustomWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { - const backup = await workingCopyBackupService.resolve({ resource, typeId: NO_TYPE_ID }); - if (!backup?.meta) { - throw new Error(`No backup found for custom editor: ${resource}`); - } + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly _workingCopyEditorService: IWorkingCopyEditorService, + @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService, + @IWebviewService private readonly _webviewService: IWebviewService, + @ICustomEditorService _customEditorService: ICustomEditorService // DO NOT REMOVE (needed on startup to register overrides properly) + ) { + super(); - const backupData = backup.meta; - const id = backupData.webview.id; - const extension = reviveWebviewExtensionDescription(backupData.extension?.id, backupData.extension?.location); - const webview = reviveWebview(webviewService, { - id, - webviewOptions: restoreWebviewOptions(backupData.webview.options), - contentOptions: restoreWebviewContentOptions(backupData.webview.options), - state: backupData.webview.state, - extension, - }); - - const editor = instantiationService.createInstance(CustomEditorInput, URI.revive(backupData.editorResource), backupData.viewType, id, webview, { backupId: backupData.backupId }); - editor.updateGroup(0); - return editor; - }); + this._installHandler(); } - public canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { - if (editorInput instanceof CustomEditorInput) { - if (editorInput.resource.path === backupResource.path && backupResource.authority === editorInput.viewType) { - return true; + private _installHandler(): void { + this._register(this._workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.resource.scheme === Schemas.vscodeCustomEditor, + isOpen: (workingCopy, editor) => { + if (!(editor instanceof CustomEditorInput)) { + return false; + } + + if (workingCopy.resource.authority !== editor.viewType.replace(/[^a-z0-9\-_]/gi, '-').toLowerCase()) { + return false; + } + + // The working copy stores the uri of the original resource as its query param + try { + const data = JSON.parse(workingCopy.resource.query); + const workingCopyResource = URI.from(data); + return isEqual(workingCopyResource, editor.resource); + } catch { + return false; + } + }, + createEditor: async workingCopy => { + const backup = await this._workingCopyBackupService.resolve(workingCopy); + if (!backup?.meta) { + throw new Error(`No backup found for custom editor: ${workingCopy.resource}`); + } + + const backupData = backup.meta; + const id = backupData.webview.id; + const extension = reviveWebviewExtensionDescription(backupData.extension?.id, backupData.extension?.location); + const webview = reviveWebview(this._webviewService, { + id, + webviewOptions: restoreWebviewOptions(backupData.webview.options), + contentOptions: restoreWebviewContentOptions(backupData.webview.options), + state: backupData.webview.state, + extension, + }); + + const editor = this._instantiationService.createInstance(CustomEditorInput, URI.revive(backupData.editorResource), backupData.viewType, id, webview, { backupId: backupData.backupId }); + editor.updateGroup(0); + return editor; } - } - - return false; + })); } -}; +} + diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 2824537bc4d8..ff0a67cfdea5 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -16,18 +16,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorInput, EditorExtensions, GroupIdentifier, IEditorInput, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { EditorExtensions, GroupIdentifier, IEditorInput, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ContributedEditorPriority, IEditorAssociationsRegistry, IEditorOverrideService, IEditorType, IEditorTypesHandler } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { ContributedEditorPriority, IEditorOverrideService, IEditorType } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ContributedCustomEditors } from '../common/contributedCustomEditors'; import { CustomEditorInput } from './customEditorInput'; -export class CustomEditorService extends Disposable implements ICustomEditorService, IEditorTypesHandler { +export class CustomEditorService extends Disposable implements ICustomEditorService { _serviceBrand: any; private readonly _contributedEditors: ContributedCustomEditors; @@ -67,7 +68,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this.updateContexts(); this._onDidChangeEditorTypes.fire(); })); - this._register(Registry.as(EditorExtensions.Associations).registerEditorTypesHandler('Custom Editor', this)); this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); this._register(fileService.onDidRunOperation(e => { diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditor.ts index c7aa1260e3f9..dbd675070a9a 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -41,6 +41,8 @@ export interface ICustomEditorService { } export interface ICustomEditorModelManager { + getAllModels(resource: URI): Promise + get(resource: URI, viewType: string): Promise; tryRetain(resource: URI, viewType: string): Promise> | undefined; @@ -55,8 +57,8 @@ export interface ICustomEditorModel extends IDisposable { readonly resource: URI; readonly backupId: string | undefined; - isEditable(): boolean; - isOnReadonlyFileSystem(): boolean; + isReadonly(): boolean; + readonly onDidChangeReadonly: Event; isOrphaned(): boolean; readonly onDidChangeOrphaned: Event; diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index b2b185ed874a..0d8176fadc43 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -16,6 +16,16 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { counter: number }>(); + public async getAllModels(resource: URI): Promise { + const keyStart = `${resource.toString()}@@@`; + const models = []; + for (const [key, entry] of this._references) { + if (key.startsWith(keyStart) && entry.model) { + models.push(await entry.model); + } + } + return models; + } public async get(resource: URI, viewType: string): Promise { const key = this.key(resource, viewType); const entry = this._references.get(key); diff --git a/lib/vscode/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/lib/vscode/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index c39fa2ce52c8..32c0e0189db7 100644 --- a/lib/vscode/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/lib/vscode/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -8,7 +8,6 @@ import { Disposable, IReference } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor'; @@ -33,12 +32,14 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo private readonly _onDidChangeOrphaned = this._register(new Emitter()); public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + private readonly _onDidChangeReadonly = this._register(new Emitter()); + public readonly onDidChangeReadonly = this._onDidChangeReadonly.event; + constructor( public readonly viewType: string, private readonly _resource: URI, private readonly _model: IReference, - @ITextFileService private readonly textFileService: ITextFileService, - @IFileService private readonly _fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService ) { super(); @@ -47,6 +48,7 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo this._textFileModel = this.textFileService.files.get(_resource); if (this._textFileModel) { this._register(this._textFileModel.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); + this._register(this._textFileModel.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); } this._register(this.textFileService.files.onDidChangeDirty(e => { @@ -61,12 +63,8 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo return this._resource; } - public isEditable(): boolean { - return !this._model.object.isReadonly(); - } - - public isOnReadonlyFileSystem(): boolean { - return this._fileService.hasCapability(this._resource, FileSystemProviderCapabilities.Readonly); + public isReadonly(): boolean { + return this._model.object.isReadonly(); } public get backupId() { diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 41237a364a9f..2df6035afdd2 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -46,6 +46,7 @@ interface IBreakpointDecoration { } const breakpointHelperDecoration: IModelDecorationOptions = { + description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; @@ -94,6 +95,7 @@ function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoi const renderInline = breakpoint.column && (breakpoint.column > model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber)); return { + description: 'breakpoint-decoration', glyphMarginClassName: ThemeIcon.asClassName(icon), glyphMarginHoverMessage, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, @@ -128,6 +130,7 @@ async function createCandidateDecorations(model: ITextModel, breakpointDecoratio result.push({ range, options: { + description: 'breakpoint-placeholder-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, beforeContentClassName: breakpointAtPosition ? undefined : `debug-breakpoint-placeholder` }, diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index f075b354d17d..7862fe95e3eb 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -27,7 +27,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; import { provideSuggestionItems, CompletionOptions } from 'vs/editor/contrib/suggest/suggest'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -57,7 +57,7 @@ function isCurlyBracketOpen(input: IActiveCodeEditor): boolean { } function createDecorations(theme: IColorTheme, placeHolder: string): IDecorationOptions[] { - const transparentForeground = transparent(editorForeground, 0.4)(theme); + const transparentForeground = theme.getColor(editorForeground)?.transparent(0.4); return [{ range: { startLineNumber: 0, @@ -125,7 +125,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.dispose(); } })); - this.codeEditorService.registerDecorationType(DECORATION_KEY, {}); + this.codeEditorService.registerDecorationType('breakpoint-widget', DECORATION_KEY, {}); this.create(); } @@ -229,7 +229,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const setDecorations = () => { const value = this.input.getModel().getValue(); const decorations = !!value ? [] : createDecorations(this.themeService.getColorTheme(), this.placeholder); - this.input.setDecorations(DECORATION_KEY, decorations); + this.input.setDecorations('breakpoint-widget', DECORATION_KEY, decorations); }; this.input.getModel().onDidChangeContent(() => setDecorations()); this.themeService.onDidColorThemeChange(() => setDecorations()); diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index e0f4fa1c42a4..bba2df5f6c2d 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -102,6 +102,7 @@ export class BreakpointsView extends ViewPane { this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); + this._register(this.debugService.onDidChangeState(() => this.onStateChange())); } override renderBody(container: HTMLElement): void { @@ -254,6 +255,22 @@ export class BreakpointsView extends ViewPane { } } + private onStateChange(): void { + const thread = this.debugService.getViewModel().focusedThread; + if (thread && thread.stoppedDetails && thread.stoppedDetails.hitBreakpointIds && thread.stoppedDetails.hitBreakpointIds.length > 0) { + const hitBreakpointIds = thread.stoppedDetails.hitBreakpointIds; + const elements = this.elements; + const index = elements.findIndex(e => { + const id = e.getIdFromAdapter(thread.session.getId()); + return typeof id === 'number' && hitBreakpointIds.indexOf(id) !== -1; + }); + if (index >= 0) { + this.list.setFocus([index]); + this.list.setSelection([index]); + } + } + } + private get elements(): BreakpointItem[] { const model = this.debugService.getModel(); const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()); diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 08bf91ebc446..b94f3199510b 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -24,6 +24,7 @@ const stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. const TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { + description: 'top-stack-frame-margin', glyphMarginClassName: ThemeIcon.asClassName(debugStackframe), stickiness, overviewRuler: { @@ -32,6 +33,7 @@ const TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { } }; const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { + description: 'focused-stack-frame-margin', glyphMarginClassName: ThemeIcon.asClassName(debugStackframeFocused), stickiness, overviewRuler: { @@ -40,14 +42,17 @@ const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { } }; const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { + description: 'top-stack-frame-decoration', isWholeLine: true, className: 'debug-top-stack-frame-line', stickiness }; const TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = { + description: 'top-stack-frame-inline-decoration', beforeContentClassName: 'debug-top-stack-frame-column' }; const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { + description: 'focused-stack-frame-decoration', isWholeLine: true, className: 'debug-focused-stack-frame-line', stickiness diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackView.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackView.ts index 94c7b595f3cb..51f7f931d44c 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -165,7 +165,7 @@ export class CallStackView extends ViewPane { const thread = sessions.length === 1 && sessions[0].getAllThreads().length === 1 ? sessions[0].getAllThreads()[0] : undefined; if (thread && thread.stoppedDetails) { this.stateMessageLabel.textContent = thread.stateLabel; - this.stateMessageLabel.title = thread.stateLabel; + this.stateMessageLabel.title = thread.stoppedDetails.text || thread.stateLabel; this.stateMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); this.stateMessage.hidden = false; } else if (sessions.length === 1 && sessions[0].state === State.Running) { @@ -603,6 +603,7 @@ class ThreadsRenderer implements ICompressibleTreeRenderer(JSONExtensions.JSONContribution); +const DEBUGGERS_AVAILABLE_KEY = 'debug.debuggersavailable'; + export class AdapterManager implements IAdapterManager { private debuggers: Debugger[]; @@ -48,12 +52,15 @@ export class AdapterManager implements IAdapterManager { @IExtensionService private readonly extensionService: IExtensionService, @IContextKeyService contextKeyService: IContextKeyService, @IModeService private readonly modeService: IModeService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IStorageService private readonly storageService: IStorageService ) { this.adapterDescriptorFactories = []; this.debuggers = []; this.registerListeners(); + const debuggersAvailable = this.storageService.getBoolean(DEBUGGERS_AVAILABLE_KEY, StorageScope.WORKSPACE, false); this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService); + this.debuggersAvailable.set(debuggersAvailable); } private registerListeners(): void { @@ -91,10 +98,46 @@ export class AdapterManager implements IAdapterManager { // update the schema to include all attributes, snippets and types from extensions. const items = (launchSchema.properties!['configurations'].items); + const taskSchema = TaskDefinitionRegistry.getJsonSchema(); + const definitions: IJSONSchemaMap = { + 'common': { + properties: { + 'name': { + type: 'string', + description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."), + default: 'Launch' + }, + 'debugServer': { + type: 'number', + description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"), + default: 4711 + }, + 'preLaunchTask': { + anyOf: [taskSchema, { + type: ['string'] + }], + default: '', + defaultSnippets: [{ body: { task: '', type: '' } }], + description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") + }, + 'postDebugTask': { + anyOf: [taskSchema, { + type: ['string'], + }], + default: '', + defaultSnippets: [{ body: { task: '', type: '' } }], + description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") + }, + 'presentation': presentationSchema, + 'internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, + } + } + }; + launchSchema.definitions = definitions; items.oneOf = []; items.defaultSnippets = []; this.debuggers.forEach(adapter => { - const schemaAttributes = adapter.getSchemaAttributes(); + const schemaAttributes = adapter.getSchemaAttributes(definitions); if (schemaAttributes && items.oneOf) { items.oneOf.push(...schemaAttributes); } @@ -121,6 +164,7 @@ export class AdapterManager implements IAdapterManager { registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable { debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher)); this.debuggersAvailable.set(this.debugAdapterFactories.size > 0); + this.storageService.store(DEBUGGERS_AVAILABLE_KEY, this.debugAdapterFactories.size > 0, StorageScope.WORKSPACE, StorageTarget.MACHINE); this._onDidRegisterDebugger.fire(); return { @@ -247,7 +291,7 @@ export class AdapterManager implements IAdapterManager { } } - if (gettingConfigurations && candidates.length === 0) { + if ((!languageLabel || gettingConfigurations) && candidates.length === 0) { await this.activateDebuggers('onDebugInitialConfigurations'); candidates = this.debuggers.filter(dbg => dbg.hasInitialConfiguration() || dbg.hasConfigurationProvider()); } diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugColors.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugColors.ts index 0afeeb754e2f..63407735c935 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -135,15 +135,23 @@ export function registerColors() { * Only visible when there are more active debug sessions/threads running. */ .debug-pane .debug-call-stack .thread > .state.label, - .debug-pane .debug-call-stack .session > .state.label, - .debug-pane .monaco-list-row.selected .thread > .state.label, - .debug-pane .monaco-list-row.selected .session > .state.label { + .debug-pane .debug-call-stack .session > .state.label { background-color: ${debugViewStateLabelBackgroundColor}; color: ${debugViewStateLabelForegroundColor}; } + /* State "badge" displaying the active session's current state. + * Only visible when there are more active debug sessions/threads running + * and thread paused due to a thrown exception. + */ + .debug-pane .debug-call-stack .thread > .state.label.exception, + .debug-pane .debug-call-stack .session > .state.label.exception { + background-color: ${debugViewExceptionLabelBackgroundColor}; + color: ${debugViewExceptionLabelForegroundColor}; + } + /* Info "badge" shown when the debugger pauses due to a thrown exception. */ - .debug-pane .debug-call-stack-title > .pause-message > .label.exception { + .debug-pane .call-stack-state-message > .label.exception { background-color: ${debugViewExceptionLabelBackgroundColor}; color: ${debugViewExceptionLabelForegroundColor}; } diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugCommands.ts index bc274a4f960a..c862f63c9cc5 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -66,7 +66,7 @@ export const STOP_LABEL = nls.localize('stop', "Stop"); export const CONTINUE_LABEL = nls.localize('continueDebug', "Continue"); export const FOCUS_SESSION_LABEL = nls.localize('focusSession', "Focus Session"); export const SELECT_AND_START_LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); -export const DEBUG_CONFIGURE_LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); +export const DEBUG_CONFIGURE_LABEL = nls.localize('openLaunchJson', "Open '{0}'", 'launch.json'); export const DEBUG_START_LABEL = nls.localize('startDebug', "Start Debugging"); export const DEBUG_RUN_LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); @@ -338,7 +338,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CONTINUE_ID, weight: KeybindingWeight.WorkbenchContrib + 10, // Use a stronger weight to get priority over start debugging F5 shortcut primary: KeyCode.F5, - when: CONTEXT_IN_DEBUG_MODE, + when: CONTEXT_DEBUG_STATE.isEqualTo('stopped'), handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { getThreadAndRun(accessor, context, thread => thread.continue()); } @@ -389,7 +389,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DEBUG_START_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.F5, - when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.isEqualTo('inactive')), handler: async (accessor: ServicesAccessor, debugStartOptions?: { config?: Partial; noDebug?: boolean }) => { const debugService = accessor.get(IDebugService); let { launch, name, getConfig } = debugService.getConfigurationManager().selectedConfiguration; diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index b3784e6901ca..51e666b94e2c 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -33,7 +33,7 @@ class ToggleBreakpointAction extends EditorAction2 { id: 'editor.debug.action.toggleBreakpoint', title: { value: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), - original: 'Toggle Breakpoint', + original: 'Debug: Toggle Breakpoint', mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") }, f1: true, diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index c4a86e5c53c0..850045df6e87 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -30,7 +30,7 @@ import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWid import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { Position } from 'vs/editor/common/core/position'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; -import { memoize, createMemoizer } from 'vs/base/common/decorators'; +import { memoize } from 'vs/base/common/decorators'; import { IEditorHoverOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover'; import { ITextModel } from 'vs/editor/common/model'; @@ -45,6 +45,8 @@ import { Event } from 'vs/base/common/event'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; const LAUNCH_JSON_REGEX = /\.vscode\/launch\.json$/; const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration'; @@ -52,6 +54,18 @@ const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped +export const debugInlineForeground = registerColor('editor.inlineValuesForeground', { + dark: '#ffffff80', + light: '#00000080', + hc: '#ffffff80' +}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); + +export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { + dark: '#ffc80033', + light: '#ffc80033', + hc: '#ffc80033' +}, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); + class InlineSegment { constructor(public column: number, public text: string) { } @@ -73,18 +87,9 @@ function createInlineValueDecoration(lineNumber: number, contentText: string, co renderOptions: { after: { contentText, - backgroundColor: 'rgba(255, 200, 0, 0.2)', - margin: '10px' - }, - dark: { - after: { - color: 'rgba(255, 255, 255, 0.5)', - } - }, - light: { - after: { - color: 'rgba(0, 0, 0, 0.5)', - } + backgroundColor: themeColorFromId(debugInlineBackground), + margin: '10px', + color: themeColorFromId(debugInlineForeground) } } }; @@ -185,7 +190,6 @@ export class DebugEditorContribution implements IDebugEditorContribution { private hoverRange: Range | null = null; private mouseDown = false; private exceptionWidgetVisible: IContextKey; - private static readonly MEMOIZER = createMemoizer(); private exceptionWidget: ExceptionWidget | undefined; private configurationWidget: FloatingClickWidget | undefined; @@ -208,7 +212,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.toDispose = []; this.registerListeners(); this.updateConfigurationWidgetVisibility(); - this.codeEditorService.registerDecorationType(INLINE_VALUE_DECORATION_KEY, {}); + this.codeEditorService.registerDecorationType('debug-inline-value-decoration', INLINE_VALUE_DECORATION_KEY, {}); this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService); this.toggleExceptionWidget(); } @@ -234,7 +238,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { })); this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e))); this.toDispose.push(this.editor.onDidChangeModelContent(() => { - DebugEditorContribution.MEMOIZER.clear(); + this._wordToLineNumbersMap = undefined; this.updateInlineValuesScheduler.schedule(); })); this.toDispose.push(this.debugService.getViewModel().onWillUpdateViews(() => this.updateInlineValuesScheduler.schedule())); @@ -247,7 +251,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.toggleExceptionWidget(); this.hideHoverWidget(); this.updateConfigurationWidgetVisibility(); - DebugEditorContribution.MEMOIZER.clear(); + this._wordToLineNumbersMap = undefined; await this.updateInlineValueDecorations(stackFrame); })); this.toDispose.push(this.editor.onDidScrollChange(() => { @@ -266,9 +270,12 @@ export class DebugEditorContribution implements IDebugEditorContribution { })); } - @DebugEditorContribution.MEMOIZER + private _wordToLineNumbersMap: Map | undefined = undefined; private get wordToLineNumbersMap(): Map { - return getWordToLineNumbersMap(this.editor.getModel()); + if (!this._wordToLineNumbersMap) { + this._wordToLineNumbersMap = getWordToLineNumbersMap(this.editor.getModel()); + } + return this._wordToLineNumbersMap; } private applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame | undefined): void { @@ -708,7 +715,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { allDecorations = decorationsPerScope.reduce((previous, current) => previous.concat(current), []); } - this.editor.setDecorations(INLINE_VALUE_DECORATION_KEY, allDecorations); + this.editor.setDecorations('debug-inline-value-decoration', INLINE_VALUE_DECORATION_KEY, allDecorations); } dispose(): void { diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugHover.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugHover.ts index 60febd20d81e..5f1f4d1123ff 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -262,6 +262,7 @@ export class DebugHoverWidget implements IContentWidget { } private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'bdebug-hover-highlight', className: 'hoverHighlight' }); diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index 3c920d1366f8..e29b61897e2e 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -33,7 +33,7 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { + protected async _getPicks(filter: string): Promise<(IQuickPickSeparator | IPickerQuickAccessItem)[]> { const picks: Array = []; picks.push({ type: 'separator', label: 'launch.json' }); diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugService.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugService.ts index 0a8d34ec3635..a0c444ab666a 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -276,11 +276,7 @@ export class DebugService implements IDebugService { */ async startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { const message = options && options.noDebug ? nls.localize('runTrust', "Running executes build tasks and program code from your workspace.") : nls.localize('debugTrust', "Debugging executes build tasks and program code from your workspace."); - const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ - modal: true, - message, - - }); + const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ message }); if (!trust) { return false; } @@ -289,8 +285,7 @@ export class DebugService implements IDebugService { // make sure to save all files and that the configuration is up to date await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { - const saveBeforeStartConfig: string = this.configurationService.getValue('debug.saveBeforeStart'); - + const saveBeforeStartConfig: string = this.configurationService.getValue('debug.saveBeforeStart', { overrideIdentifier: this.editorService.activeTextEditorMode }); if (saveBeforeStartConfig !== 'none') { await this.editorService.saveAll(); if (saveBeforeStartConfig === 'allEditorsInActiveGroup') { diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugStatus.ts index a83612abdcae..91806d765195 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -23,7 +23,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { ) { const addStatusBarEntry = () => { - this.entryAccessor = this.statusBarService.addEntry(this.entry, 'status.debug', nls.localize('status.debug', "Debug"), StatusbarAlignment.LEFT, 30 /* Low Priority */); + this.entryAccessor = this.statusBarService.addEntry(this.entry, 'status.debug', StatusbarAlignment.LEFT, 30 /* Low Priority */); }; const setShowInStatusBar = () => { @@ -65,6 +65,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { } return { + name: nls.localize('status.debug', "Debug"), text: '$(debug-alt-small) ' + text, ariaLabel: nls.localize('debugTarget', "Debug: {0}", text), tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-tb.png index fe7679985333..8e2d11acd7ba 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png index f42e02ee0c0b..a28d75bbbe72 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/pause-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/pause-tb.png index f564b78761f7..079c9d25f5f8 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/pause-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/pause-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/restart-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/restart-tb.png index 619de170ed39..9ec0196946a0 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/restart-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/restart-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepinto-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepinto-tb.png index f7a4eb230138..f347b6cce6ca 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepinto-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepinto-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepout-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepout-tb.png index b7332b3485c5..b61d6dfe722c 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepout-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepout-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepover-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepover-tb.png index 8eca81cefb3b..f492f8955712 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepover-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stepover-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stop-tb.png b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stop-tb.png index 715c180ccd6d..f4ae35a946dd 100644 Binary files a/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stop-tb.png and b/lib/vscode/src/vs/workbench/contrib/debug/browser/media/stop-tb.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index f9c3477a8a5d..125cc091e2e3 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -692,7 +692,7 @@ export class RawDebugSession implements IDisposable { const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); return errors.createErrorWithActions(userMessage, { actions: [new Action('debug.moreInfo', label, undefined, true, async () => { - this.openerService.open(URI.parse(url)); + this.openerService.open(URI.parse(url), { allowCommands: true }); })] }); } diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/repl.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/repl.ts index 4a4ded5663da..0653e7f174d4 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/repl.ts @@ -33,7 +33,7 @@ import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { CompletionContext, CompletionList, CompletionProviderRegistry, CompletionItem, completionKindFromString, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; @@ -68,6 +68,7 @@ const $ = dom.$; const HISTORY_STORAGE_KEY = 'debug.repl.history'; const FILTER_HISTORY_STORAGE_KEY = 'debug.repl.filterHistory'; +const FILTER_VALUE_STORAGE_KEY = 'debug.repl.filterValue'; const DECORATION_KEY = 'replinputdecoration'; const FILTER_ACTION_ID = `workbench.actions.treeView.repl.filter`; @@ -132,10 +133,11 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); this.filter = new ReplFilter(); this.filterState = new ReplFilterState(this); + this.filter.filterQuery = this.filterState.filterText = this.storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, ''); this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); this.multiSessionRepl.set(this.isMultiSessionView); - codeEditorService.registerDecorationType(DECORATION_KEY, {}); + codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {}); this.registerListeners(); } @@ -271,9 +273,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } getFilterStats(): { total: number, filtered: number } { + // This could be called before the tree is created when setting this.filterState.filterText value return { - total: this.tree.getNode().children.length, - filtered: this.tree.getNode().children.filter(c => c.visible).length + total: this.tree?.getNode().children.length ?? 0, + filtered: this.tree?.getNode().children.filter(c => c.visible).length ?? 0 }; } @@ -657,7 +660,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { const decorations: IDecorationOptions[] = []; if (this.isReadonly && this.replInput.hasTextFocus() && !this.replInput.getValue()) { - const transparentForeground = transparent(editorForeground, 0.4)(this.themeService.getColorTheme()); + const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); decorations.push({ range: { startLineNumber: 0, @@ -674,7 +677,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { }); } - this.replInput.setDecorations(DECORATION_KEY, decorations); + this.replInput.setDecorations('repl-decoration', DECORATION_KEY, decorations); } override saveState(): void { @@ -691,6 +694,12 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } else { this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE); } + const filterValue = this.filterState.filterText; + if (filterValue) { + this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.USER); + } else { + this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE); + } } super.saveState(); diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/replViewer.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/replViewer.ts index d393d97ca5f7..794e917ad7c1 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -394,7 +394,7 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider 1 ? localize({ key: 'occurred', comment: ['Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] }, - ", occured {0} times", element.count) : ''); + ", occurred {0} times", element.count) : ''); } if (element instanceof RawObjectReplElement) { return localize('replRawObjectAriaLabel', "Debug console variable {0}, value {1}", element.name, element.value); diff --git a/lib/vscode/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/lib/vscode/src/vs/workbench/contrib/debug/browser/welcomeView.ts index e5b63d8da09b..5fb19d85d79a 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -32,8 +32,8 @@ const CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR = new RawContextKey( export class WelcomeView extends ViewPane { - static ID = 'workbench.debug.welcome'; - static LABEL = localize('run', "Run"); + static readonly ID = 'workbench.debug.welcome'; + static readonly LABEL = localize('run', "Run"); private debugStartLanguageContext: IContextKey; private debuggerInterestedContext: IContextKey; @@ -123,7 +123,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Show](command:{0}) all automatic debug configurations.", SELECT_AND_START_ID), + "[Show all automatic debug configurations](command:{0}).", SELECT_AND_START_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug, order: 10 diff --git a/lib/vscode/src/vs/workbench/contrib/debug/common/debug.ts b/lib/vscode/src/vs/workbench/contrib/debug/common/debug.ts index c005f81bd2aa..3a46f18c937d 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/common/debug.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/common/debug.ts @@ -106,6 +106,7 @@ export interface IRawStoppedDetails { totalFrames?: number; allThreadsStopped?: boolean; framesErrorMessage?: string; + hitBreakpointIds?: number[]; } // model diff --git a/lib/vscode/src/vs/workbench/contrib/debug/common/debugger.ts b/lib/vscode/src/vs/workbench/contrib/debug/common/debugger.ts index d021319ffc4c..bf3633def3fb 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/common/debugger.ts @@ -4,21 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as objects from 'vs/base/common/objects'; import { isObject } from 'vs/base/common/types'; -import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaMap, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IDebuggerContribution, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IDebugAdapter, IDebugger, IDebugSession, IAdapterManager, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IConfig, IDebuggerContribution, IDebugAdapter, IDebugger, IDebugSession, IAdapterManager, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils'; -import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { presentationSchema } from 'vs/workbench/contrib/debug/common/debugSchemas'; import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -194,15 +191,15 @@ export class Debugger implements IDebugger { }; } - getSchemaAttributes(): IJSONSchema[] | null { + getSchemaAttributes(definitions: IJSONSchemaMap): IJSONSchema[] | null { if (!this.debuggerContribution.configurationAttributes) { return null; } // fill in the default configuration attributes shared by all adapters. - const taskSchema = TaskDefinitionRegistry.getJsonSchema(); return Object.keys(this.debuggerContribution.configurationAttributes).map(request => { + const definitionId = `${this.type}:${request}`; const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request]; const defaultRequired = ['name', 'type', 'request']; attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; @@ -219,64 +216,44 @@ export class Debugger implements IDebugger { errorMessage: nls.localize('debugTypeNotRecognised', "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled."), patternErrorMessage: nls.localize('node2NotSupported', "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".") }; - properties['name'] = { - type: 'string', - description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."), - default: 'Launch' - }; properties['request'] = { enum: [request], description: nls.localize('debugRequest', "Request type of configuration. Can be \"launch\" or \"attach\"."), }; - properties['debugServer'] = { - type: 'number', - description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"), - default: 4711 - }; - properties['preLaunchTask'] = { - anyOf: [taskSchema, { - type: ['string'] - }], - default: '', - defaultSnippets: [{ body: { task: '', type: '' } }], - description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") - }; - properties['postDebugTask'] = { - anyOf: [taskSchema, { - type: ['string'], - }], - default: '', - defaultSnippets: [{ body: { task: '', type: '' } }], - description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") - }; - properties['presentation'] = presentationSchema; - properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; - // Clear out windows, linux and osx fields to not have cycles inside the properties object - delete properties['windows']; - delete properties['osx']; - delete properties['linux']; - - const osProperties = objects.deepClone(properties); - properties['windows'] = { - type: 'object', - description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), - properties: osProperties - }; - properties['osx'] = { - type: 'object', - description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), - properties: osProperties - }; - properties['linux'] = { - type: 'object', - description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), - properties: osProperties - }; + for (const prop in definitions['common'].properties) { + properties[prop] = { + $ref: `#/definitions/common/properties/${prop}` + }; + } + definitions[definitionId] = attributes; + Object.keys(properties).forEach(name => { // Use schema allOf property to get independent error reporting #21113 ConfigurationResolverUtils.applyDeprecatedVariableMessage(properties[name]); }); - return attributes; + + const result = { + allOf: [{ + $ref: `#/definitions/${definitionId}` + }, { + properties: { + windows: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes.") + }, + osx: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes.") + }, + linux: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes.") + } + } + }] + }; + + return result; }); } } diff --git a/lib/vscode/src/vs/workbench/contrib/debug/node/terminals.ts b/lib/vscode/src/vs/workbench/contrib/debug/node/terminals.ts index 03c4fc8f6c5d..4155cf3bfdd1 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/node/terminals.ts @@ -5,29 +5,12 @@ import * as cp from 'child_process'; import * as platform from 'vs/base/common/platform'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; -import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { getDriveLetter } from 'vs/base/common/extpath'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; -let externalTerminalService: IExternalTerminalService | undefined = undefined; -export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { - if (!externalTerminalService) { - if (platform.isWindows) { - externalTerminalService = new WindowsExternalTerminalService(undefined); - } else if (platform.isMacintosh) { - externalTerminalService = new MacExternalTerminalService(undefined); - } else if (platform.isLinux) { - externalTerminalService = new LinuxExternalTerminalService(undefined); - } else { - throw new Error('external terminals not supported on this platform'); - } - } - const config = configProvider.getConfiguration('terminal'); - return externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {}); -} function spawnAsPromised(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { @@ -47,15 +30,35 @@ function spawnAsPromised(command: string, args: string[]): Promise { }); } +let externalTerminalService: IExternalTerminalService | undefined = undefined; + +export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { + if (!externalTerminalService) { + if (platform.isWindows) { + externalTerminalService = new WindowsExternalTerminalService(); + } else if (platform.isMacintosh) { + externalTerminalService = new MacExternalTerminalService(); + } else if (platform.isLinux) { + externalTerminalService = new LinuxExternalTerminalService(); + } else { + throw new Error('external terminals not supported on this platform'); + } + } + const config = configProvider.getConfiguration('terminal'); + return externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {}); +} + export function hasChildProcesses(processId: number | undefined): Promise { if (processId) { + // if shell has at least one child process, assume that shell is busy if (platform.isWindows) { - return spawnAsPromised('wmic', ['process', 'get', 'ParentProcessId']).then(stdout => { - const pids = stdout.split('\r\n'); - return pids.some(p => parseInt(p) === processId); - }, error => { - return true; + return new Promise(async (resolve) => { + // See #123296 + const windowsProcessTree = await import('windows-process-tree'); + windowsProcessTree.getProcessTree(processId, (processTree) => { + resolve(processTree.children.length > 0); + }); }); } else { return spawnAsPromised('/usr/bin/pgrep', ['-lP', String(processId)]).then(stdout => { diff --git a/lib/vscode/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/lib/vscode/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index c66810f43cb1..fb93786d406a 100644 --- a/lib/vscode/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -149,20 +149,6 @@ suite('Debug - Debugger', () => { assert.deepStrictEqual(ae!.args, debuggerContribution.args); }); - test('schema attributes', () => { - const schemaAttribute = _debugger.getSchemaAttributes()![0]; - assert.notDeepStrictEqual(schemaAttribute, debuggerContribution.configurationAttributes); - Object.keys(debuggerContribution.configurationAttributes.launch).forEach(key => { - assert.deepStrictEqual((schemaAttribute)[key], (debuggerContribution.configurationAttributes.launch)[key]); - }); - - assert.strictEqual(schemaAttribute['additionalProperties'], false); - assert.strictEqual(!!schemaAttribute['properties']!['request'], true); - assert.strictEqual(!!schemaAttribute['properties']!['name'], true); - assert.strictEqual(!!schemaAttribute['properties']!['type'], true); - assert.strictEqual(!!schemaAttribute['properties']!['preLaunchTask'], true); - }); - test('merge platform specific attributes', () => { const ae = ExecutableDebugAdapter.platformAdapterExecutable([extensionDescriptor1, extensionDescriptor2], 'mock')!; assert.strictEqual(ae.command, platform.isLinux ? 'linuxRuntime' : (platform.isMacintosh ? 'osxRuntime' : 'winRuntime')); diff --git a/lib/vscode/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/lib/vscode/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index 508337608c4d..25a6a9fc255b 100644 --- a/lib/vscode/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -8,24 +8,6 @@ import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import * as assert from 'assert'; import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; -// -// To run the emmet tests only change .vscode/launch.json -// { -// "name": "Stacks Tests", -// "type": "node", -// "request": "launch", -// "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", -// "stopOnEntry": false, -// "args": [ -// "--timeout", -// "999999", -// "--colors", -// "-g", -// "Stacks" <<<--- Emmet -// ], -// Select the 'Stacks Tests' launch config and F5 -// - class MockGrammarContributions implements IGrammarContributions { private scopeName: string; diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 9fb0cfc9dfac..ba3363c265b6 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; @@ -36,6 +36,9 @@ import { domEvent } from 'vs/base/browser/event'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { DefaultIconPath } from 'vs/platform/extensionManagement/common/extensionManagement'; interface IExtensionProfileInformation { /** @@ -54,7 +57,7 @@ interface IExtensionProfileInformation { export interface IRuntimeExtension { originalIndex: number; description: IExtensionDescription; - marketplaceInfo: IExtension; + marketplaceInfo: IExtension | undefined; status: IExtensionsStatus; profileInfo?: IExtensionProfileInformation; unresponsiveProfile?: IExtensionHostProfile; @@ -265,8 +268,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { data.root.classList.toggle('odd', index % 2 === 1); const onError = Event.once(domEvent(data.icon, 'error')); - onError(() => data.icon.src = element.marketplaceInfo.iconUrlFallback, null, data.elementDisposables); - data.icon.src = element.marketplaceInfo.iconUrl; + onError(() => data.icon.src = element.marketplaceInfo?.iconUrlFallback || DefaultIconPath, null, data.elementDisposables); + data.icon.src = element.marketplaceInfo?.iconUrl || DefaultIconPath; if (!data.icon.complete) { data.icon.style.visibility = 'hidden'; @@ -274,7 +277,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } else { data.icon.style.visibility = 'inherit'; } - data.name.textContent = element.marketplaceInfo.displayName; + data.name.textContent = element.marketplaceInfo?.displayName || element.description.identifier.value; data.version.textContent = element.description.version; const activationTimes = element.status.activationTimes!; @@ -427,8 +430,10 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { actions.push(new Separator()); } - actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace))); - actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally))); + if (e.element!.marketplaceInfo) { + actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledWorkspace))); + actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledGlobally))); + } actions.push(new Separator()); const profileAction = this._createProfileAction(); @@ -466,18 +471,18 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { protected abstract _createProfileAction(): Action | null; } -export class ShowRuntimeExtensionsAction extends Action { - static readonly ID = 'workbench.action.showRuntimeExtensions'; - static readonly LABEL = nls.localize('showRuntimeExtensions', "Show Running Extensions"); +export class ShowRuntimeExtensionsAction extends Action2 { - constructor( - id: string, label: string, - @IEditorService private readonly _editorService: IEditorService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.showRuntimeExtensions', + title: { value: nls.localize('showRuntimeExtensions', "Show Running Extensions"), original: 'Show Running Extensions' }, + category: CATEGORIES.Developer, + f1: true + }); } - public override async run(e?: any): Promise { - await this._editorService.openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true, pinned: true }); + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IEditorService).openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true, pinned: true }); } } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 3f99c17d6385..ecc8962769eb 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -25,7 +25,7 @@ import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { UpdateAction, ReloadAction, MaliciousStatusLabelAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, @@ -69,6 +69,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; class NavBar extends Disposable { @@ -325,7 +326,7 @@ export class ExtensionEditor extends EditorPane { return disposables; } - override async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: ExtensionsInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); if (this.template) { await this.updateTemplate(input, this.template, !!options?.preserveFocus); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts deleted file mode 100644 index 9cd55e30eb51..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; - - -export class ExtensionEnablementByWorkspaceTrustRequirement extends Disposable implements IWorkbenchContribution { - - constructor( - @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IExtensionService private readonly extensionService: IExtensionService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - ) { - super(); - - this._register(workspaceTrustManagementService.onDidChangeTrust(trusted => this.onDidChangeTrustState(trusted))); - } - - private async onDidChangeTrustState(trusted: boolean): Promise { - if (trusted) { - // Untrusted -> Trusted - await this.extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); - } else { - // Trusted -> Untrusted - this.extensionService.stopExtensionHosts(); - await this.extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); - this.extensionService.startExtensionHosts(); - } - } -} diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts new file mode 100644 index 000000000000..606d019fe0e1 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustTransitionParticipant } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; + +export class ExtensionEnablementWorkspaceTrustTransitionParticipant extends Disposable implements IWorkbenchContribution { + constructor( + @IExtensionService extensionService: IExtensionService, + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, + @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, + ) { + super(); + + if (workspaceTrustManagementService.workspaceTrustEnabled) { + // The extension enablement participant will be registered only after the + // workspace trust state has been initialized. There is no need to execute + // the participant as part of the initialization process, as the workspace + // trust state is initialized before starting the extension host. + workspaceTrustManagementService.workspaceTrustInitialized.then(() => { + const workspaceTrustTransitionParticipant = new class implements IWorkspaceTrustTransitionParticipant { + async participate(trusted: boolean): Promise { + if (trusted) { + // Untrusted -> Trusted + await extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); + } else { + // Trusted -> Untrusted + if (environmentService.remoteAuthority) { + hostService.reload(); + } else { + extensionService.stopExtensionHosts(); + await extensionEnablementService.updateEnablementByWorkspaceTrustRequirement(); + extensionService.startExtensionHosts(); + } + } + } + }; + + // Execute BEFORE the workspace trust transition completes + this._register(workspaceTrustManagementService.addWorkspaceTrustTransitionParticipant(workspaceTrustTransitionParticipant)); + }); + } + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 5477d2147cf1..104d7ffc6e0a 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -18,9 +18,11 @@ import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/bro import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations'; import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations'; import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations'; +import { LanguageRecommendations } from 'vs/workbench/contrib/extensions/browser/languageRecommendations'; import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { timeout } from 'vs/base/common/async'; type IgnoreRecommendationClassification = { recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -39,6 +41,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte private readonly exeBasedRecommendations: ExeBasedRecommendations; private readonly dynamicWorkspaceRecommendations: DynamicWorkspaceRecommendations; private readonly keymapRecommendations: KeymapRecommendations; + private readonly languageRecommendations: LanguageRecommendations; public readonly activationPromise: Promise; private sessionSeed: number; @@ -65,6 +68,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations); this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations); this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations); + this.languageRecommendations = instantiationService.createInstance(LanguageRecommendations); if (!this.isEnabled()) { this.sessionSeed = 0; @@ -89,6 +93,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.fileBasedRecommendations.activate(), this.experimentalRecommendations.activate(), this.keymapRecommendations.activate(), + this.languageRecommendations.activate(), ]); this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); @@ -126,6 +131,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.fileBasedRecommendations.recommendations, ...this.workspaceRecommendations.recommendations, ...this.keymapRecommendations.recommendations, + ...this.languageRecommendations.recommendations, ]; for (const { extensionId, reason } of allRecommendations) { @@ -184,6 +190,10 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return this.toExtensionRecommendations(this.keymapRecommendations.recommendations); } + getLanguageRecommendations(): string[] { + return this.toExtensionRecommendations(this.languageRecommendations.recommendations); + } + async getWorkspaceRecommendations(): Promise { if (!this.isEnabled()) { return []; @@ -232,16 +242,20 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase()); } + // for testing + protected get workbenchRecommendationDelay() { + // remote extensions might still being installed #124119 + return 5000; + } + private async promptWorkspaceRecommendations(): Promise { const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] .map(({ extensionId }) => extensionId) .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); if (allowedRecommendations.length) { + await timeout(this.workbenchRecommendationDelay); await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations); } } - - - } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 41e39af95a13..4232ee5f9e3b 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -14,7 +14,7 @@ import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsServi import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -23,7 +23,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor, optional } from 'vs/platform/instantiation/common/instantiation'; import { KeymapExtensions } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; @@ -49,7 +49,7 @@ import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; @@ -60,7 +60,7 @@ import { IAction } from 'vs/base/common/actions'; import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { Schemas } from 'vs/base/common/network'; import { ShowRuntimeExtensionsAction } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; -import { ExtensionEnablementByWorkspaceTrustRequirement } from 'vs/workbench/contrib/extensions/browser/extensionEnablementByWorkspaceTrustRequirement'; +import { ExtensionEnablementWorkspaceTrustTransitionParticipant } from 'vs/workbench/contrib/extensions/browser/extensionEnablementWorkspaceTrustTransitionParticipant'; import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -72,6 +72,8 @@ import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { Promises } from 'vs/base/common/async'; import { EditorExtensions } from 'vs/workbench/common/editor'; import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -249,14 +251,29 @@ CommandsRegistry.registerCommand({ description: localize('workbench.extensions.installExtension.description', "Install the given extension"), args: [ { - name: localize('workbench.extensions.installExtension.arg.name', "Extension id or VSIX resource uri"), + name: 'extensionIdOrVSIXUri', + description: localize('workbench.extensions.installExtension.arg.decription', "Extension id or VSIX resource uri"), + constraint: (value: any) => typeof value === 'string' || value instanceof URI, + }, + { + name: 'options', + description: '(optional) Options for installing the extension. Object with the following properties: ' + + '`installOnlyNewlyAddedFromExtensionPackVSIX`: When enabled, VS Code installs only newly added extensions from the extension pack VSIX. This option is considered only when installing VSIX. ', + isOptional: true, schema: { - 'type': ['object', 'string'] + 'type': 'object', + 'properties': { + 'installOnlyNewlyAddedFromExtensionPackVSIX': { + 'type': 'boolean', + 'description': localize('workbench.extensions.installExtension.option.installOnlyNewlyAddedFromExtensionPackVSIX', "When enabled, VS Code installs only newly added extensions from the extension pack VSIX. This option is considered only while installing a VSIX."), + default: false + } + } } } ] }, - handler: async (accessor, arg: string | UriComponents) => { + handler: async (accessor, arg: string | UriComponents, options?: { installOnlyNewlyAddedFromExtensionPackVSIX?: boolean }) => { const extensionManagementService = accessor.get(IExtensionManagementService); const extensionGalleryService = accessor.get(IExtensionGalleryService); try { @@ -269,7 +286,7 @@ CommandsRegistry.registerCommand({ } } else { const vsix = URI.revive(arg); - await extensionManagementService.install(vsix); + await extensionManagementService.install(vsix, { installOnlyNewlyAddedFromExtensionPack: options?.installOnlyNewlyAddedFromExtensionPackVSIX }); } } catch (e) { onUnexpectedError(e); @@ -377,6 +394,8 @@ interface IExtensionActionOptions extends IAction2Options { class ExtensionsContributions extends Disposable implements IWorkbenchContribution { + private tasExperimentService?: ITASExperimentService; + constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @@ -387,6 +406,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi @IInstantiationService private readonly instantiationService: IInstantiationService, @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, + @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { super(); const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService); @@ -409,6 +429,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi hasWebServerContext.set(true); } + this.tasExperimentService = tasExperimentService; this.registerGlobalActions(); this.registerContextMenuActions(); this.registerQuickAccessProvider(); @@ -500,7 +521,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.CommandPalette, when: CONTEXT_HAS_GALLERY }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@category:"programming languages" @sort:installs ')) + run: async () => { + const recommended = await this.tasExperimentService?.getTreatment('recommendedLanguages'); + runAction(this.instantiationService.createInstance(SearchExtensionsAction, recommended ? '@recommended:languages ' : '@category:"programming languages" @sort:installs ')); + } }); this.registerExtensionAction({ @@ -876,10 +900,21 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); this.registerExtensionAction({ - id: 'workbench.extensions.action.listTrustRequiredExtensions', - title: { value: localize('showTrustRequiredExtensions', "Show Extensions Requiring Trust"), original: 'Show Extensions Requiring Trust' }, + id: LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, + title: { value: localize('showWorkspaceUnsupportedExtensions', "Show Extensions Unsupported By Workspace"), original: 'Show Extensions Unsupported By Workspace' }, category: ExtensionsLocalizedLabel, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@trustRequired')) + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 6, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('workspace unsupported filter', "Workspace Unsupported") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@workspaceUnsupported')) }); this.registerExtensionAction({ @@ -1325,8 +1360,8 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementByWorkspaceTrustRequirement, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrustTransitionParticipant, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); // Running Extensions -const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ShowRuntimeExtensionsAction), 'Show Running Extensions', CATEGORIES.Developer.value); +registerAction2(ShowRuntimeExtensionsAction); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 458801de1eee..c1dbb44158a3 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -18,7 +18,7 @@ import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, I import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, getWorkpaceSupportTypeMessage } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IFileService, IFileContent } from 'vs/platform/files/common/files'; @@ -59,8 +59,9 @@ import { ILogService } from 'vs/platform/log/common/log'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { isWeb } from 'vs/base/common/platform'; -import { isWorkspaceTrustEnabled } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -2085,10 +2086,12 @@ export class SystemDisabledWarningAction extends ExtensionAction { constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @ILabelService private readonly labelService: ILabelService, + @ICommandService private readonly commandService: ICommandService, + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super('extensions.install', '', `${SystemDisabledWarningAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); @@ -2104,6 +2107,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { update(): void { this.class = `${SystemDisabledWarningAction.CLASS} hide`; this.tooltip = ''; + this.enabled = false; if ( !this.extension || !this.extension.local || @@ -2113,10 +2117,17 @@ export class SystemDisabledWarningAction extends ExtensionAction { ) { return; } - if (this.extension.enablementState === EnablementState.DisabledByVirtualWorkspace) { - this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; - this.tooltip = localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces."); - return; + + if (isVirtualWorkspace(this.contextService.getWorkspace())) { + const virtualSupportType = this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(this.extension.local.manifest); + if (virtualSupportType !== true) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + const details = getWorkpaceSupportTypeMessage(this.extension.local.manifest.capabilities?.virtualWorkspaces); + this.tooltip = details || (virtualSupportType === 'limited' ? + localize('extension limited because of virtual workspace', "This extension has limited features because the current workspace is virtual.") : + localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces.")); + return; + } } if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { if (isLanguagePackExtension(this.extension.local.manifest)) { @@ -2159,14 +2170,25 @@ export class SystemDisabledWarningAction extends ExtensionAction { return; } } - if (isWorkspaceTrustEnabled(this.configurationService) && this.extension.enablementState === EnablementState.DisabledByTrustRequirement) { + + const untrustedSupportType = this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(this.extension.local.manifest); + if (this.workspaceTrustService.workspaceTrustEnabled && untrustedSupportType !== true && !this.workspaceTrustService.isWorkpaceTrusted()) { + const untrustedDetails = getWorkpaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces); + this.enabled = true; this.class = `${SystemDisabledWarningAction.TRUST_CLASS}`; - this.tooltip = localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted"); + this.tooltip = untrustedDetails || (untrustedSupportType === 'limited' ? + localize('extension limited because of trust requirement', "This extension has limited features because the current workspace is not trusted.") : + localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted.")); return; } } override run(): Promise { + // Only enabled by the workspace trust version of this action + // If other actions enable, add a new member to control this + if (this.enabled) { + this.commandService.executeCommand('workbench.trust.manage'); + } return Promise.resolve(null); } } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.ts new file mode 100644 index 000000000000..fa1c28987541 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { getLocation, parse } from 'vs/base/common/json'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { CompletionContext, CompletionList, CompletionProviderRegistry, CompletionItemKind, CompletionItem } from 'vs/editor/common/modes'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Range } from 'vs/editor/common/core/range'; + + +export class ExtensionsCompletionItemsProvider extends Disposable implements IWorkbenchContribution { + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + ) { + super(); + + this._register(CompletionProviderRegistry.register({ language: 'jsonc', pattern: '**/settings.json' }, { + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { + const getWordRangeAtPosition = (model: ITextModel, position: Position): Range | null => { + const wordAtPosition = model.getWordAtPosition(position); + return wordAtPosition ? new Range(position.lineNumber, wordAtPosition.startColumn, position.lineNumber, wordAtPosition.endColumn) : null; + }; + + const location = getLocation(model.getValue(), model.getOffsetAt(position)); + const range = getWordRangeAtPosition(model, position) ?? Range.fromPositions(position, position); + + // extensions.supportUntrustedWorkspaces + if (location.path[0] === 'extensions.supportUntrustedWorkspaces' && location.path.length === 2 && location.isAtPropertyKey) { + let alreadyConfigured: string[] = []; + try { + alreadyConfigured = Object.keys(parse(model.getValue())['extensions.supportUntrustedWorkspaces']); + } catch (e) {/* ignore error */ } + + return { suggestions: await this.provideSupportUntrustedWorkspacesExtensionProposals(alreadyConfigured, range) }; + } + + return { suggestions: [] }; + } + })); + } + + private async provideSupportUntrustedWorkspacesExtensionProposals(alreadyConfigured: string[], range: Range): Promise { + const suggestions: CompletionItem[] = []; + const installedExtensions = (await this.extensionManagementService.getInstalled()).filter(e => e.manifest.main); + const proposedExtensions = installedExtensions.filter(e => alreadyConfigured.indexOf(e.identifier.id) === -1); + + if (proposedExtensions.length) { + suggestions.push(...proposedExtensions.map(e => { + const text = `"${e.identifier.id}": {\n\t"supported": true,\n\t"version": "${e.manifest.version}"\n},`; + return { label: e.identifier.id, kind: CompletionItemKind.Value, insertText: text, filterText: text, range }; + })); + } else { + const text = '"vscode.csharp": {\n\t"supported": true,\n\t"version": "0.0.0"\n},'; + suggestions.push({ label: localize('exampleExtension', "Example"), kind: CompletionItemKind.Value, insertText: text, filterText: text, range }); + } + + return suggestions; + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts index 921231f3f869..8aecf0ed0c35 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -28,7 +28,7 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid super(InstallExtensionQuickAccessProvider.PREFIX); } - protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> { + protected _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> { // Nothing typed if (!filter) { @@ -100,7 +100,7 @@ export class ManageExtensionsQuickAccessProvider extends PickerQuickAccessProvid super(ManageExtensionsQuickAccessProvider.PREFIX); } - protected getPicks(): Array { + protected _getPicks(): Array { return [{ label: localize('manage', "Press Enter to manage your extensions."), accept: () => openExtensionsViewlet(this.viewletService) diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index e09a04de0c79..b243f17fa2e0 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -22,7 +22,7 @@ import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAct import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, TrustRequiredOnStartExtensionsView, TrustRequiredOnDemandExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; @@ -54,12 +54,13 @@ import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { VirtualWorkspaceContext, WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { isWeb } from 'vs/base/common/platform'; +import { isIOS, isWeb } from 'vs/base/common/platform'; import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { WorkspaceTrustContext } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); @@ -70,8 +71,7 @@ const HasInstalledExtensionsContext = new RawContextKey('hasInstalledEx const HasInstalledWebExtensionsContext = new RawContextKey('hasInstalledWebExtensions', false); const BuiltInExtensionsContext = new RawContextKey('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); -const TrustRequiredExtensionsContext = new RawContextKey('trustRequiredExtensions', false); -const SearchTrustRequiredExtensionsContext = new RawContextKey('searchTrustRequiredExtensions', false); +const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey('searchUnsupportedWorkspaceExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { @@ -104,7 +104,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio viewDescriptors.push(...this.createBuiltinExtensionsViewDescriptors()); /* Trust Required extensions views */ - viewDescriptors.push(...this.createTrustRequiredExtensionsViewDescriptors()); + viewDescriptors.push(...this.createUnsupportedWorkspaceExtensionsViewDescriptors()); Registry.as(Extensions.ViewsRegistry).registerViews(viewDescriptors, this.container); } @@ -344,13 +344,13 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio }); /* - * View used for searching trustRequired extensions + * View used for searching workspace unsupported extensions */ viewDescriptors.push({ - id: 'workbench.views.extensions.searchTrustRequired', - name: localize('trustRequired', "Trust Required"), + id: 'workbench.views.extensions.searchWorkspaceUnsupported', + name: localize('workspaceUnsupported', "Workspace Unsupported"), ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('searchTrustRequiredExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchWorkspaceUnsupportedExtensions')), }); return viewDescriptors; @@ -405,21 +405,35 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return viewDescriptors; } - private createTrustRequiredExtensionsViewDescriptors(): IViewDescriptor[] { + private createUnsupportedWorkspaceExtensionsViewDescriptors(): IViewDescriptor[] { const viewDescriptors: IViewDescriptor[] = []; viewDescriptors.push({ - id: 'workbench.views.extensions.trustRequiredOnStartExtensions', - name: localize('trustRequiredOnStartExtensions', "Trust Required To Enable"), - ctorDescriptor: new SyncDescriptor(TrustRequiredOnStartExtensionsView, [{}]), - when: ContextKeyExpr.has('trustRequiredExtensions'), + id: 'workbench.views.extensions.untrustedUnsupportedExtensions', + name: localize('untrustedUnsupportedExtensions', "Disabled in Restricted Mode"), + ctorDescriptor: new SyncDescriptor(UntrustedWorkspaceUnsupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(WorkspaceTrustContext.IsTrusted.negate(), SearchUnsupportedWorkspaceExtensionsContext), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.untrustedPartiallySupportedExtensions', + name: localize('untrustedPartiallySupportedExtensions', "Limited in Restricted Mode"), + ctorDescriptor: new SyncDescriptor(UntrustedWorkspacePartiallySupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(WorkspaceTrustContext.IsTrusted.negate(), SearchUnsupportedWorkspaceExtensionsContext), }); viewDescriptors.push({ - id: 'workbench.views.extensions.trustRequiredOnDemandExtensions', - name: localize('trustRequiredOnDemandExtensions', "Trust Required For Features"), - ctorDescriptor: new SyncDescriptor(TrustRequiredOnDemandExtensionsView, [{}]), - when: ContextKeyExpr.has('trustRequiredExtensions'), + id: 'workbench.views.extensions.virtualUnsupportedExtensions', + name: localize('virtualUnsupportedExtensions', "Disabled in Virtual Workspaces"), + ctorDescriptor: new SyncDescriptor(VirtualWorkspaceUnsupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.virtualPartiallySupportedExtensions', + name: localize('virtualPartiallySupportedExtensions', "Limited in Virtual Workspaces"), + ctorDescriptor: new SyncDescriptor(VirtualWorkspacePartiallySupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext), }); return viewDescriptors; @@ -440,8 +454,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private hasInstalledWebExtensionsContextKey: IContextKey; private builtInExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; - private trustRequiredExtensionsContextKey: IContextKey; - private searchTrustRequiredExtensionsContextKey: IContextKey; + private searchWorkspaceUnsupportedExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; @@ -477,8 +490,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); - this.trustRequiredExtensionsContextKey = TrustRequiredExtensionsContext.bindTo(contextKeyService); - this.searchTrustRequiredExtensionsContextKey = SearchTrustRequiredExtensionsContext.bindTo(contextKeyService); + this.searchWorkspaceUnsupportedExtensionsContextKey = SearchUnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService); @@ -557,6 +569,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE const header = append(this.root, $('.header')); const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); + const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : ''; this.searchBox = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, header, { @@ -585,7 +598,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); this._register(this.onDidChangeVisibility(visible => { - if (visible) { + if (visible && !isIOS) { this.searchBox!.focus(); } })); @@ -637,7 +650,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } override focus(): void { - if (this.searchBox) { + if (this.searchBox && !isIOS) { this.searchBox.focus(); } } @@ -711,8 +724,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value)); - this.trustRequiredExtensionsContextKey.set(ExtensionsListView.isTrustRequiredExtensionsQuery(value)); - this.searchTrustRequiredExtensionsContextKey.set(ExtensionsListView.isSearchTrustRequiredExtensionsQuery(value)); + this.searchWorkspaceUnsupportedExtensionsContextKey.set(ExtensionsListView.isSearchWorkspaceUnsupportedExtensionsQuery(value)); this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 67e0af117291..7ee9c0358268 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -37,7 +37,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction, Action, Separator, ActionRunner } from 'vs/base/common/actions'; -import { ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType, ExtensionVirtualWorkpaceSupportType, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; @@ -49,6 +49,8 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -121,12 +123,14 @@ export class ExtensionsListView extends ViewPane { @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWorkbenchExtensionManagementService protected readonly extensionManagementService: IWorkbenchExtensionManagementService, + @IWorkspaceContextService protected readonly workspaceService: IWorkspaceContextService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super({ ...(viewletViewOptions as IViewPaneOptions), @@ -394,8 +398,8 @@ export class ExtensionsListView extends ViewPane { extensions = this.filterEnabledExtensions(local, runningExtensions, query, options); } - else if (/@trustRequired/i.test(value)) { - extensions = this.filterTrustRequiredExtensions(local, query, options); + else if (/@workspaceUnsupported/i.test(value)) { + extensions = this.filterWorkspaceUnsupportedExtensions(local, query, options); } return { extensions, canIncludeInstalledExtensions }; @@ -548,32 +552,44 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(result, options); } - private filterTrustRequiredExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { - let value = query.value; - const onStartOnly = /@trustRequired:onStart/i.test(value); - if (onStartOnly) { - value = value.replace(/@trustRequired:onStart/g, ''); - } - const onDemandOnly = /@trustRequired:onDemand/i.test(value); - if (onDemandOnly) { - value = value.replace(/@trustRequired:onDemand/g, ''); - } + private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { - value = value.replace(/@trustRequired/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); + // shows local extensions which are restricted or disabled in the current workspace because of the extension's capability + + const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace()); + const inRestrictedWorkspace = !this.workspaceTrustManagementService.isWorkpaceTrusted(); + if (!inVirtualWorkspace && !inRestrictedWorkspace) { + return []; + } - const result = local.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) !== true && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); + let queryString = query.value; // @sortby is already filtered out - if (onStartOnly) { - const onStartExtensions = result.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === false); - return this.sortExtensions(onStartExtensions, options); + const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i); + if (!match) { + return []; } + const type = match[1]?.toLowerCase(); + const partial = !!match[2]; + const nameFilter = match[3]?.toLowerCase(); - if (onDemandOnly) { - const onDemandExtensions = result.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === 'limited'); - return this.sortExtensions(onDemandExtensions, options); + if (nameFilter) { + local = local.filter(extension => extension.name.toLowerCase().indexOf(nameFilter) > -1 || extension.displayName.toLowerCase().indexOf(nameFilter) > -1); } - return this.sortExtensions(result, options); + const hasVirtualSupportType = (extension: IExtension, supportType: ExtensionVirtualWorkpaceSupportType) => extension.local && this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.local.manifest) === supportType; + const hasRestrictedSupportType = (extension: IExtension, supportType: ExtensionUntrustedWorkpaceSupportType) => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === supportType; + + if (type === 'virtual') { + // show limited and disabled extensions unless disabled because of a untrusted workspace + local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'limited' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false))); + } else if (type === 'untrusted') { + // show limited and disabled extensions unless disabled because of a virtual workspace + local = local.filter(extension => inRestrictedWorkspace && hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false))); + } else { + // show extensions that are restricted or disabled in the current workspace + local = local.filter(extension => inVirtualWorkspace && !hasVirtualSupportType(extension, true) || inRestrictedWorkspace && !hasRestrictedSupportType(extension, true)); + } + return this.sortExtensions(local, options); } @@ -706,6 +722,7 @@ export class ExtensionsListView extends ViewPane { private isRecommendationsQuery(query: Query): boolean { return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value) || ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value) || ExtensionsListView.isExeRecommendedExtensionsQuery(query.value) || /@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) @@ -723,6 +740,11 @@ export class ExtensionsListView extends ViewPane { return this.getKeymapRecommendationsModel(query, options, token); } + // Language recommendations + if (ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)) { + return this.getLanguageRecommendationsModel(query, options, token); + } + // Exe recommendations if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) { return this.getExeRecommendationsModel(query, options, token); @@ -787,6 +809,14 @@ export class ExtensionsListView extends ViewPane { return new PagedModel(installableRecommendations); } + private async getLanguageRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:languages/g, '').trim().toLowerCase(); + const recommendations = this.extensionRecommendationsService.getLanguageRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-languages' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + return new PagedModel(installableRecommendations); + } + private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase(); const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe); @@ -964,9 +994,7 @@ export class ExtensionsListView extends ViewPane { || this.isBuiltInExtensionsQuery(query) || this.isSearchBuiltInExtensionsQuery(query) || this.isBuiltInGroupExtensionsQuery(query) - || this.isSearchTrustRequiredExtensionsQuery(query) - || this.isTrustRequiredExtensionsQuery(query) - || this.isTrustRequiredGroupExtensionsQuery(query); + || this.isSearchWorkspaceUnsupportedExtensionsQuery(query); } static isSearchBuiltInExtensionsQuery(query: string): boolean { @@ -981,16 +1009,8 @@ export class ExtensionsListView extends ViewPane { return /^\s*@builtin:.+$/i.test(query.trim()); } - static isSearchTrustRequiredExtensionsQuery(query: string): boolean { - return /@trustRequired\s.+/i.test(query); - } - - static isTrustRequiredExtensionsQuery(query: string): boolean { - return /^\s*@trustRequired$/i.test(query.trim()); - } - - static isTrustRequiredGroupExtensionsQuery(query: string): boolean { - return /^\s*@trustRequired:.+$/i.test(query.trim()); + static isSearchWorkspaceUnsupportedExtensionsQuery(query: string): boolean { + return /^\s*@workspaceUnsupported(:(untrusted|virtual)(Partial)?)?(\s|$)/i.test(query); } static isInstalledExtensionsQuery(query: string): boolean { @@ -1029,6 +1049,10 @@ export class ExtensionsListView extends ViewPane { return /@recommended:keymaps/i.test(query); } + static isLanguageRecommendedExtensionsQuery(query: string): boolean { + return /@recommended:languages/i.test(query); + } + override focus(): void { super.focus(); if (!this.list) { @@ -1088,15 +1112,46 @@ export class BuiltInProgrammingLanguageExtensionsView extends ExtensionsListView } } -export class TrustRequiredOnStartExtensionsView extends ExtensionsListView { +function toSpecificWorkspaceUnsupportedQuery(query: string, qualifier: string): string | undefined { + if (!query) { + return '@workspaceUnsupported:' + qualifier; + } + const match = query.match(new RegExp(`@workspaceUnsupported(:${qualifier})?(\\s|$)`, 'i')); + if (match) { + if (!match[1]) { + return query.replace(/@workspaceUnsupported/gi, '@workspaceUnsupported:' + qualifier); + } + return query; + } + return undefined; +} + + +export class UntrustedWorkspaceUnsupportedExtensionsView extends ExtensionsListView { + override async show(query: string): Promise> { + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrusted'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); + } +} + +export class UntrustedWorkspacePartiallySupportedExtensionsView extends ExtensionsListView { + override async show(query: string): Promise> { + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrustedPartial'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); + } +} + +export class VirtualWorkspaceUnsupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@trustRequired') ? this.showEmptyModel() : super.show('@trustRequired:onStart'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtual'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } -export class TrustRequiredOnDemandExtensionsView extends ExtensionsListView { +export class VirtualWorkspacePartiallySupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@trustRequired') ? this.showEmptyModel() : super.show('@trustRequired:onDemand'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtualPartial'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/lib/vscode/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts new file mode 100644 index 000000000000..9258305b84aa --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; + +export class LanguageRecommendations extends ExtensionRecommendations { + + private _recommendations: ExtensionRecommendation[] = []; + get recommendations(): ReadonlyArray { return this._recommendations; } + + constructor( + @IProductService private readonly productService: IProductService, + ) { + super(); + } + + protected async doActivate(): Promise { + if (this.productService.languageExtensionTips) { + this._recommendations = this.productService.languageExtensionTips.map(extensionId => ({ + extensionId: extensionId.toLowerCase(), + reason: { + reasonId: ExtensionRecommendationReason.Application, + reasonText: '' + } + })); + } + } + +} + diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index e1cac15cc906..fe3716f36d47 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -13,7 +13,7 @@ export class Query { } static suggestions(query: string): string[] { - const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'trustRequired', 'sort', 'category', 'tag', 'ext', 'id'] as const; + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'workspaceUnsupported', 'sort', 'category', 'tag', 'ext', 'id'] as const; const subcommands = { 'sort': ['installs', 'rating', 'name'], 'category': EXTENSION_CATEGORIES.map(c => `"${c.toLowerCase()}"`), diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/common/extensions.ts b/lib/vscode/src/vs/workbench/contrib/extensions/common/extensions.ts index 127ae556dae8..adc2776375b5 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -156,6 +156,8 @@ export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.to export const SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID = 'workbench.extensions.action.installVSIX'; export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX'; +export const LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID = 'workbench.extensions.action.listWorkspaceUnsupportedExtensions'; + // Context Keys export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index ad1f9febec2b..2c142d26a6b1 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/common/extensionsInput.ts @@ -6,7 +6,8 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { join } from 'vs/base/common/path'; @@ -19,6 +20,10 @@ export class ExtensionsInput extends EditorInput { return ExtensionsInput.ID; } + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + } + override get resource() { return URI.from({ scheme: Schemas.extension, @@ -36,10 +41,6 @@ export class ExtensionsInput extends EditorInput { return localize('extensionsInputName', "Extension: {0}", this.extension.displayName); } - override canSplit(): boolean { - return false; - } - override matches(other: unknown): boolean { if (super.matches(other)) { return true; diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts b/lib/vscode/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts index e5ff9281fd03..c153c41d1071 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/common/runtimeExtensionsInput.ts @@ -5,7 +5,8 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; export class RuntimeExtensionsInput extends EditorInput { @@ -15,6 +16,10 @@ export class RuntimeExtensionsInput extends EditorInput { return RuntimeExtensionsInput.ID; } + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + } + static _instance: RuntimeExtensionsInput; static get instance() { if (!RuntimeExtensionsInput._instance || RuntimeExtensionsInput._instance.isDisposed()) { @@ -33,10 +38,6 @@ export class RuntimeExtensionsInput extends EditorInput { return nls.localize('extensionsInputName', "Running Extensions"); } - override canSplit(): boolean { - return false; - } - override matches(other: unknown): boolean { return other instanceof RuntimeExtensionsInput; } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index ff347a59dc57..ac64051f0aaa 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -10,11 +10,11 @@ import { IExtensionHostProfile, ProfileSession, IExtensionService } from 'vs/wor import { Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; -import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; +import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { randomPort } from 'vs/base/node/ports'; +import { randomPort } from 'vs/base/common/ports'; import { IProductService } from 'vs/platform/product/common/productService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -82,6 +82,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio if (visible) { const indicator: IStatusbarEntry = { + name: nls.localize('status.profiler', "Extension Profiler"), text: nls.localize('profilingExtensionHost', "Profiling Extension Host"), showProgress: true, ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"), @@ -98,7 +99,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); if (!this.profilingStatusBarIndicator) { - this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', nls.localize('status.profiler', "Extension Profiler"), StatusbarAlignment.RIGHT); + this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', StatusbarAlignment.RIGHT); } else { this.profilingStatusBarIndicator.update(indicator); } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 2c5810945595..555bb6af1163 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -3,136 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { RuntimeExtensionsEditor, IExtensionHostProfileService, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; -import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction'; -import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, ActiveEditorContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/extensionProfileService'; -import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; -import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions'; -import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { ExtensionRecommendationNotificationServiceChannel } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; -import { Codicon } from 'vs/base/common/codicons'; // Singletons registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); - -// Running Extensions Editor -Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create(RuntimeExtensionsEditor, RuntimeExtensionsEditor.ID, localize('runtimeExtension', "Running Extensions")), - [new SyncDescriptor(RuntimeExtensionsInput)] -); - -class RuntimeExtensionsInputSerializer implements IEditorInputSerializer { - canSerialize(editorInput: EditorInput): boolean { - return true; - } - serialize(editorInput: EditorInput): string { - return ''; - } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - return RuntimeExtensionsInput.instance; - } -} - -Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(RuntimeExtensionsInput.ID, RuntimeExtensionsInputSerializer); - - -// Global actions -const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); - -class ExtensionsContributions implements IWorkbenchContribution { - - constructor( - @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, - @ISharedProcessService sharedProcessService: ISharedProcessService, - ) { - sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); - const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction); - actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel); - } -} - -workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); - -// Register Commands - -CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(DebugExtensionHostAction).run(); -}); - -CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(StopExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(SaveExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL).run(); -}); - -// Running extensions - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: DebugExtensionHostAction.ID, - title: DebugExtensionHostAction.LABEL, - icon: Codicon.debugStart - }, - group: 'navigation', - when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StartExtensionHostProfileAction.ID, - title: StartExtensionHostProfileAction.LABEL, - icon: Codicon.circleFilled - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StopExtensionHostProfileAction.ID, - title: StopExtensionHostProfileAction.LABEL, - icon: Codicon.debugStop - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: SaveExtensionHostProfileAction.ID, - title: SaveExtensionHostProfileAction.LABEL, - icon: Codicon.saveAll, - precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) -}); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts index 96b09731b4e6..0320b2f08b7f 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts @@ -11,14 +11,14 @@ import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { joinPath } from 'vs/base/common/resources'; -import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; +import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; +import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts similarity index 97% rename from lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts rename to lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts index 45eb957a3791..6787e95d718a 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts @@ -10,7 +10,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { randomPort } from 'vs/base/node/ports'; +import { randomPort } from 'vs/base/common/ports'; export class DebugExtensionHostAction extends Action { static readonly ID = 'workbench.extensions.action.debugExtensionHost'; diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts new file mode 100644 index 000000000000..560131188671 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; +import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction'; +import { IEditorInputSerializer, IEditorInputFactoryRegistry, ActiveEditorContext, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions'; +import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { ExtensionRecommendationNotificationServiceChannel } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; +import { Codicon } from 'vs/base/common/codicons'; + +// Running Extensions Editor +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create(RuntimeExtensionsEditor, RuntimeExtensionsEditor.ID, localize('runtimeExtension', "Running Extensions")), + [new SyncDescriptor(RuntimeExtensionsInput)] +); + +class RuntimeExtensionsInputSerializer implements IEditorInputSerializer { + canSerialize(editorInput: EditorInput): boolean { + return true; + } + serialize(editorInput: EditorInput): string { + return ''; + } + deserialize(instantiationService: IInstantiationService): EditorInput { + return RuntimeExtensionsInput.instance; + } +} + +Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(RuntimeExtensionsInput.ID, RuntimeExtensionsInputSerializer); + + +// Global actions + +class ExtensionsContributions implements IWorkbenchContribution { + + constructor( + @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); + registerAction2(OpenExtensionsFolderAction); + } +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); + +// Register Commands + +CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(DebugExtensionHostAction).run(); +}); + +CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL).run(); +}); + +CommandsRegistry.registerCommand(StopExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL).run(); +}); + +CommandsRegistry.registerCommand(SaveExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL).run(); +}); + +// Running extensions + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: DebugExtensionHostAction.ID, + title: DebugExtensionHostAction.LABEL, + icon: Codicon.debugStart + }, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: StartExtensionHostProfileAction.ID, + title: StartExtensionHostProfileAction.LABEL, + icon: Codicon.circleFilled + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: StopExtensionHostProfileAction.ID, + title: StopExtensionHostProfileAction.LABEL, + icon: Codicon.debugStop + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: SaveExtensionHostProfileAction.ID, + title: SaveExtensionHostProfileAction.LABEL, + icon: Codicon.saveAll, + precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) +}); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts index 0ff09d630a05..2f86974b1427 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsActions.ts @@ -4,31 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { Schemas } from 'vs/base/common/network'; - -export class OpenExtensionsFolderAction extends Action { - - static readonly ID = 'workbench.extensions.action.openExtensionsFolder'; - static readonly LABEL = localize('openExtensionsFolder', "Open Extensions Folder"); - - constructor( - id: string, - label: string, - @INativeHostService private readonly nativeHostService: INativeHostService, - @IFileService private readonly fileService: IFileService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService - ) { - super(id, label, undefined, true); +import { Action2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ExtensionsLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; + +export class OpenExtensionsFolderAction extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.openExtensionsFolder', + title: { value: localize('openExtensionsFolder', "Open Extensions Folder"), original: 'Open Extensions Folder' }, + category: ExtensionsLocalizedLabel, + f1: true + }); } - override async run(): Promise { - const extensionsHome = URI.file(this.environmentService.extensionsPath); - const file = await this.fileService.resolve(extensionsHome); + async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + const fileService = accessor.get(IFileService); + const environmentService = accessor.get(INativeWorkbenchEnvironmentService); + + const extensionsHome = URI.file(environmentService.extensionsPath); + const file = await fileService.resolve(extensionsHome); let itemToShow: URI; if (file.children && file.children.length > 0) { @@ -38,7 +40,7 @@ export class OpenExtensionsFolderAction extends Action { } if (itemToShow.scheme === Schemas.file) { - return this.nativeHostService.showItemInFolder(itemToShow.fsPath); + return nativeHostService.showItemInFolder(itemToShow.fsPath); } } } diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions.ts similarity index 100% rename from lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts rename to lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions.ts diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts similarity index 100% rename from lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts rename to lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts similarity index 95% rename from lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts rename to lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index a5c303f843a1..ea3f8ed3b89e 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -17,9 +17,9 @@ import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/cont import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; +import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction'; +import { ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction'; import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; @@ -105,7 +105,15 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { } protected _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null { - return this._instantiationService.createInstance(ReportExtensionIssueAction, element); + if (element.marketplaceInfo) { + return this._instantiationService.createInstance(ReportExtensionIssueAction, { + description: element.description, + marketplaceInfo: element.marketplaceInfo, + status: element.status, + unresponsiveProfile: element.unresponsiveProfile + }); + } + return null; } protected _createSaveExtensionHostProfileAction(): Action | null { diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index efedc1c28dd4..9798f435ebf0 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -175,6 +175,12 @@ function aGalleryExtension(name: string, properties: any = {}, galleryExtensionP return galleryExtension; } +class TestExtensionRecommendationsService extends ExtensionRecommendationsService { + protected override get workbenchRecommendationDelay() { + return 0; + } +} + suite('ExtensionRecommendationsService Test', () => { let workspaceService: IWorkspaceContextService; let instantiationService: TestInstantiationService; @@ -311,7 +317,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.strictEqual(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); @@ -321,7 +327,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -350,7 +356,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await Event.toPromise(promptedEmitter.event); const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); @@ -379,7 +385,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -389,7 +395,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if showRecommendationsOnlyOnDemand is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -407,7 +413,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored @@ -425,7 +431,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored @@ -447,7 +453,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); @@ -467,7 +473,7 @@ suite('ExtensionRecommendationsService Test', () => { await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; let recommendations = testObject.getAllRecommendationsWithReason(); @@ -499,7 +505,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget); extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation(ignoredExtensionId, true); @@ -514,7 +520,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); @@ -533,7 +539,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); diff --git a/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 36890e2ac3d0..33cd340d10fb 100644 --- a/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -176,14 +176,14 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(4, actual.rating); assert.strictEqual(100, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual(['pub.1', 'pub.2'], actual.dependencies); + assert.deepStrictEqual(['pub.1', 'pub.2'], actual.dependencies); }); }); test('test for empty installed extensions', async () => { testObject = await aWorkbenchService(); - assert.deepEqual([], testObject.local); + assert.deepStrictEqual([], testObject.local); }); test('test for installed extensions', async () => { @@ -233,7 +233,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(undefined, actual.rating); assert.strictEqual(undefined, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual(['pub.1', 'pub.2'], actual.dependencies); + assert.deepStrictEqual(['pub.1', 'pub.2'], actual.dependencies); actual = actuals[1]; assert.strictEqual(ExtensionType.System, actual.type); @@ -251,7 +251,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(undefined, actual.rating); assert.strictEqual(undefined, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual([], actual.dependencies); + assert.deepStrictEqual([], actual.dependencies); }); test('test installed extensions get syncs with gallery', async () => { @@ -327,7 +327,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(4, actual.rating); assert.strictEqual(100, actual.ratingCount); assert.strictEqual(true, actual.outdated); - assert.deepEqual(['pub.1'], actual.dependencies); + assert.deepStrictEqual(['pub.1'], actual.dependencies); actual = actuals[1]; assert.strictEqual(ExtensionType.System, actual.type); @@ -345,7 +345,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(undefined, actual.rating); assert.strictEqual(undefined, actual.ratingCount); assert.strictEqual(false, actual.outdated); - assert.deepEqual([], actual.dependencies); + assert.deepStrictEqual([], actual.dependencies); }); }); diff --git a/lib/vscode/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/lib/vscode/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 148401905f6d..daa8d8ec03a2 100644 --- a/lib/vscode/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; -import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; import { MenuId, MenuRegistry, IMenuItem } from 'vs/platform/actions/common/actions'; import { ITerminalService as IIntegratedTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ResourceContextKey } from 'vs/workbench/common/resources'; @@ -26,6 +25,7 @@ import { isWeb, isWindows } from 'vs/base/common/platform'; import { dirname, basename } from 'vs/base/common/path'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; CommandsRegistry.registerCommand({ @@ -42,13 +42,10 @@ CommandsRegistry.registerCommand({ return fileService.resolveAll(resources.map(r => ({ resource: r }))).then(async stats => { const targets = distinct(stats.filter(data => data.success)); // Always use integrated terminal when using a remote - const useIntegratedTerminal = remoteAgentService.getConnection() || configurationService.getValue().terminal.explorerKind === 'integrated'; + const config = configurationService.getValue(); + const useIntegratedTerminal = remoteAgentService.getConnection() || config.terminal.explorerKind === 'integrated'; if (useIntegratedTerminal) { - - // TODO: Use uri for cwd in createterminal - - const opened: { [path: string]: boolean } = {}; targets.map(({ stat }) => { const resource = stat!.resource; @@ -75,7 +72,7 @@ CommandsRegistry.registerCommand({ }); } else { distinct(targets.map(({ stat }) => stat!.isDirectory ? stat!.resource.fsPath : dirname(stat!.resource.fsPath))).forEach(cwd => { - terminalService!.openTerminal(cwd); + terminalService!.openTerminal(config.terminal.external, cwd); }); } }); @@ -121,8 +118,7 @@ export class ExternalTerminalContribution extends Disposable implements IWorkben this._openInTerminalMenuItem.command.title = nls.localize('scopedConsoleAction.integrated', "Open in Integrated Terminal"); return; } - - if (isWindows && config.external.windowsExec) { + if (isWindows && config.external?.windowsExec) { const file = basename(config.external.windowsExec); if (file === 'wt' || file === 'wt.exe') { this._openInTerminalMenuItem.command.title = nls.localize('scopedConsoleAction.wt', "Open in Windows Terminal"); diff --git a/lib/vscode/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts b/lib/vscode/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts new file mode 100644 index 000000000000..b7acdba1aec5 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as paths from 'vs/base/common/path'; +import { DEFAULT_TERMINAL_OSX, IExternalTerminalService, IExternalTerminalSettings } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Schemas } from 'vs/base/common/network'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/electron-sandbox/externalTerminalMainService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const OPEN_NATIVE_CONSOLE_COMMAND_ID = 'workbench.action.terminal.openNativeConsole'; +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: OPEN_NATIVE_CONSOLE_COMMAND_ID, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C, + when: KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED, + weight: KeybindingWeight.WorkbenchContrib, + handler: async (accessor) => { + const historyService = accessor.get(IHistoryService); + // Open external terminal in local workspaces + const terminalService = accessor.get(IExternalTerminalService); + const configurationService = accessor.get(IConfigurationService); + const root = historyService.getLastActiveWorkspaceRoot(Schemas.file); + const config = configurationService.getValue('terminal.external'); + if (root) { + terminalService.openTerminal(config, root.fsPath); + } else { + // Opens current file's folder, if no folder is open in editor + const activeFile = historyService.getLastActiveFile(Schemas.file); + if (activeFile) { + terminalService.openTerminal(config, paths.dirname(activeFile.fsPath)); + } else { + const pathService = accessor.get(IPathService); + const userHome = await pathService.userHome(); + terminalService.openTerminal(config, userHome.fsPath); + } + } + } +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: OPEN_NATIVE_CONSOLE_COMMAND_ID, + title: { value: nls.localize('globalConsoleAction', "Open New External Terminal"), original: 'Open New External Terminal' } + } +}); + +export class ExternalTerminalContribution implements IWorkbenchContribution { + + public _serviceBrand: undefined; + constructor(@IExternalTerminalMainService private readonly _externalTerminalService: IExternalTerminalMainService) { + this._updateConfiguration(); + } + + private async _updateConfiguration(): Promise { + const terminals = await this._externalTerminalService.getDefaultTerminalForPlatforms(); + let configurationRegistry = Registry.as(Extensions.Configuration); + configurationRegistry.registerConfiguration({ + id: 'externalTerminal', + order: 100, + title: nls.localize('terminalConfigurationTitle', "External Terminal"), + type: 'object', + properties: { + 'terminal.explorerKind': { + type: 'string', + enum: [ + 'integrated', + 'external' + ], + enumDescriptions: [ + nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), + nls.localize('terminal.explorerKind.external', "Use the configured external terminal.") + ], + description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), + default: 'integrated' + }, + 'terminal.external.windowsExec': { + type: 'string', + description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), + default: terminals.windows, + scope: ConfigurationScope.APPLICATION + }, + 'terminal.external.osxExec': { + type: 'string', + description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."), + default: DEFAULT_TERMINAL_OSX, + scope: ConfigurationScope.APPLICATION + }, + 'terminal.external.linuxExec': { + type: 'string', + description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), + default: terminals.linux, + scope: ConfigurationScope.APPLICATION + } + } + }); + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts b/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts deleted file mode 100644 index 84e406323bec..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/externalTerminal/node/externalTerminal.contribution.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import * as paths from 'vs/base/common/path'; -import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Schemas } from 'vs/base/common/network'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; -import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; -import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node/externalTerminal'; - -const OPEN_NATIVE_CONSOLE_COMMAND_ID = 'workbench.action.terminal.openNativeConsole'; -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: OPEN_NATIVE_CONSOLE_COMMAND_ID, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C, - when: KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED, - weight: KeybindingWeight.WorkbenchContrib, - handler: async (accessor) => { - const historyService = accessor.get(IHistoryService); - // Open external terminal in local workspaces - const terminalService = accessor.get(IExternalTerminalService); - const root = historyService.getLastActiveWorkspaceRoot(Schemas.file); - if (root) { - terminalService.openTerminal(root.fsPath); - } else { - // Opens current file's folder, if no folder is open in editor - const activeFile = historyService.getLastActiveFile(Schemas.file); - if (activeFile) { - terminalService.openTerminal(paths.dirname(activeFile.fsPath)); - } else { - const pathService = accessor.get(IPathService); - const userHome = await pathService.userHome(); - terminalService.openTerminal(userHome.fsPath); - } - } - } -}); - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: OPEN_NATIVE_CONSOLE_COMMAND_ID, - title: { value: nls.localize('globalConsoleAction', "Open New External Terminal"), original: 'Open New External Terminal' } - } -}); - -if (isWindows) { - registerSingleton(IExternalTerminalService, WindowsExternalTerminalService, true); -} else if (isMacintosh) { - registerSingleton(IExternalTerminalService, MacExternalTerminalService, true); -} else if (isLinux) { - registerSingleton(IExternalTerminalService, LinuxExternalTerminalService, true); -} - -LinuxExternalTerminalService.getDefaultTerminalLinuxReady().then(defaultTerminalLinux => { - let configurationRegistry = Registry.as(Extensions.Configuration); - configurationRegistry.registerConfiguration({ - id: 'externalTerminal', - order: 100, - title: nls.localize('terminalConfigurationTitle', "External Terminal"), - type: 'object', - properties: { - 'terminal.explorerKind': { - type: 'string', - enum: [ - 'integrated', - 'external' - ], - enumDescriptions: [ - nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), - nls.localize('terminal.explorerKind.external', "Use the configured external terminal.") - ], - description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), - default: 'integrated' - }, - 'terminal.external.windowsExec': { - type: 'string', - description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), - default: WindowsExternalTerminalService.getDefaultTerminalWindows(), - scope: ConfigurationScope.APPLICATION - }, - 'terminal.external.osxExec': { - type: 'string', - description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."), - default: DEFAULT_TERMINAL_OSX, - scope: ConfigurationScope.APPLICATION - }, - 'terminal.external.linuxExec': { - type: 'string', - description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), - default: defaultTerminalLinux, - scope: ConfigurationScope.APPLICATION - } - } - }); -}); diff --git a/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedback.ts b/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedback.ts index 9bd1c3ba1081..08dc43a8a56e 100644 --- a/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -13,7 +13,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { editorWidgetBackground, editorWidgetForeground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, textLinkForeground, contrastBorder, darken } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, editorWidgetForeground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, textLinkForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { Button } from 'vs/base/browser/ui/button/button'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -142,7 +142,7 @@ export class FeedbackWidget extends Dropdown { if (darkenFactor) { const backgroundBaseColor = theme.getColor(editorWidgetBackground); if (backgroundBaseColor) { - const backgroundColor = darken(backgroundBaseColor, darkenFactor)(theme); + const backgroundColor = backgroundBaseColor.darken(darkenFactor); if (backgroundColor) { closeBtn.style.backgroundColor = backgroundColor.toString(); } diff --git a/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index a53db17373f8..7708f4a8f65b 100644 --- a/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/lib/vscode/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -19,6 +19,7 @@ import { CATEGORIES } from 'vs/workbench/common/actions'; import { assertIsDefined } from 'vs/base/common/types'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { HIDE_NOTIFICATIONS_CENTER, HIDE_NOTIFICATION_TOAST } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { isIOS } from 'vs/base/common/platform'; class TwitterFeedbackService implements IFeedbackDelegate { @@ -70,7 +71,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben ) { super(); - if (productService.sendASmile) { + if (productService.sendASmile && !isIOS) { this.createFeedbackStatusEntry(); this.registerListeners(); } @@ -79,7 +80,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private createFeedbackStatusEntry(): void { // Status entry - this.entry = this._register(this.statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', localize('status.feedback', "Tweet Feedback"), StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); + this.entry = this._register(this.statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); // Command to toggle CommandsRegistry.registerCommand(FeedbackStatusbarConribution.TOGGLE_FEEDBACK_COMMAND, () => this.toggleFeedback()); @@ -135,6 +136,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private getStatusEntry(showBeak?: boolean): IStatusbarEntry { return { + name: localize('status.feedback.name', "Feedback"), text: '$(feedback)', ariaLabel: localize('status.feedback', "Tweet Feedback"), tooltip: localize('status.feedback', "Tweet Feedback"), diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 7eed77c03abc..15845c6758dc 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -7,12 +7,12 @@ import { localize } from 'vs/nls'; import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { EditorOverride, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; /** @@ -40,7 +40,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { ); } - private async openInternal(input: EditorInput, options: EditorOptions | undefined): Promise { + private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { if (input instanceof FileEditorInput && this.group) { // Enforce to open the input as text to enable our text based viewer @@ -49,13 +49,14 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { // Try to let the user pick an override if there is one availabe const overridenInput = await this.editorOverrideService.resolveEditorOverride(input, { ...options, override: EditorOverride.PICK, }, this.group); - let newOptions = overridenInput?.options ?? options; - newOptions = { ...newOptions, override: EditorOverride.DISABLED }; // Replace the overrriden input, with the text based input await this.editorService.replaceEditors([{ editor: input, replacement: overridenInput?.editor ?? input, - options: newOptions, + options: { + ...overridenInput?.options ?? options, + override: EditorOverride.DISABLED + } }], overridenInput?.group ?? this.group); } } diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts new file mode 100644 index 000000000000..7d1270d53b90 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isEqual } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; +import { IFileService } from 'vs/platform/files/common/files'; + +interface ISerializedFileEditorInput { + resourceJSON: UriComponents; + preferredResourceJSON?: UriComponents; + name?: string; + description?: string; + encoding?: string; + modeId?: string; +} + +export class FileEditorInputSerializer implements IEditorInputSerializer { + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string { + const fileEditorInput = editorInput as FileEditorInput; + const resource = fileEditorInput.resource; + const preferredResource = fileEditorInput.preferredResource; + const serializedFileEditorInput: ISerializedFileEditorInput = { + resourceJSON: resource.toJSON(), + preferredResourceJSON: isEqual(resource, preferredResource) ? undefined : preferredResource, // only storing preferredResource if it differs from the resource + name: fileEditorInput.getPreferredName(), + description: fileEditorInput.getPreferredDescription(), + encoding: fileEditorInput.getEncoding(), + modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data + }; + + return JSON.stringify(serializedFileEditorInput); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput { + return instantiationService.invokeFunction(accessor => { + const serializedFileEditorInput: ISerializedFileEditorInput = JSON.parse(serializedEditorInput); + const resource = URI.revive(serializedFileEditorInput.resourceJSON); + const preferredResource = URI.revive(serializedFileEditorInput.preferredResourceJSON); + const name = serializedFileEditorInput.name; + const description = serializedFileEditorInput.description; + const encoding = serializedFileEditorInput.encoding; + const mode = serializedFileEditorInput.modeId; + + const fileEditorInput = accessor.get(IEditorService).createEditorInput({ resource, label: name, description, encoding, mode, forceFile: true }) as FileEditorInput; + if (preferredResource) { + fileEditorInput.setPreferredResource(preferredResource); + } + + return fileEditorInput; + }); + } +} + +export class FileEditorWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService private readonly editorService: IEditorService, + @IFileService private readonly fileService: IFileService + ) { + super(); + + this.installHandler(); + } + + private installHandler(): void { + this._register(this.workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.typeId === NO_TYPE_ID && this.fileService.canHandleResource(workingCopy.resource), + // Naturally it would make sense here to check for `instanceof FileEditorInput` + // but because some custom editors also leverage text file based working copies + // we need to do a weaker check by only comparing for the resource + isOpen: (workingCopy, editor) => isEqual(workingCopy.resource, editor.resource), + createEditor: workingCopy => this.editorService.createEditorInput({ resource: workingCopy.resource, forceFile: true }) + })); + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts similarity index 75% rename from lib/vscode/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts rename to lib/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 342eaecd6055..488441ba5e39 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -3,25 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, isTextEditorPane } from 'vs/workbench/common/editor'; +import { IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { EditorOverride, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { ITextFileService, TextFileEditorModelState, TextFileResolveReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel, EncodingMode } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isEqual } from 'vs/base/common/resources'; import { Event } from 'vs/base/common/event'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { Schemas } from 'vs/base/common/network'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; const enum ForceOpenAs { None, @@ -38,10 +37,31 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return FILE_EDITOR_INPUT_ID; } + override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; + + if (this.model) { + if (this.model.isReadonly()) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + if (this.fileService.canHandleResource(this.resource)) { + if (this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + capabilities |= EditorInputCapabilities.Untitled; + } + } + + return capabilities; + } + private preferredName: string | undefined; private preferredDescription: string | undefined; private preferredEncoding: string | undefined; private preferredMode: string | undefined; + private preferredContents: string | undefined; private forceOpenAs: ForceOpenAs = ForceOpenAs.None; @@ -57,16 +77,16 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredMode: string | undefined, + preferredContents: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextFileService textFileService: ITextFileService, @ITextModelService private readonly textModelResolverService: ITextModelService, @ILabelService labelService: ILabelService, @IFileService fileService: IFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, - @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IEditorService editorService: IEditorService ) { - super(resource, preferredResource, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + super(resource, preferredResource, editorService, textFileService, labelService, fileService); this.model = this.textFileService.files.get(resource); @@ -86,17 +106,17 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.setPreferredMode(preferredMode); } - // If a file model already exists, make sure to wire it in - if (this.model) { - this.registerModelListeners(this.model); + if (typeof preferredContents === 'string') { + this.setPreferredContents(preferredContents); } - } - - protected override registerListeners(): void { - super.registerListeners(); // Attach to model that matches our resource once created this._register(this.textFileService.files.onDidCreate(model => this.onDidCreateTextFileModel(model))); + + // If a file model already exists, make sure to wire it in + if (this.model) { + this.registerModelListeners(this.model); + } } private onDidCreateTextFileModel(model: ITextFileEditorModel): void { @@ -118,6 +138,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements // re-emit some events from the model this.modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this.modelListeners.add(model.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); + this.modelListeners.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); // important: treat save errors as potential dirty change because // a file that is in save conflict or error will report dirty even @@ -132,7 +153,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements } override getName(): string { - return this.preferredName || this.decorateLabel(super.getName()); + return this.preferredName || super.getName(); } setPreferredName(name: string): void { @@ -175,22 +196,6 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return this.preferredDescription; } - override getTitle(verbosity: Verbosity): string { - switch (verbosity) { - case Verbosity.SHORT: - return this.decorateLabel(super.getName()); - case Verbosity.MEDIUM: - case Verbosity.LONG: - return this.decorateLabel(super.getTitle(verbosity)); - } - } - - private decorateLabel(label: string): string { - const orphaned = this.model?.hasState(TextFileEditorModelState.ORPHAN); - const readonly = this.isReadonly(); - return decorateFileEditorLabel(label, { orphaned: !!orphaned, readonly }); - } - getEncoding(): string | undefined { if (this.model) { return this.model.getEncoding(); @@ -216,6 +221,14 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.setForceOpenAsText(); } + getMode(): string | undefined { + if (this.model) { + return this.model.getMode(); + } + + return this.preferredMode; + } + getPreferredMode(): string | undefined { return this.preferredMode; } @@ -233,6 +246,13 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.setForceOpenAsText(); } + setPreferredContents(contents: string): void { + this.preferredContents = contents; + + // contents is a good hint to open the file as text + this.setForceOpenAsText(); + } + setForceOpenAsText(): void { this.forceOpenAs = ForceOpenAs.Text; } @@ -245,12 +265,12 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return !!(this.model?.isDirty()); } - override isReadonly(): boolean { + override isOrphaned(): boolean { if (this.model) { - return this.model.isReadonly(); + return this.model.hasState(TextFileEditorModelState.ORPHAN); } - return super.isReadonly(); + return super.isOrphaned(); } override isSaving(): boolean { @@ -263,11 +283,19 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements // and it could result in bad UX where an editor can be closed even though // it shows up as dirty and has not finished saving yet. + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return true; // a short auto save is configured, treat this as being saved + } + return super.isSaving(); } - override getPreferredEditorId(candidates: string[]): string { - return this.forceOpenAs === ForceOpenAs.Binary ? BINARY_FILE_EDITOR_ID : TEXT_FILE_EDITOR_ID; + override prefersEditor>(editors: T[]): T | undefined { + if (this.forceOpenAs === ForceOpenAs.Binary) { + return editors.find(editor => editor.typeId === BINARY_FILE_EDITOR_ID); + } + + return editors.find(editor => editor.typeId === TEXT_FILE_EDITOR_ID); } override resolve(): Promise { @@ -284,11 +312,18 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements private async doResolveAsText(): Promise { try { + // Unset preferred contents after having applied it once + // to prevent this property to stick. We still want future + // `resolve` calls to fetch the contents from disk. + const preferredContents = this.preferredContents; + this.preferredContents = undefined; + // Resolve resource via text file service and only allow // to open binary files if we are instructed so await this.textFileService.files.resolve(this.resource, { mode: this.preferredMode, encoding: this.preferredEncoding, + contents: typeof preferredContents === 'string' ? createTextBufferFactory(preferredContents) : undefined, reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model allowBinary: this.forceOpenAs === ForceOpenAs.Text, reason: TextFileResolveReason.EDITOR @@ -350,20 +385,29 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements }; } - private getViewStateFor(group: GroupIdentifier): IEditorViewState | undefined { - for (const editorPane of this.editorService.visibleEditorPanes) { - if (editorPane.group.id === group && this.matches(editorPane.input)) { - if (isTextEditorPane(editorPane)) { - return editorPane.getViewState(); + override asResourceEditorInput(group: GroupIdentifier): ITextResourceEditorInput { + return { + resource: this.preferredResource, + forceFile: true, + encoding: this.getEncoding(), + mode: this.getMode(), + contents: (() => { + const model = this.textFileService.files.get(this.resource); + if (model && model.isDirty()) { + return model.textEditorModel.getValue(); // only if dirty } - } - } - return undefined; + return undefined; + })(), + options: { + viewState: this.getViewStateFor(group), + override: EditorOverride.DISABLED + } + }; } override matches(otherInput: unknown): boolean { - if (otherInput === this) { + if (super.matches(otherInput)) { return true; } @@ -390,19 +434,3 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements this.cachedTextFileModelReference = undefined; } } - -export function decorateFileEditorLabel(label: string, state: { orphaned: boolean, readonly: boolean }): string { - if (state.orphaned && state.readonly) { - return localize('orphanedReadonlyFile', "{0} (deleted, read-only)", label); - } - - if (state.orphaned) { - return localize('orphanedFile', "{0} (deleted)", label); - } - - if (state.readonly) { - return localize('readonlyFile', "{0} (read-only)", label); - } - - return label; -} diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 0d0bac8499d1..6e6705404424 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -5,16 +5,17 @@ import { localize } from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { isFunction, assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined } from 'vs/base/common/types'; import { isValidBasename } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/resources'; import { toAction } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions, TextEditorOptions, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorOpenContext, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { FileOperationError, FileOperationResult, FileChangesEvent, IFileService, FileOperationEvent, FileOperation } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,9 +29,10 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { createErrorWithActions } from 'vs/base/common/errors'; -import { EditorActivation, EditorOverride, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOverride, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; /** * An implementation of editor for file system resources. @@ -39,6 +41,8 @@ export class TextFileEditor extends BaseTextEditor { static readonly ID = TEXT_FILE_EDITOR_ID; + private readonly inputListener = this._register(new MutableDisposable()); + constructor( @ITelemetryService telemetryService: ITelemetryService, @IFileService private readonly fileService: IFileService, @@ -63,8 +67,8 @@ export class TextFileEditor extends BaseTextEditor { this._register(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } private onDidFilesChange(e: FileChangesEvent): void { @@ -80,11 +84,22 @@ export class TextFileEditor extends BaseTextEditor { } } - private onDidFileSystemProviderChange(scheme: string): void { + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input?.resource.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: FileEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: FileEditorInput): void { const control = this.getControl(); - const input = this.input; - if (control && input?.resource.scheme === scheme) { - control.updateOptions({ readOnly: input.isReadonly() }); + if (control) { + control.updateOptions({ readOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } } @@ -104,7 +119,10 @@ export class TextFileEditor extends BaseTextEditor { return this._input as FileEditorInput; } - override async setInput(input: FileEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: FileEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); @@ -140,9 +158,9 @@ export class TextFileEditor extends BaseTextEditor { } } - // TextOptions (avoiding instanceof here for a reason, do not change!) - if (options && isFunction((options).apply)) { - (options).apply(textEditor, ScrollType.Immediate); + // Apply options to editor if any + if (options) { + applyTextEditorOptions(options, textEditor, ScrollType.Immediate); } // Since the resolved model provides information about being readonly @@ -156,7 +174,7 @@ export class TextFileEditor extends BaseTextEditor { } } - protected handleSetInputError(error: Error, input: FileEditorInput, options: EditorOptions | undefined): void { + protected handleSetInputError(error: Error, input: FileEditorInput, options: ITextEditorOptions | undefined): void { // In case we tried to open a file inside the text editor and the response // indicates that this is not a text file, reopen the file through the binary @@ -196,21 +214,18 @@ export class TextFileEditor extends BaseTextEditor { throw error; } - private openAsBinary(input: FileEditorInput, options: EditorOptions | undefined): void { + private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { input.setForceOpenAsBinary(); - // Make sure to not steal away the currently active group - // because we are triggering another openEditor() call - // and do not control the initial intent that resulted - // in us now opening as binary. - const preservingOptions: IEditorOptions = { activation: EditorActivation.PRESERVE, override: EditorOverride.DISABLED }; - if (options) { - options.overwrite(preservingOptions); - } else { - options = EditorOptions.create(preservingOptions); - } - - this.editorService.openEditor(input, options, this.group); + this.editorService.openEditor(input, { + ...options, + // Make sure to not steal away the currently active group + // because we are triggering another openEditor() call + // and do not control the initial intent that resulted + // in us now opening as binary. + activation: EditorActivation.PRESERVE, + override: EditorOverride.DISABLED + }, this.group); } private async openAsFolder(input: FileEditorInput): Promise { @@ -231,6 +246,9 @@ export class TextFileEditor extends BaseTextEditor { override clearInput(): void { + // Clear input listener + this.inputListener.clear(); + // Update/clear editor view state in settings this.doSaveOrClearTextEditorViewState(this.input); diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts index 20ed5ecfea8b..51bc0067083e 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts @@ -17,6 +17,7 @@ import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/ import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { Schemas } from 'vs/base/common/network'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; export class TextFileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -26,7 +27,8 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr @ILifecycleService private readonly lifecycleService: ILifecycleService, @IHostService private readonly hostService: IHostService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService ) { super(); @@ -57,19 +59,24 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr return false; // resource must be dirty } - const model = this.textFileService.files.get(resource); - if (model?.hasState(TextFileEditorModelState.PENDING_SAVE)) { + const fileModel = this.textFileService.files.get(resource); + if (fileModel?.hasState(TextFileEditorModelState.PENDING_SAVE)) { return false; // resource must not be pending to save } - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY && !model?.hasState(TextFileEditorModelState.ERROR)) { + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY && !fileModel?.hasState(TextFileEditorModelState.ERROR)) { // leave models auto saved after short delay unless // the save resulted in an error return false; } if (this.editorService.isOpened({ resource, typeId: resource.scheme === Schemas.untitled ? UntitledTextEditorInput.ID : FILE_EDITOR_INPUT_ID })) { - return false; // model must not be opened already as file + return false; // model must not be opened already as file (fast check via editor type) + } + + const model = fileModel ?? this.textFileService.untitled.get(resource); + if (model && this.workingCopyEditorService.findEditor(model)) { + return false; // model must not be opened already as file (slower check via working copy) } return true; diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index c9c2651dc2e6..24192e1198a5 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -18,7 +18,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; import { INotificationService, INotificationHandle, INotificationActions, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/explorerService.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/explorerService.ts index 9a5d39685f9a..55c934a4e36b 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IFilesConfiguration, SortOrder } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, ISortOrderConfiguration, SortOrder, LexicographicOptions } from 'vs/workbench/contrib/files/common/files'; import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileService, FileChangesEvent, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; @@ -33,6 +33,7 @@ export class ExplorerService implements IExplorerService { private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem, data: IEditableData } | undefined; private _sortOrder: SortOrder; + private _lexicographicOptions: LexicographicOptions; private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; @@ -50,6 +51,7 @@ export class ExplorerService implements IExplorerService { @IProgressService private readonly progressService: IProgressService ) { this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); + this._lexicographicOptions = this.configurationService.getValue('explorer.sortOrderLexicographicOptions'); this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService); this.disposables.add(this.model); @@ -125,8 +127,11 @@ export class ExplorerService implements IExplorerService { return this.model.roots; } - get sortOrder(): SortOrder { - return this._sortOrder; + get sortOrderConfiguration(): ISortOrderConfiguration { + return { + sortOrder: this._sortOrder, + lexicographicOptions: this._lexicographicOptions, + }; } registerView(contextProvider: IExplorerView): void { @@ -226,7 +231,7 @@ export class ExplorerService implements IExplorerService { } // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === SortOrder.Modified }; + const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this._sortOrder === SortOrder.Modified }; const root = this.findClosestRoot(resource); if (!root) { return undefined; @@ -278,7 +283,7 @@ export class ExplorerService implements IExplorerService { // Add the new file to its parent (Model) await Promise.all(parents.map(async p => { // We have to check if the parent is resolved #29177 - const resolveMetadata = this.sortOrder === `modified`; + const resolveMetadata = this._sortOrder === `modified`; if (!p.isDirectoryResolved) { const stat = await this.fileService.resolve(p.resource, { resolveMetadata }); if (stat) { @@ -348,13 +353,22 @@ export class ExplorerService implements IExplorerService { } private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { - const configSortOrder = configuration?.explorer?.sortOrder || 'default'; + let shouldRefresh = false; + + const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default; if (this._sortOrder !== configSortOrder) { - const shouldRefresh = this._sortOrder !== undefined; + shouldRefresh = this._sortOrder !== undefined; this._sortOrder = configSortOrder; - if (shouldRefresh) { - await this.refresh(); - } + } + + const configLexicographicOptions = configuration?.explorer?.sortOrderLexicographicOptions || LexicographicOptions.Default; + if (this._lexicographicOptions !== configLexicographicOptions) { + shouldRefresh = shouldRefresh || this._lexicographicOptions !== undefined; + this._lexicographicOptions = configLexicographicOptions; + } + + if (shouldRefresh) { + await this.refresh(); } } diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 7081f40a535d..3e91e275941c 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -37,7 +37,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { WorkbenchStateContext, RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { AddRootFolderAction, OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -294,7 +294,7 @@ viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { order: 1 }); -const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; +const commandId = (isMacintosh && !isWeb) ? OpenFileFolderAction.ID : OpenFolderAction.ID; viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize({ key: 'remoteNoFolderHelp', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "Connected to remote.\n[Open Folder](command:{0})", commandId), diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index ab99f725f5a2..17fa76012f26 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, ShowActiveFileInExplorer, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; +import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, ShowActiveFileInExplorer, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow, UPLOAD_COMMAND_ID, UPLOAD_LABEL } from 'vs/workbench/contrib/files/browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; @@ -112,7 +112,7 @@ const CUT_FILE_ID = 'filesExplorer.cut'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CUT_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), primary: KeyMod.CtrlCmd | KeyCode.KEY_X, handler: cutFileHandler, }); @@ -460,7 +460,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { id: CUT_FILE_ID, title: nls.localize('cut', "Cut") }, - when: ExplorerRootContext.toNegated() + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -485,8 +485,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ - group: '5_cutcopypaste', - order: 30, + group: '5b_importexport', + order: 10, command: { id: DOWNLOAD_COMMAND_ID, title: DOWNLOAD_LABEL, @@ -501,16 +501,33 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ ) })); +MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ + group: '5b_importexport', + order: 20, + command: { + id: UPLOAD_COMMAND_ID, + title: UPLOAD_LABEL, + }, + when: ContextKeyExpr.and( + // only in web + IsWebContext, + // only on folders + ExplorerFolderContext, + // only on editable folders + ExplorerResourceNotReadonlyContext + ) +})); + MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '6_copypath', - order: 30, + order: 10, command: copyPathCommand, when: ResourceContextKey.IsFileSystemResource }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '6_copypath', - order: 30, + order: 20, command: copyRelativePathCommand, when: ResourceContextKey.IsFileSystemResource }); diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.ts index 1a9407c20ab8..258000894b2f 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { isWindows, isWeb } from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; import { extname, basename } from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Action } from 'vs/base/common/actions'; -import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; -import { ByteSize, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -28,8 +28,8 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { FileAccess, Schemas } from 'vs/base/common/network'; -import { IDialogService, IConfirmationResult, getFileNamesMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { Schemas } from 'vs/base/common/network'; +import { IDialogService, IConfirmationResult, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Constants } from 'vs/base/common/uint'; @@ -37,26 +37,19 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { coalesce } from 'vs/base/common/arrays'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { getErrorMessage } from 'vs/base/common/errors'; -import { WebFileSystemAccess, triggerDownload } from 'vs/base/browser/dom'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { triggerUpload } from 'vs/base/browser/dom'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { RunOnceWorker, sequence, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { once } from 'vs/base/common/functional'; import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; import { trim, rtrim } from 'vs/base/common/strings'; -import { IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { listenStream } from 'vs/base/common/stream'; -import { EditorOverride } from 'vs/platform/editor/common/editor'; -import { ContributedEditorPriority, IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { BrowserFileUpload, FileDownload } from 'vs/workbench/contrib/files/browser/fileImportExport'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -67,7 +60,10 @@ export const MOVE_FILE_TO_TRASH_LABEL = nls.localize('delete', "Delete"); export const COPY_FILE_LABEL = nls.localize('copyFile', "Copy"); export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste"); export const FileCopiedContext = new RawContextKey('fileCopied', false); +export const DOWNLOAD_COMMAND_ID = 'explorer.download'; export const DOWNLOAD_LABEL = nls.localize('download', "Download..."); +export const UPLOAD_COMMAND_ID = 'explorer.upload'; +export const UPLOAD_LABEL = nls.localize('upload', "Upload..."); const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; const MAX_UNDO_FILE_SIZE = 5000000; // 5mb @@ -444,8 +440,7 @@ export class GlobalCompareResourcesAction extends Action { label: string, @IQuickInputService private readonly quickInputService: IQuickInputService, @IEditorService private readonly editorService: IEditorService, - @ITextModelService private readonly textModelService: ITextModelService, - @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService + @ITextModelService private readonly textModelService: ITextModelService ) { super(id, label); } @@ -454,49 +449,17 @@ export class GlobalCompareResourcesAction extends Action { const activeInput = this.editorService.activeEditor; const activeResource = EditorResourceAccessor.getOriginalUri(activeInput); if (activeResource && this.textModelService.canHandleResource(activeResource)) { - - // Define a one-time override that has highest priority - // and matches every resource to be able to create a - // diff editor to show the comparison. - const editorOverrideDisposable = this.editorOverrideService.registerContributionPoint('*', { - id: GlobalCompareResourcesAction.ID, - label: GlobalCompareResourcesAction.LABEL, - priority: ContributedEditorPriority.exclusive, - detail: '', - describes: () => false - }, {}, resource => { - - // Only once! - editorOverrideDisposable.dispose(); - - // Open editor as diff if the selected editor resource - // can be handled by the text model service - if (this.textModelService.canHandleResource(resource)) { - return { - editor: this.editorService.createEditorInput({ - leftResource: activeResource, - rightResource: resource, - options: { override: EditorOverride.DISABLED, pinned: true } - }) - }; + const picks = await this.quickInputService.quickAccess.pick('', { itemActivation: ItemActivation.SECOND }); + if (picks?.length === 1) { + const resource = (picks[0] as unknown as { resource: unknown }).resource; + if (URI.isUri(resource) && this.textModelService.canHandleResource(resource)) { + this.editorService.openEditor({ + originalInput: { resource: activeResource }, + modifiedInput: { resource: resource }, + options: { pinned: true } + }); } - - // Otherwise stay on current resource - return { - editor: this.editorService.createEditorInput({ - resource: activeResource, - options: { override: EditorOverride.DISABLED, pinned: true } - }) - }; - }); - - once(this.quickInputService.onHide)((async () => { - await timeout(0); // prevent race condition with editor - editorOverrideDisposable.dispose(); - })); - - // Bring up quick access - this.quickInputService.quickAccess.show('', { itemActivation: ItemActivation.SECOND }); + } } } } @@ -639,7 +602,7 @@ export class ShowOpenedFileInNewWindow extends Action { label: string, @IEditorService private readonly editorService: IEditorService, @IHostService private readonly hostService: IHostService, - @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, @IFileService private readonly fileService: IFileService ) { super(id, label); @@ -651,7 +614,7 @@ export class ShowOpenedFileInNewWindow extends Action { if (this.fileService.canHandleResource(fileResource)) { this.hostService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true }); } else { - this.notificationService.info(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource.")); + this.dialogService.show(Severity.Error, nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource."), [nls.localize('ok', 'OK')]); } } } @@ -718,7 +681,7 @@ function trimLongName(name: string): string { return name; } -export function getWellFormedFileName(filename: string): string { +function getWellFormedFileName(filename: string): string { if (!filename) { return filename; } @@ -726,8 +689,7 @@ export function getWellFormedFileName(filename: string): string { // Trim tabs filename = trim(filename, '\t'); - // Remove trailing dots and slashes - filename = rtrim(filename, '.'); + // Remove trailing slashes filename = rtrim(filename, '/'); filename = rtrim(filename, '\\'); @@ -768,8 +730,9 @@ export class CompareWithClipboardAction extends Action { const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard โ†” {0}", name); await this.editorService.openEditor({ - leftResource: resource.with({ scheme }), - rightResource: resource, label: editorLabel, + originalInput: { resource: resource.with({ scheme }) }, + modifiedInput: { resource: resource }, + label: editorLabel, options: { pinned: true } }).finally(() => { dispose(this.registrationDisposal); @@ -965,245 +928,39 @@ export const cutFileHandler = async (accessor: ServicesAccessor) => { } }; -export const DOWNLOAD_COMMAND_ID = 'explorer.download'; const downloadFileHandler = async (accessor: ServicesAccessor) => { - const logService = accessor.get(ILogService); - const fileService = accessor.get(IFileService); - const fileDialogService = accessor.get(IFileDialogService); const explorerService = accessor.get(IExplorerService); - const progressService = accessor.get(IProgressService); + const instantiationService = accessor.get(IInstantiationService); const context = explorerService.getContext(true); const explorerItems = context.length ? context : explorerService.roots; - const cts = new CancellationTokenSource(); - - await progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: isWeb, - title: nls.localize('downloadingFiles', "Downloading") - }, async progress => { - return sequence(explorerItems.map(explorerItem => async () => { - if (cts.token.isCancellationRequested) { - return; - } - - // Web: use DOM APIs to download files with optional support - // for folders and large files - if (isWeb) { - const stat = await fileService.resolve(explorerItem.resource, { resolveMetadata: true }); - - if (cts.token.isCancellationRequested) { - return; - } - - const maxBlobDownloadSize = 32 * ByteSize.MB; // avoid to download via blob-trick >32MB to avoid memory pressure - const preferFileSystemAccessWebApis = stat.isDirectory || stat.size > maxBlobDownloadSize; - - // Folder: use FS APIs to download files and folders if available and preferred - if (preferFileSystemAccessWebApis && WebFileSystemAccess.supported(window)) { - - interface IDownloadOperation { - startTime: number; - progressScheduler: RunOnceWorker; - - filesTotal: number; - filesDownloaded: number; - - totalBytesDownloaded: number; - fileBytesDownloaded: number; - } - - async function downloadFileBuffered(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation): Promise { - const contents = await fileService.readFileStream(resource); - if (cts.token.isCancellationRequested) { - target.close(); - return; - } - - return new Promise((resolve, reject) => { - const sourceStream = contents.value; - - const disposables = new DisposableStore(); - disposables.add(toDisposable(() => target.close())); - - let disposed = false; - disposables.add(toDisposable(() => disposed = true)); - - disposables.add(once(cts.token.onCancellationRequested)(() => { - disposables.dispose(); - reject(); - })); - - listenStream(sourceStream, { - onData: data => { - if (!disposed) { - target.write(data.buffer); - reportProgress(contents.name, contents.size, data.byteLength, operation); - } - }, - onError: error => { - disposables.dispose(); - reject(error); - }, - onEnd: () => { - disposables.dispose(); - resolve(); - } - }); - }); - } - - async function downloadFileUnbuffered(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation): Promise { - const contents = await fileService.readFile(resource); - if (!cts.token.isCancellationRequested) { - target.write(contents.value.buffer); - reportProgress(contents.name, contents.size, contents.value.byteLength, operation); - } - - target.close(); - } - - async function downloadFile(targetFolder: FileSystemDirectoryHandle, file: IFileStatWithMetadata, operation: IDownloadOperation): Promise { - - // Report progress - operation.filesDownloaded++; - operation.fileBytesDownloaded = 0; // reset for this file - reportProgress(file.name, 0, 0, operation); - - // Start to download - const targetFile = await targetFolder.getFileHandle(file.name, { create: true }); - const targetFileWriter = await targetFile.createWritable(); - - // For large files, write buffered using streams - if (file.size > ByteSize.MB) { - return downloadFileBuffered(file.resource, targetFileWriter, operation); - } - - // For small files prefer to write unbuffered to reduce overhead - return downloadFileUnbuffered(file.resource, targetFileWriter, operation); - } - - async function downloadFolder(folder: IFileStatWithMetadata, targetFolder: FileSystemDirectoryHandle, operation: IDownloadOperation): Promise { - if (folder.children) { - operation.filesTotal += (folder.children.map(child => child.isFile)).length; - - for (const child of folder.children) { - if (cts.token.isCancellationRequested) { - return; - } - - if (child.isFile) { - await downloadFile(targetFolder, child, operation); - } else { - const childFolder = await targetFolder.getDirectoryHandle(child.name, { create: true }); - const resolvedChildFolder = await fileService.resolve(child.resource, { resolveMetadata: true }); - - await downloadFolder(resolvedChildFolder, childFolder, operation); - } - } - } - } - - function reportProgress(name: string, fileSize: number, bytesDownloaded: number, operation: IDownloadOperation): void { - operation.fileBytesDownloaded += bytesDownloaded; - operation.totalBytesDownloaded += bytesDownloaded; - - const bytesDownloadedPerSecond = operation.totalBytesDownloaded / ((Date.now() - operation.startTime) / 1000); - - // Small file - let message: string; - if (fileSize < ByteSize.MB) { - if (operation.filesTotal === 1) { - message = name; - } else { - message = nls.localize('downloadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesDownloaded, operation.filesTotal, ByteSize.formatSize(bytesDownloadedPerSecond)); - } - } - - // Large file - else { - message = nls.localize('downloadProgressLarge', "{0} ({1} of {2}, {3}/s)", name, ByteSize.formatSize(operation.fileBytesDownloaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesDownloadedPerSecond)); - } - - // Report progress but limit to update only once per second - operation.progressScheduler.work({ message }); - } + const downloadHandler = instantiationService.createInstance(FileDownload); + return downloadHandler.download(explorerItems); +}; - try { - const parentFolder: FileSystemDirectoryHandle = await window.showDirectoryPicker(); - const operation: IDownloadOperation = { - startTime: Date.now(), - progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), - - filesTotal: stat.isDirectory ? 0 : 1, // folders increment filesTotal within downloadFolder method - filesDownloaded: 0, - - totalBytesDownloaded: 0, - fileBytesDownloaded: 0 - }; - - if (stat.isDirectory) { - const targetFolder = await parentFolder.getDirectoryHandle(stat.name, { create: true }); - await downloadFolder(stat, targetFolder, operation); - } else { - await downloadFile(parentFolder, stat, operation); - } - - operation.progressScheduler.dispose(); - } catch (error) { - logService.warn(error); - cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels - } - } +CommandsRegistry.registerCommand({ + id: DOWNLOAD_COMMAND_ID, + handler: downloadFileHandler +}); - // File: use traditional download to circumvent browser limitations - else if (stat.isFile) { - let bufferOrUri: Uint8Array | URI; - try { - bufferOrUri = (await fileService.readFile(stat.resource, { limits: { size: maxBlobDownloadSize } })).value.buffer; - } catch (error) { - bufferOrUri = FileAccess.asBrowserUri(stat.resource); - } +const uploadFileHandler = async (accessor: ServicesAccessor) => { + const explorerService = accessor.get(IExplorerService); + const instantiationService = accessor.get(IInstantiationService); - if (!cts.token.isCancellationRequested) { - triggerDownload(bufferOrUri, stat.name); - } - } - } + const context = explorerService.getContext(true); + const element = context.length ? context[0] : explorerService.roots[0]; - // Native: use working copy file service to get at the contents - else { - progress.report({ message: explorerItem.name }); - - let defaultUri = explorerItem.isDirectory ? await fileDialogService.defaultFolderPath(Schemas.file) : await fileDialogService.defaultFilePath(Schemas.file); - defaultUri = resources.joinPath(defaultUri, explorerItem.name); - - const destination = await fileDialogService.showSaveDialog({ - availableFileSystems: [Schemas.file], - saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), - title: nls.localize('chooseWhereToDownload', "Choose Where to Download"), - defaultUri - }); - - if (destination) { - await explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { - undoLabel: nls.localize('downloadBulkEdit', "Download {0}", explorerItem.name), - progressLabel: nls.localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), - progressLocation: ProgressLocation.Explorer - }); - } else { - cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 - } - } - })); - }, () => cts.dispose(true)); + const files = await triggerUpload(); + if (files) { + const browserUpload = instantiationService.createInstance(BrowserFileUpload); + return browserUpload.upload(element, files); + } }; CommandsRegistry.registerCommand({ - id: DOWNLOAD_COMMAND_ID, - handler: downloadFileHandler + id: UPLOAD_COMMAND_ID, + handler: uploadFileHandler }); export const pasteFileHandler = async (accessor: ServicesAccessor) => { diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts index f2894496f223..f078cb142bb4 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,7 +5,8 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { EditorResourceAccessor, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason, SideBySideEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IWindowOpenable, IOpenWindowOptions, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -243,8 +244,8 @@ CommandsRegistry.registerCommand({ if (resources.length === 2) { return editorService.openEditor({ - leftResource: resources[0], - rightResource: resources[1], + originalInput: { resource: resources[0] }, + modifiedInput: { resource: resources[1] }, options: { pinned: true } }); } @@ -262,8 +263,8 @@ CommandsRegistry.registerCommand({ const rightResource = getResourceForCommand(resource, listService, editorService); if (globalResourceToCompare && rightResource) { editorService.openEditor({ - leftResource: globalResourceToCompare, - rightResource, + originalInput: { resource: globalResourceToCompare }, + modifiedInput: { resource: rightResource }, options: { pinned: true } }); } @@ -386,7 +387,7 @@ async function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEd // See also https://github.com/microsoft/vscode/issues/106330 if ( activeGroup.activeEditor instanceof SideBySideEditorInput && - !options?.saveAs && !(activeGroup.activeEditor.primary.isUntitled() || activeGroup.activeEditor.secondary.isUntitled()) + !options?.saveAs && !(activeGroup.activeEditor.primary.hasCapability(EditorInputCapabilities.Untitled) || activeGroup.activeEditor.secondary.hasCapability(EditorInputCapabilities.Untitled)) ) { editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.primary }); editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.secondary }); @@ -550,7 +551,7 @@ CommandsRegistry.registerCommand({ } try { - await editorService.revert(editors.filter(({ editor }) => !editor.isUntitled() /* all except untitled */), { force: true }); + await editorService.revert(editors.filter(({ editor }) => !editor.hasCapability(EditorInputCapabilities.Untitled) /* all except untitled */), { force: true }); } catch (error) { notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); } diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/fileImportExport.ts new file mode 100644 index 000000000000..bf558d47c9b4 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -0,0 +1,825 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { getFileNamesMessage, IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ByteSize, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { VIEW_ID } from 'vs/workbench/contrib/files/common/files'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Limiter, Promises, RunOnceWorker } from 'vs/base/common/async'; +import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer'; +import { basename, joinPath } from 'vs/base/common/resources'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; +import { URI } from 'vs/base/common/uri'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; +import { isWeb } from 'vs/base/common/platform'; +import { triggerDownload, WebFileSystemAccess } from 'vs/base/browser/dom'; +import { ILogService } from 'vs/platform/log/common/log'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { listenStream } from 'vs/base/common/stream'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { once } from 'vs/base/common/functional'; +import { coalesce } from 'vs/base/common/arrays'; + +//#region Browser File Upload (drag and drop, input element) + +interface IBrowserUploadOperation { + startTime: number; + progressScheduler: RunOnceWorker; + + filesTotal: number; + filesUploaded: number; + + totalBytesUploaded: number; +} + +interface IWebkitDataTransfer { + items: IWebkitDataTransferItem[]; +} + +interface IWebkitDataTransferItem { + webkitGetAsEntry(): IWebkitDataTransferItemEntry; +} + +interface IWebkitDataTransferItemEntry { + name: string | undefined; + isFile: boolean; + isDirectory: boolean; + + file(resolve: (file: File) => void, reject: () => void): void; + createReader(): IWebkitDataTransferItemEntryReader; +} + +interface IWebkitDataTransferItemEntryReader { + readEntries(resolve: (file: IWebkitDataTransferItemEntry[]) => void, reject: () => void): void +} + +export class BrowserFileUpload { + + private static readonly MAX_PARALLEL_UPLOADS = 20; + + constructor( + @IProgressService private readonly progressService: IProgressService, + @IDialogService private readonly dialogService: IDialogService, + @IExplorerService private readonly explorerService: IExplorerService, + @IEditorService private readonly editorService: IEditorService, + @IFileService private readonly fileService: IFileService + ) { + } + + upload(target: ExplorerItem, source: DragEvent | FileList): Promise { + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const uploadPromise = this.progressService.withProgress( + { + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('uploadingFiles', "Uploading") + }, + async progress => this.doUpload(target, this.toTransfer(source), progress, cts.token), + () => cts.dispose(true) + ); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => uploadPromise); + + return uploadPromise; + } + + private toTransfer(source: DragEvent | FileList): IWebkitDataTransfer { + if (source instanceof DragEvent) { + return source.dataTransfer as unknown as IWebkitDataTransfer; + } + + const transfer: IWebkitDataTransfer = { items: [] }; + + // We want to reuse the same code for uploading from + // Drag & Drop as well as input element based upload + // so we convert into webkit data transfer when the + // input element approach is used (simplified). + for (const file of source) { + transfer.items.push({ + webkitGetAsEntry: () => { + return { + name: file.name, + isDirectory: false, + isFile: true, + createReader: () => { throw new Error('Unsupported for files'); }, + file: resolve => resolve(file) + }; + } + }); + } + + return transfer; + } + + private async doUpload(target: ExplorerItem, source: IWebkitDataTransfer, progress: IProgress, token: CancellationToken): Promise { + const items = source.items; + + // Somehow the items thing is being modified at random, maybe as a security + // measure since this is a DND operation. As such, we copy the items into + // an array we own as early as possible before using it. + const entries: IWebkitDataTransferItemEntry[] = []; + for (const item of items) { + entries.push(item.webkitGetAsEntry()); + } + + const results: { isFile: boolean, resource: URI }[] = []; + const operation: IBrowserUploadOperation = { + startTime: Date.now(), + progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), + + filesTotal: entries.length, + filesUploaded: 0, + + totalBytesUploaded: 0 + }; + + // Upload all entries in parallel up to a + // certain maximum leveraging the `Limiter` + const uploadLimiter = new Limiter(BrowserFileUpload.MAX_PARALLEL_UPLOADS); + await Promises.settled(entries.map(entry => { + return uploadLimiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + // Confirm overwrite as needed + if (target && entry.name && target.getChild(entry.name)) { + const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); + if (!confirmed) { + return; + } + + await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true, folder: target.getChild(entry.name)?.isDirectory })], { + undoLabel: localize('overwrite', "Overwrite {0}", entry.name), + progressLabel: localize('overwriting', "Overwriting {0}", entry.name), + }); + + if (token.isCancellationRequested) { + return; + } + } + + // Upload entry + const result = await this.doUploadEntry(entry, target.resource, target, progress, operation, token); + if (result) { + results.push(result); + } + }); + })); + + operation.progressScheduler.dispose(); + + // Open uploaded file in editor only if we upload just one + const firstUploadedFile = results[0]; + if (!token.isCancellationRequested && firstUploadedFile?.isFile) { + await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } }); + } + } + + private async doUploadEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress, operation: IBrowserUploadOperation, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> { + if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) { + return undefined; + } + + // Report progress + let fileBytesUploaded = 0; + const reportProgress = (fileSize: number, bytesUploaded: number): void => { + fileBytesUploaded += bytesUploaded; + operation.totalBytesUploaded += bytesUploaded; + + const bytesUploadedPerSecond = operation.totalBytesUploaded / ((Date.now() - operation.startTime) / 1000); + + // Small file + let message: string; + if (fileSize < ByteSize.MB) { + if (operation.filesTotal === 1) { + message = `${entry.name}`; + } else { + message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, ByteSize.formatSize(bytesUploadedPerSecond)); + } + } + + // Large file + else { + message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, ByteSize.formatSize(fileBytesUploaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesUploadedPerSecond)); + } + + // Report progress but limit to update only once per second + operation.progressScheduler.work({ message }); + }; + operation.filesUploaded++; + reportProgress(0, 0); + + // Handle file upload + const resource = joinPath(parentResource, entry.name); + if (entry.isFile) { + const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); + + if (token.isCancellationRequested) { + return undefined; + } + + // Chrome/Edge/Firefox support stream method, but only use it for + // larger files to reduce the overhead of the streaming approach + if (typeof file.stream === 'function' && file.size > ByteSize.MB) { + await this.doUploadFileBuffered(resource, file, reportProgress, token); + } + + // Fallback to unbuffered upload for other browsers or small files + else { + await this.doUploadFileUnbuffered(resource, file, reportProgress); + } + + return { isFile: true, resource }; + } + + // Handle folder upload + else { + + // Create target folder + await this.fileService.createFolder(resource); + + if (token.isCancellationRequested) { + return undefined; + } + + // Recursive upload files in this directory + const dirReader = entry.createReader(); + const childEntries: IWebkitDataTransferItemEntry[] = []; + let done = false; + do { + const childEntriesChunk = await new Promise((resolve, reject) => dirReader.readEntries(resolve, reject)); + if (childEntriesChunk.length > 0) { + childEntries.push(...childEntriesChunk); + } else { + done = true; // an empty array is a signal that all entries have been read + } + } while (!done && !token.isCancellationRequested); + + // Update operation total based on new counts + operation.filesTotal += childEntries.length; + + // Split up files from folders to upload + const folderTarget = target && target.getChild(entry.name) || undefined; + const fileChildEntries: IWebkitDataTransferItemEntry[] = []; + const folderChildEntries: IWebkitDataTransferItemEntry[] = []; + for (const childEntry of childEntries) { + if (childEntry.isFile) { + fileChildEntries.push(childEntry); + } else if (childEntry.isDirectory) { + folderChildEntries.push(childEntry); + } + } + + // Upload files (up to `MAX_PARALLEL_UPLOADS` in parallel) + const fileUploadQueue = new Limiter(BrowserFileUpload.MAX_PARALLEL_UPLOADS); + await Promises.settled(fileChildEntries.map(fileChildEntry => { + return fileUploadQueue.queue(() => this.doUploadEntry(fileChildEntry, resource, folderTarget, progress, operation, token)); + })); + + // Upload folders (sequentially give we don't know their sizes) + for (const folderChildEntry of folderChildEntries) { + await this.doUploadEntry(folderChildEntry, resource, folderTarget, progress, operation, token); + } + + return { isFile: false, resource }; + } + } + + private async doUploadFileBuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void, token: CancellationToken): Promise { + const writeableStream = newWriteableBufferStream({ + // Set a highWaterMark to prevent the stream + // for file upload to produce large buffers + // in-memory + highWaterMark: 10 + }); + const writeFilePromise = this.fileService.writeFile(resource, writeableStream); + + // Read the file in chunks using File.stream() web APIs + try { + const reader: ReadableStreamDefaultReader = file.stream().getReader(); + + let res = await reader.read(); + while (!res.done) { + if (token.isCancellationRequested) { + return undefined; + } + + // Write buffer into stream but make sure to wait + // in case the highWaterMark is reached + const buffer = VSBuffer.wrap(res.value); + await writeableStream.write(buffer); + + if (token.isCancellationRequested) { + return undefined; + } + + // Report progress + progressReporter(file.size, buffer.byteLength); + + res = await reader.read(); + } + writeableStream.end(undefined); + } catch (error) { + writeableStream.error(error); + writeableStream.end(); + } + + if (token.isCancellationRequested) { + return undefined; + } + + // Wait for file being written to target + await writeFilePromise; + } + + private doUploadFileUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = async event => { + try { + if (event.target?.result instanceof ArrayBuffer) { + const buffer = VSBuffer.wrap(new Uint8Array(event.target.result)); + await this.fileService.writeFile(resource, buffer); + + // Report progress + progressReporter(file.size, buffer.byteLength); + } else { + throw new Error('Could not read from dropped file.'); + } + + resolve(); + } catch (error) { + reject(error); + } + }; + + // Start reading the file to trigger `onload` + reader.readAsArrayBuffer(file); + }); + } +} + +//#endregion + +//#region Native File Import (drag and drop) + +export class NativeFileImport { + + constructor( + @IFileService private readonly fileService: IFileService, + @IHostService private readonly hostService: IHostService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, + @IExplorerService private readonly explorerService: IExplorerService, + @IEditorService private readonly editorService: IEditorService, + @IProgressService private readonly progressService: IProgressService + ) { + } + + async import(target: ExplorerItem, source: DragEvent): Promise { + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const importPromise = this.progressService.withProgress( + { + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('copyingFiles', "Copying...") + }, + async () => await this.doImport(target, source, cts.token), + () => cts.dispose(true) + ); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => importPromise); + + return importPromise; + } + + private async doImport(target: ExplorerItem, source: DragEvent, token: CancellationToken): Promise { + + // Check for dropped external files to be folders + const files = coalesce(extractEditorsDropData(source, true).filter(editor => URI.isUri(editor.resource) && this.fileService.canHandleResource(editor.resource)).map(editor => editor.resource)); + const resolvedFiles = await this.fileService.resolveAll(files.map(file => ({ resource: file }))); + + if (token.isCancellationRequested) { + return; + } + + // Pass focus to window + this.hostService.focus(); + + // Handle folders by adding to workspace if we are in workspace context and if dropped on top + const folders = resolvedFiles.filter(resolvedFile => resolvedFile.success && resolvedFile.stat?.isDirectory).map(resolvedFile => ({ uri: resolvedFile.stat!.resource })); + if (folders.length > 0 && target.isRoot) { + const buttons = [ + folders.length > 1 ? + localize('copyFolders', "&&Copy Folders") : + localize('copyFolder', "&&Copy Folder"), + localize('cancel', "Cancel") + ]; + + let message: string; + + // We only allow to add a folder to the workspace if there is already a workspace folder with that scheme + const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(folder => folder.uri.scheme); + if (folders.some(folder => workspaceFolderSchemas.indexOf(folder.uri.scheme) >= 0)) { + buttons.unshift(folders.length > 1 ? localize('addFolders', "&&Add Folders to Workspace") : localize('addFolder', "&&Add Folder to Workspace")); + message = folders.length > 1 ? + localize('dropFolders', "Do you want to copy the folders or add the folders to the workspace?") : + localize('dropFolder', "Do you want to copy '{0}' or add '{0}' as a folder to the workspace?", basename(folders[0].uri)); + } else { + message = folders.length > 1 ? + localize('copyfolders', "Are you sure to want to copy folders?") : + localize('copyfolder', "Are you sure to want to copy '{0}'?", basename(folders[0].uri)); + } + + const { choice } = await this.dialogService.show(Severity.Info, message, buttons); + + // Add folders + if (choice === buttons.length - 3) { + return this.workspaceEditingService.addFolders(folders); + } + + // Copy resources + if (choice === buttons.length - 2) { + return this.importResources(target, files, token); + } + } + + // Handle dropped files (only support FileStat as target) + else if (target instanceof ExplorerItem) { + return this.importResources(target, files, token); + } + } + + private async importResources(target: ExplorerItem, resources: URI[], token: CancellationToken): Promise { + if (resources && resources.length > 0) { + + // Resolve target to check for name collisions and ask user + const targetStat = await this.fileService.resolve(target.resource); + + if (token.isCancellationRequested) { + return; + } + + // Check for name collisions + const targetNames = new Set(); + const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); + if (targetStat.children) { + targetStat.children.forEach(child => { + targetNames.add(caseSensitive ? child.name : child.name.toLowerCase()); + }); + } + + const resourcesFiltered = coalesce((await Promises.settled(resources.map(async resource => { + if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) { + const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource))); + if (!confirmationResult.confirmed) { + return undefined; + } + } + + return resource; + })))); + + // Copy resources through bulk edit API + const resourceFileEdits = resourcesFiltered.map(resource => { + const sourceFileName = basename(resource); + const targetFile = joinPath(target.resource, sourceFileName); + + return new ResourceFileEdit(resource, targetFile, { overwrite: true, copy: true }); + }); + + await this.explorerService.applyBulkEdit(resourceFileEdits, { + undoLabel: resourcesFiltered.length === 1 ? + localize('copyFile', "Copy {0}", basename(resourcesFiltered[0])) : + localize('copynFile', "Copy {0} resources", resourcesFiltered.length), + progressLabel: resourcesFiltered.length === 1 ? + localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : + localize('copyingnFile', "Copying {0} resources", resourcesFiltered.length), + progressLocation: ProgressLocation.Window + }); + + // if we only add one file, just open it directly + if (resourceFileEdits.length === 1) { + const item = this.explorerService.findClosest(resourceFileEdits[0].newResource!); + if (item && !item.isDirectory) { + this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); + } + } + } + } +} + +//#endregion + +//#region Download (web, native) + +interface IDownloadOperation { + startTime: number; + progressScheduler: RunOnceWorker; + + filesTotal: number; + filesDownloaded: number; + + totalBytesDownloaded: number; + fileBytesDownloaded: number; +} + +export class FileDownload { + + constructor( + @IFileService private readonly fileService: IFileService, + @IExplorerService private readonly explorerService: IExplorerService, + @IProgressService private readonly progressService: IProgressService, + @ILogService private readonly logService: ILogService, + @IFileDialogService private readonly fileDialogService: IFileDialogService + ) { + } + + download(source: ExplorerItem[]): Promise { + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const downloadPromise = this.progressService.withProgress( + { + location: ProgressLocation.Window, + delay: 800, + cancellable: isWeb, + title: localize('downloadingFiles', "Downloading") + }, + async progress => this.doDownload(source, progress, cts), + () => cts.dispose(true) + ); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => downloadPromise); + + return downloadPromise; + } + + private async doDownload(sources: ExplorerItem[], progress: IProgress, cts: CancellationTokenSource): Promise { + for (const source of sources) { + if (cts.token.isCancellationRequested) { + return; + } + + // Web: use DOM APIs to download files with optional support + // for folders and large files + if (isWeb) { + await this.doDownloadBrowser(source.resource, progress, cts); + } + + // Native: use working copy file service to get at the contents + else { + await this.doDownloadNative(source, progress, cts); + } + } + } + + private async doDownloadBrowser(resource: URI, progress: IProgress, cts: CancellationTokenSource): Promise { + const stat = await this.fileService.resolve(resource, { resolveMetadata: true }); + + if (cts.token.isCancellationRequested) { + return; + } + + const maxBlobDownloadSize = 32 * ByteSize.MB; // avoid to download via blob-trick >32MB to avoid memory pressure + const preferFileSystemAccessWebApis = stat.isDirectory || stat.size > maxBlobDownloadSize; + + // Folder: use FS APIs to download files and folders if available and preferred + if (preferFileSystemAccessWebApis && WebFileSystemAccess.supported(window)) { + try { + const parentFolder: FileSystemDirectoryHandle = await window.showDirectoryPicker(); + const operation: IDownloadOperation = { + startTime: Date.now(), + progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), + + filesTotal: stat.isDirectory ? 0 : 1, // folders increment filesTotal within downloadFolder method + filesDownloaded: 0, + + totalBytesDownloaded: 0, + fileBytesDownloaded: 0 + }; + + if (stat.isDirectory) { + const targetFolder = await parentFolder.getDirectoryHandle(stat.name, { create: true }); + await this.downloadFolderBrowser(stat, targetFolder, operation, cts.token); + } else { + await this.downloadFileBrowser(parentFolder, stat, operation, cts.token); + } + + operation.progressScheduler.dispose(); + } catch (error) { + this.logService.warn(error); + cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels + } + } + + // File: use traditional download to circumvent browser limitations + else if (stat.isFile) { + let bufferOrUri: Uint8Array | URI; + try { + bufferOrUri = (await this.fileService.readFile(stat.resource, { limits: { size: maxBlobDownloadSize } })).value.buffer; + } catch (error) { + bufferOrUri = FileAccess.asBrowserUri(stat.resource); + } + + if (!cts.token.isCancellationRequested) { + triggerDownload(bufferOrUri, stat.name); + } + } + } + + private async downloadFileBufferedBrowser(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation, token: CancellationToken): Promise { + const contents = await this.fileService.readFileStream(resource); + if (token.isCancellationRequested) { + target.close(); + return; + } + + return new Promise((resolve, reject) => { + const sourceStream = contents.value; + + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => target.close())); + + let disposed = false; + disposables.add(toDisposable(() => disposed = true)); + + disposables.add(once(token.onCancellationRequested)(() => { + disposables.dispose(); + reject(); + })); + + listenStream(sourceStream, { + onData: data => { + if (!disposed) { + target.write(data.buffer); + this.reportProgress(contents.name, contents.size, data.byteLength, operation); + } + }, + onError: error => { + disposables.dispose(); + reject(error); + }, + onEnd: () => { + disposables.dispose(); + resolve(); + } + }); + }); + } + + private async downloadFileUnbufferedBrowser(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation, token: CancellationToken): Promise { + const contents = await this.fileService.readFile(resource); + if (!token.isCancellationRequested) { + target.write(contents.value.buffer); + this.reportProgress(contents.name, contents.size, contents.value.byteLength, operation); + } + + target.close(); + } + + private async downloadFileBrowser(targetFolder: FileSystemDirectoryHandle, file: IFileStatWithMetadata, operation: IDownloadOperation, token: CancellationToken): Promise { + + // Report progress + operation.filesDownloaded++; + operation.fileBytesDownloaded = 0; // reset for this file + this.reportProgress(file.name, 0, 0, operation); + + // Start to download + const targetFile = await targetFolder.getFileHandle(file.name, { create: true }); + const targetFileWriter = await targetFile.createWritable(); + + // For large files, write buffered using streams + if (file.size > ByteSize.MB) { + return this.downloadFileBufferedBrowser(file.resource, targetFileWriter, operation, token); + } + + // For small files prefer to write unbuffered to reduce overhead + return this.downloadFileUnbufferedBrowser(file.resource, targetFileWriter, operation, token); + } + + private async downloadFolderBrowser(folder: IFileStatWithMetadata, targetFolder: FileSystemDirectoryHandle, operation: IDownloadOperation, token: CancellationToken): Promise { + if (folder.children) { + operation.filesTotal += (folder.children.map(child => child.isFile)).length; + + for (const child of folder.children) { + if (token.isCancellationRequested) { + return; + } + + if (child.isFile) { + await this.downloadFileBrowser(targetFolder, child, operation, token); + } else { + const childFolder = await targetFolder.getDirectoryHandle(child.name, { create: true }); + const resolvedChildFolder = await this.fileService.resolve(child.resource, { resolveMetadata: true }); + + await this.downloadFolderBrowser(resolvedChildFolder, childFolder, operation, token); + } + } + } + } + + private reportProgress(name: string, fileSize: number, bytesDownloaded: number, operation: IDownloadOperation): void { + operation.fileBytesDownloaded += bytesDownloaded; + operation.totalBytesDownloaded += bytesDownloaded; + + const bytesDownloadedPerSecond = operation.totalBytesDownloaded / ((Date.now() - operation.startTime) / 1000); + + // Small file + let message: string; + if (fileSize < ByteSize.MB) { + if (operation.filesTotal === 1) { + message = name; + } else { + message = localize('downloadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesDownloaded, operation.filesTotal, ByteSize.formatSize(bytesDownloadedPerSecond)); + } + } + + // Large file + else { + message = localize('downloadProgressLarge', "{0} ({1} of {2}, {3}/s)", name, ByteSize.formatSize(operation.fileBytesDownloaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesDownloadedPerSecond)); + } + + // Report progress but limit to update only once per second + operation.progressScheduler.work({ message }); + } + + private async doDownloadNative(explorerItem: ExplorerItem, progress: IProgress, cts: CancellationTokenSource): Promise { + progress.report({ message: explorerItem.name }); + + const defaultUri = joinPath( + explorerItem.isDirectory ? + await this.fileDialogService.defaultFolderPath(Schemas.file) : + await this.fileDialogService.defaultFilePath(Schemas.file), + explorerItem.name + ); + + const destination = await this.fileDialogService.showSaveDialog({ + availableFileSystems: [Schemas.file], + saveLabel: mnemonicButtonLabel(localize('downloadButton', "Download")), + title: localize('chooseWhereToDownload', "Choose Where to Download"), + defaultUri + }); + + if (destination) { + await this.explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { + undoLabel: localize('downloadBulkEdit', "Download {0}", explorerItem.name), + progressLabel: localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), + progressLocation: ProgressLocation.Window + }); + } else { + cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 + } + } +} + +//#endregion + +//#region Helpers + +export function getFileOverwriteConfirm(name: string): IConfirmation { + return { + message: localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", name), + detail: localize('irreversible', "This action is irreversible!"), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; +} + +export function getMultipleFilesOverwriteConfirm(files: URI[]): IConfirmation { + if (files.length > 1) { + return { + message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length), + detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + } + + return getFileOverwriteConfirm(basename(files[0])); +} + +//#endregion diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/files.contribution.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/files.contribution.ts index 196ff12fa9e1..b9ff354280e3 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -8,16 +8,16 @@ import { sep } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { IFileEditorInput, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, FILES_EXCLUDE_CONFIG, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; -import { SortOrder, FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; +import { SortOrder, LexicographicOptions, FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import * as platform from 'vs/base/common/platform'; +import { isNative, isWeb, isWindows } from 'vs/base/common/platform'; import { ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -31,7 +31,8 @@ import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEdito import { DirtyFilesIndicator } from 'vs/workbench/contrib/files/common/dirtyFilesIndicator'; import { UndoCommand, RedoCommand } from 'vs/editor/browser/editorExtensions'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { FileEditorInputSerializer, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { FileEditorInputSerializer, FileEditorWorkingCopyEditorHandler } from 'vs/workbench/contrib/files/browser/editors/fileEditorHandler'; class FileUriLabelContribution implements IWorkbenchContribution { @@ -41,8 +42,8 @@ class FileUriLabelContribution implements IWorkbenchContribution { formatting: { label: '${authority}${path}', separator: sep, - tildify: !platform.isWindows, - normalizeDriveLetter: platform.isWindows, + tildify: !isWindows, + normalizeDriveLetter: isWindows, authorityPrefix: sep + sep, workspaceSuffix: '' } @@ -60,7 +61,7 @@ Registry.as(EditorExtensions.Editors).registerEditor( nls.localize('binaryFileEditor', "Binary File Editor") ), [ - new SyncDescriptor(FileEditorInput) + new SyncDescriptor(FileEditorInput) ] ); @@ -69,8 +70,8 @@ Registry.as(EditorExtensions.EditorInputFactories). typeId: FILE_EDITOR_INPUT_ID, - createFileEditorInput: (resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, instantiationService): IFileEditorInput => { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode); + createFileEditorInput: (resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, preferredContents, instantiationService): IFileEditorInput => { + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, preferredEncoding, preferredMode, preferredContents); }, isFileEditorInput: (obj): obj is IFileEditorInput => { @@ -78,8 +79,9 @@ Registry.as(EditorExtensions.EditorInputFactories). } }); -// Register Editor Input Serializer +// Register Editor Input Serializer & Handler Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer(FILE_EDITOR_INPUT_ID, FileEditorInputSerializer); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileEditorWorkingCopyEditorHandler, LifecyclePhase.Ready); // Register Explorer views Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExplorerViewletViewsContribution, LifecyclePhase.Starting); @@ -102,7 +104,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? +const hotExitConfiguration: IConfigurationPropertySchema = isNative ? { 'type': 'string', 'scope': ConfigurationScope.APPLICATION, @@ -227,7 +229,7 @@ configurationRegistry.registerConfiguration({ nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty editor is automatically saved when the editor loses focus."), nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty editor is automatically saved when the window loses focus.") ], - 'default': platform.isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF, + 'default': isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF, 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty editors. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) }, 'files.autoSaveDelay': { @@ -237,7 +239,7 @@ configurationRegistry.registerConfiguration({ }, 'files.watcherExclude': { 'type': 'object', - 'default': platform.isWindows /* https://github.com/microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true, '**/.hg/store/**': true }, + 'default': isWindows /* https://github.com/microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true, '**/.hg/store/**': true }, 'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths (i.e. prefix with ** or the full path to match properly). Changing this setting requires a restart. When you experience Code consuming lots of CPU time on startup, you can exclude large folders to reduce the initial load."), 'scope': ConfigurationScope.RESOURCE }, @@ -250,7 +252,7 @@ configurationRegistry.registerConfiguration({ 'type': 'number', 'default': 4096, 'markdownDescription': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying `--max-memory=NEWSIZE` on the command line."), - included: platform.isNative + included: isNative }, 'files.restoreUndoStack': { 'type': 'boolean', @@ -356,13 +358,25 @@ configurationRegistry.registerConfiguration({ 'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.Modified], 'default': SortOrder.Default, 'enumDescriptions': [ - nls.localize('sortOrder.default', 'Files and folders are sorted by their names, in alphabetical order. Folders are displayed before files.'), - nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names, in alphabetical order. Files are interwoven with folders.'), - nls.localize('sortOrder.filesFirst', 'Files and folders are sorted by their names, in alphabetical order. Files are displayed before folders.'), - nls.localize('sortOrder.type', 'Files and folders are sorted by their extensions, in alphabetical order. Folders are displayed before files.'), - nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date, in descending order. Folders are displayed before files.') + nls.localize('sortOrder.default', 'Files and folders are sorted by their names. Folders are displayed before files.'), + nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names. Files are interwoven with folders.'), + nls.localize('sortOrder.filesFirst', 'Files and folders are sorted by their names. Files are displayed before folders.'), + nls.localize('sortOrder.type', 'Files and folders are grouped by extension type then sorted by their names. Folders are displayed before files.'), + nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date in descending order. Folders are displayed before files.') ], - 'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.") + 'description': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer.") + }, + 'explorer.sortOrderLexicographicOptions': { + 'type': 'string', + 'enum': [LexicographicOptions.Default, LexicographicOptions.Upper, LexicographicOptions.Lower, LexicographicOptions.Unicode], + 'default': LexicographicOptions.Default, + 'enumDescriptions': [ + nls.localize('sortOrderLexicographicOptions.default', 'Uppercase and lowercase names are mixed together.'), + nls.localize('sortOrderLexicographicOptions.upper', 'Uppercase names are grouped together before lowercase names.'), + nls.localize('sortOrderLexicographicOptions.lower', 'Lowercase names are grouped together before uppercase names.'), + nls.localize('sortOrderLexicographicOptions.unicode', 'Names are sorted in unicode order.') + ], + 'description': nls.localize('sortOrderLexicographicOptions', "Controls the lexicographic sorting of file and folder names in the explorer.") }, 'explorer.decorations.colors': { type: 'boolean', diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/files.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/files.ts index 0cf9348d3d2b..ad2dd9354d51 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/files.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/files.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; -import { OpenEditor, SortOrder } from 'vs/workbench/contrib/files/common/files'; -import { EditorResourceAccessor, SideBySideEditor, IEditorIdentifier, EditorInput, IEditorInputSerializer } from 'vs/workbench/common/editor'; +import { OpenEditor, ISortOrderConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { EditorResourceAccessor, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; @@ -14,67 +14,14 @@ import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditableData } from 'vs/workbench/common/views'; -import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { ProgressLocation } from 'vs/platform/progress/common/progress'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { isEqual } from 'vs/base/common/resources'; - -interface ISerializedFileEditorInput { - resourceJSON: UriComponents; - preferredResourceJSON?: UriComponents; - name?: string; - description?: string; - encoding?: string; - modeId?: string; -} - -export class FileEditorInputSerializer implements IEditorInputSerializer { - - canSerialize(editorInput: EditorInput): boolean { - return true; - } - - serialize(editorInput: EditorInput): string { - const fileEditorInput = editorInput as FileEditorInput; - const resource = fileEditorInput.resource; - const preferredResource = fileEditorInput.preferredResource; - const serializedFileEditorInput: ISerializedFileEditorInput = { - resourceJSON: resource.toJSON(), - preferredResourceJSON: isEqual(resource, preferredResource) ? undefined : preferredResource, // only storing preferredResource if it differs from the resource - name: fileEditorInput.getPreferredName(), - description: fileEditorInput.getPreferredDescription(), - encoding: fileEditorInput.getEncoding(), - modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data - }; - - return JSON.stringify(serializedFileEditorInput); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput { - return instantiationService.invokeFunction(accessor => { - const serializedFileEditorInput: ISerializedFileEditorInput = JSON.parse(serializedEditorInput); - const resource = URI.revive(serializedFileEditorInput.resourceJSON); - const preferredResource = URI.revive(serializedFileEditorInput.preferredResourceJSON); - const name = serializedFileEditorInput.name; - const description = serializedFileEditorInput.description; - const encoding = serializedFileEditorInput.encoding; - const mode = serializedFileEditorInput.modeId; - - const fileEditorInput = accessor.get(IEditorService).createEditorInput({ resource, label: name, description, encoding, mode, forceFile: true }) as FileEditorInput; - if (preferredResource) { - fileEditorInput.setPreferredResource(preferredResource); - } - - return fileEditorInput; - }); - } -} export interface IExplorerService { readonly _serviceBrand: undefined; readonly roots: ExplorerItem[]; - readonly sortOrder: SortOrder; + readonly sortOrderConfiguration: ISortOrderConfiguration; getContext(respectMultiSelection: boolean): ExplorerItem[]; hasViewFocus(): boolean; diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/files.web.contribution.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/files.web.contribution.ts index 729150402ca3..b91ef6e6e063 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/files.web.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/files.web.contribution.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorExtensions, EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { EditorExtensions } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; @@ -16,9 +16,9 @@ Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextFileEditor, TextFileEditor.ID, - nls.localize('textFileEditor', "Text File Editor") + localize('textFileEditor', "Text File Editor") ), [ - new SyncDescriptor(FileEditorInput) + new SyncDescriptor(FileEditorInput) ] ); diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.ts index a519921ad8e0..91d8c0d79328 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, VIEWLET_ID, ExplorerResourceNotReadonlyContext } from 'vs/workbench/contrib/files/common/files'; import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileActions'; import * as DOM from 'vs/base/browser/dom'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -55,6 +55,7 @@ import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { Codicon } from 'vs/base/common/codicons'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -169,6 +170,7 @@ export class ExplorerView extends ViewPane { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IProgressService private readonly progressService: IProgressService, @IEditorService private readonly editorService: IEditorService, + @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @@ -493,8 +495,8 @@ export class ExplorerView extends ViewPane { this.rootContext.set(!stat || (stat && stat.isRoot)); if (resource) { - const overrides = resource ? this.editorService.getEditorOverrides(resource, undefined, undefined) : []; - this.availableEditorIdsContext.set(overrides.map(([, entry]) => entry.id).join(',')); + const overrides = resource ? this.editorOverrideService.getEditorIds(resource) : []; + this.availableEditorIdsContext.set(overrides.join(',')); } else { this.availableEditorIdsContext.reset(); } @@ -863,6 +865,7 @@ registerAction2(class extends Action2 { title: nls.localize('createNewFile', "New File"), f1: false, icon: Codicon.newFile, + precondition: ExplorerResourceNotReadonlyContext, menu: { id: MenuId.ViewTitle, group: 'navigation', @@ -885,6 +888,7 @@ registerAction2(class extends Action2 { title: nls.localize('createNewFolder', "New Folder"), f1: false, icon: Codicon.newFolder, + precondition: ExplorerResourceNotReadonlyContext, menu: { id: MenuId.ViewTitle, group: 'navigation', diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 5ef31be6d7d1..aa024f8993a4 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -7,9 +7,9 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as DOM from 'vs/base/browser/dom'; import * as glob from 'vs/base/common/glob'; import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; -import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, } from 'vs/platform/progress/common/progress'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, ByteSize } from 'vs/platform/files/common/files'; +import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -19,8 +19,8 @@ import { ITreeNode, ITreeFilter, TreeVisibility, IAsyncDataSource, ITreeSorter, import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; -import { dirname, joinPath, basename, distinctParents } from 'vs/base/common/resources'; +import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { dirname, joinPath, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -29,18 +29,16 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; -import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd'; +import { compareFileExtensionsDefault, compareFileNamesDefault, compareFileNamesUpper, compareFileExtensionsUpper, compareFileNamesLower, compareFileExtensionsLower, compareFileNamesUnicode, compareFileExtensionsUnicode } from 'vs/base/common/comparers'; +import { fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { NativeDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { URI } from 'vs/base/common/uri'; -import { RunOnceWorker } from 'vs/base/common/async'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; @@ -49,16 +47,16 @@ import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { VSBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { isNumber } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; import { IEditableData } from 'vs/workbench/common/views'; import { IEditorInput } from 'vs/workbench/common/editor'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { BrowserFileUpload, NativeFileImport, getMultipleFilesOverwriteConfirm } from 'vs/workbench/contrib/files/browser/fileImportExport'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -94,7 +92,7 @@ export class ExplorerDataSource implements IAsyncDataSource { if (element instanceof ExplorerItem && element.isRoot) { @@ -669,7 +667,29 @@ export class FileSorter implements ITreeSorter { return 1; } - const sortOrder = this.explorerService.sortOrder; + const sortOrder = this.explorerService.sortOrderConfiguration.sortOrder; + const lexicographicOptions = this.explorerService.sortOrderConfiguration.lexicographicOptions; + + let compareFileNames; + let compareFileExtensions; + switch (lexicographicOptions) { + case 'upper': + compareFileNames = compareFileNamesUpper; + compareFileExtensions = compareFileExtensionsUpper; + break; + case 'lower': + compareFileNames = compareFileNamesLower; + compareFileExtensions = compareFileExtensionsLower; + break; + case 'unicode': + compareFileNames = compareFileNamesUnicode; + compareFileExtensions = compareFileExtensionsUnicode; + break; + default: + // 'default' + compareFileNames = compareFileNamesDefault; + compareFileExtensions = compareFileExtensionsDefault; + } // Sort Directories switch (sortOrder) { @@ -683,7 +703,7 @@ export class FileSorter implements ITreeSorter { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNamesDefault(statA.name, statB.name); + return compareFileNames(statA.name, statB.name); } break; @@ -717,74 +737,21 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensionsDefault(statA.name, statB.name); + return compareFileExtensions(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNamesDefault(statA.name, statB.name); + return compareFileNames(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNamesDefault(statA.name, statB.name); + return compareFileNames(statA.name, statB.name); } } } -function getFileOverwriteConfirm(name: string): IConfirmation { - return { - message: localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", name), - detail: localize('irreversible', "This action is irreversible!"), - primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; -} - -function getMultipleFilesOverwriteConfirm(files: URI[]): IConfirmation { - if (files.length > 1) { - return { - message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length), - detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"), - primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - } - - return getFileOverwriteConfirm(basename(files[0])); -} - -interface IWebkitDataTransfer { - items: IWebkitDataTransferItem[]; -} - -interface IWebkitDataTransferItem { - webkitGetAsEntry(): IWebkitDataTransferItemEntry; -} - -interface IWebkitDataTransferItemEntry { - name: string | undefined; - isFile: boolean; - isDirectory: boolean; - - file(resolve: (file: File) => void, reject: () => void): void; - createReader(): IWebkitDataTransferItemEntryReader; -} - -interface IWebkitDataTransferItemEntryReader { - readEntries(resolve: (file: IWebkitDataTransferItemEntry[]) => void, reject: () => void): void -} - -interface IUploadOperation { - startTime: number; - progressScheduler: RunOnceWorker; - - filesTotal: number; - filesUploaded: number; - - totalBytesUploaded: number; -} - export class FileDragAndDrop implements ITreeDragAndDrop { private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; @@ -795,7 +762,6 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private dropEnabled = false; constructor( - @INotificationService private notificationService: INotificationService, @IExplorerService private explorerService: IExplorerService, @IEditorService private editorService: IEditorService, @IDialogService private dialogService: IDialogService, @@ -803,9 +769,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IFileService private fileService: IFileService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private instantiationService: IInstantiationService, - @IHostService private hostService: IHostService, @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, - @IProgressService private readonly progressService: IProgressService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { this.toDispose = []; @@ -867,6 +831,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES, DataTransfers.RESOURCES)) { return false; } + if (isWeb && originalEvent.dataTransfer?.types.indexOf('Files') === -1) { + // DnD from vscode to web is not supported #115535. Only if we are dragging from native finder / explorer then the "Files" data transfer will be set + return false; + } } // Other-Tree DND @@ -964,7 +932,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData, originalEvent); if (items && items.length && originalEvent.dataTransfer) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, items, undefined, originalEvent); + this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, items, originalEvent)); // The only custom data transfer we set from the explorer is a file transfer // to be able to DND between multiple code file explorers across windows @@ -1002,358 +970,25 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return; } - // Desktop DND (Import file) - if (data instanceof NativeDragAndDropData) { - const cts = new CancellationTokenSource(); - - if (isWeb) { - // Indicate progress globally - const dropPromise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: true, - title: localize('uploadingFiles', "Uploading") - }, async progress => { - try { - await this.handleWebExternalDrop(resolvedTarget, originalEvent, progress, cts.token); - } catch (error) { - this.notificationService.warn(error); - } - }, () => cts.dispose(true)); - // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => dropPromise); - } else { - try { - await this.handleExternalDrop(resolvedTarget, originalEvent, cts.token); - } catch (error) { - this.notificationService.warn(error); - } - } - } - // In-Explorer DND (Move/Copy file) - else { - this.handleExplorerDrop(data as ElementsDragAndDropData, resolvedTarget, originalEvent).then(undefined, e => this.notificationService.warn(e)); - } - } - - private async handleWebExternalDrop(target: ExplorerItem, originalEvent: DragEvent, progress: IProgress, token: CancellationToken): Promise { - const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; - - // Somehow the items thing is being modified at random, maybe as a security - // measure since this is a DND operation. As such, we copy the items into - // an array we own as early as possible before using it. - const entries: IWebkitDataTransferItemEntry[] = []; - for (const item of items) { - entries.push(item.webkitGetAsEntry()); - } - - const results: { isFile: boolean, resource: URI }[] = []; - const operation: IUploadOperation = { - startTime: Date.now(), - progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), - - filesTotal: entries.length, - filesUploaded: 0, - - totalBytesUploaded: 0 - }; - - for (let entry of entries) { - if (token.isCancellationRequested) { - break; - } - - // Confirm overwrite as needed - if (target && entry.name && target.getChild(entry.name)) { - const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); - if (!confirmed) { - continue; - } - - await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true })], { - undoLabel: localize('overwrite', "Overwrite {0}", entry.name), - progressLabel: localize('overwriting', "Overwriting {0}", entry.name), - }); - - if (token.isCancellationRequested) { - break; - } - } - - // Upload entry - const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, token); - if (result) { - results.push(result); - } - } - - operation.progressScheduler.dispose(); - - // Open uploaded file in editor only if we upload just one - const firstUploadedFile = results[0]; - if (!token.isCancellationRequested && firstUploadedFile?.isFile) { - await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } }); - } - } - - private async doUploadWebFileEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress, operation: IUploadOperation, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> { - if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) { - return undefined; - } - - // Report progress - let fileBytesUploaded = 0; - const reportProgress = (fileSize: number, bytesUploaded: number): void => { - fileBytesUploaded += bytesUploaded; - operation.totalBytesUploaded += bytesUploaded; - - const bytesUploadedPerSecond = operation.totalBytesUploaded / ((Date.now() - operation.startTime) / 1000); + try { - // Small file - let message: string; - if (fileSize < ByteSize.MB) { - if (operation.filesTotal === 1) { - message = `${entry.name}`; + // Desktop DND (Import file) + if (data instanceof NativeDragAndDropData) { + if (isWeb) { + const browserUpload = this.instantiationService.createInstance(BrowserFileUpload); + await browserUpload.upload(target, originalEvent); } else { - message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, ByteSize.formatSize(bytesUploadedPerSecond)); + const nativeImport = this.instantiationService.createInstance(NativeFileImport); + await nativeImport.import(resolvedTarget, originalEvent); } } - // Large file + // In-Explorer DND (Move/Copy file) else { - message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, ByteSize.formatSize(fileBytesUploaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesUploadedPerSecond)); + await this.handleExplorerDrop(data as ElementsDragAndDropData, resolvedTarget, originalEvent); } - - // Report progress but limit to update only once per second - operation.progressScheduler.work({ message }); - }; - operation.filesUploaded++; - reportProgress(0, 0); - - // Handle file upload - const resource = joinPath(parentResource, entry.name); - if (entry.isFile) { - const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); - - if (token.isCancellationRequested) { - return undefined; - } - - // Chrome/Edge/Firefox support stream method, but only use it for - // larger files to reduce the overhead of the streaming approach - if (typeof file.stream === 'function' && file.size > ByteSize.MB) { - await this.doUploadWebFileEntryBuffered(resource, file, reportProgress, token); - } - - // Fallback to unbuffered upload for other browsers or small files - else { - await this.doUploadWebFileEntryUnbuffered(resource, file, reportProgress); - } - - return { isFile: true, resource }; - } - - // Handle folder upload - else { - - // Create target folder - await this.fileService.createFolder(resource); - - if (token.isCancellationRequested) { - return undefined; - } - - // Recursive upload files in this directory - const dirReader = entry.createReader(); - const childEntries: IWebkitDataTransferItemEntry[] = []; - let done = false; - do { - const childEntriesChunk = await new Promise((resolve, reject) => dirReader.readEntries(resolve, reject)); - if (childEntriesChunk.length > 0) { - childEntries.push(...childEntriesChunk); - } else { - done = true; // an empty array is a signal that all entries have been read - } - } while (!done && !token.isCancellationRequested); - - // Update operation total based on new counts - operation.filesTotal += childEntries.length; - - // Upload all entries as files to target - const folderTarget = target && target.getChild(entry.name) || undefined; - for (let childEntry of childEntries) { - await this.doUploadWebFileEntry(childEntry, resource, folderTarget, progress, operation, token); - } - - return { isFile: false, resource }; - } - } - - private async doUploadWebFileEntryBuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void, token: CancellationToken): Promise { - const writeableStream = newWriteableBufferStream({ - // Set a highWaterMark to prevent the stream - // for file upload to produce large buffers - // in-memory - highWaterMark: 10 - }); - const writeFilePromise = this.fileService.writeFile(resource, writeableStream); - - // Read the file in chunks using File.stream() web APIs - try { - const reader: ReadableStreamDefaultReader = file.stream().getReader(); - - let res = await reader.read(); - while (!res.done) { - if (token.isCancellationRequested) { - return undefined; - } - - // Write buffer into stream but make sure to wait - // in case the highWaterMark is reached - const buffer = VSBuffer.wrap(res.value); - await writeableStream.write(buffer); - - if (token.isCancellationRequested) { - return undefined; - } - - // Report progress - progressReporter(file.size, buffer.byteLength); - - res = await reader.read(); - } - writeableStream.end(undefined); } catch (error) { - writeableStream.error(error); - writeableStream.end(); - } - - if (token.isCancellationRequested) { - return undefined; - } - - // Wait for file being written to target - await writeFilePromise; - } - - private doUploadWebFileEntryUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = async event => { - try { - if (event.target?.result instanceof ArrayBuffer) { - const buffer = VSBuffer.wrap(new Uint8Array(event.target.result)); - await this.fileService.writeFile(resource, buffer); - - // Report progress - progressReporter(file.size, buffer.byteLength); - } else { - throw new Error('Could not read from dropped file.'); - } - - resolve(); - } catch (error) { - reject(error); - } - }; - - // Start reading the file to trigger `onload` - reader.readAsArrayBuffer(file); - }); - } - - private async handleExternalDrop(target: ExplorerItem, originalEvent: DragEvent, token: CancellationToken): Promise { - - // Check for dropped external files to be folders - const droppedResources = extractResources(originalEvent, true); - const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource }))); - - if (token.isCancellationRequested) { - return; - } - - // Pass focus to window - this.hostService.focus(); - - // Handle folders by adding to workspace if we are in workspace context and if dropped on top - const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource })); - if (folders.length > 0 && target.isRoot) { - const buttons = [ - folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"), - localize('cancel', "Cancel") - ]; - const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme); - let message = folders.length > 1 ? localize('copyfolders', "Are you sure to want to copy folders?") : localize('copyfolder', "Are you sure to want to copy '{0}'?", basename(folders[0].uri)); - if (folders.some(f => workspaceFolderSchemas.indexOf(f.uri.scheme) >= 0)) { - // We only allow to add a folder to the workspace if there is already a workspace folder with that scheme - buttons.unshift(folders.length > 1 ? localize('addFolders', "&&Add Folders to Workspace") : localize('addFolder', "&&Add Folder to Workspace")); - message = folders.length > 1 ? localize('dropFolders', "Do you want to copy the folders or add the folders to the workspace?") - : localize('dropFolder', "Do you want to copy '{0}' or add '{0}' as a folder to the workspace?", basename(folders[0].uri)); - } - - const { choice } = await this.dialogService.show(Severity.Info, message, buttons); - if (choice === buttons.length - 3) { - return this.workspaceEditingService.addFolders(folders); - } - if (choice === buttons.length - 2) { - return this.addResources(target, droppedResources.map(res => res.resource), token); - } - - return undefined; - } - - // Handle dropped files (only support FileStat as target) - else if (target instanceof ExplorerItem) { - return this.addResources(target, droppedResources.map(res => res.resource), token); - } - } - - private async addResources(target: ExplorerItem, resources: URI[], token: CancellationToken): Promise { - if (resources && resources.length > 0) { - - // Resolve target to check for name collisions and ask user - const targetStat = await this.fileService.resolve(target.resource); - - if (token.isCancellationRequested) { - return; - } - - // Check for name collisions - const targetNames = new Set(); - const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); - if (targetStat.children) { - targetStat.children.forEach(child => { - targetNames.add(caseSensitive ? child.name : child.name.toLowerCase()); - }); - } - - const resourcesFiltered = (await Promise.all(resources.map(async resource => { - if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) { - const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource))); - if (!confirmationResult.confirmed) { - return undefined; - } - } - return resource; - }))).filter(r => r instanceof URI) as URI[]; - const resourceFileEdits = resourcesFiltered.map(resource => { - const sourceFileName = basename(resource); - const targetFile = joinPath(target.resource, sourceFileName); - return new ResourceFileEdit(resource, targetFile, { overwrite: true, copy: true }); - }); - - await this.explorerService.applyBulkEdit(resourceFileEdits, { - undoLabel: resourcesFiltered.length === 1 ? localize('copyFile', "Copy {0}", basename(resourcesFiltered[0])) : localize('copynFile', "Copy {0} resources", resourcesFiltered.length), - progressLabel: resourcesFiltered.length === 1 ? localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : localize('copyingnFile', "Copying {0} resources", resourcesFiltered.length) - }); - - // if we only add one file, just open it directly - if (resourceFileEdits.length === 1) { - const item = this.explorerService.findClosest(resourceFileEdits[0].newResource!); - if (item && !item.isDirectory) { - this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); - } - } + this.dialogService.show(Severity.Error, toErrorMessage(error), [localize('ok', 'OK')]); } } @@ -1395,15 +1030,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const sources = items.filter(s => !s.isRoot); if (isCopy) { - await this.doHandleExplorerDropOnCopy(sources, target); - } else { - return this.doHandleExplorerDropOnMove(sources, target); + return this.doHandleExplorerDropOnCopy(sources, target); } + + return this.doHandleExplorerDropOnMove(sources, target); } - private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise { + private async doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise { if (roots.length === 0) { - return Promise.resolve(undefined); + return; } const folders = this.contextService.getWorkspace().folders; @@ -1431,10 +1066,12 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } workspaceCreationData.splice(targetIndex, 0, ...rootsToMove); + return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData); } private async doHandleExplorerDropOnCopy(sources: ExplorerItem[], target: ExplorerItem): Promise { + // Reuse duplicate action when user copies const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming; const resourceFileEdits = sources.map(({ resource, isDirectory }) => (new ResourceFileEdit(resource, findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming), { copy: true }))); @@ -1465,6 +1102,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { try { await this.explorerService.applyBulkEdit(resourceFileEdits, options); } catch (error) { + // Conflict if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { @@ -1475,20 +1113,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - const confirm = getMultipleFilesOverwriteConfirm(overwrites); // Move with overwrite if the user confirms + const confirm = getMultipleFilesOverwriteConfirm(overwrites); const { confirmed } = await this.dialogService.confirm(confirm); if (confirmed) { - try { - await this.explorerService.applyBulkEdit(resourceFileEdits.map(re => new ResourceFileEdit(re.oldResource, re.newResource, { overwrite: true })), options); - } catch (error) { - this.notificationService.error(error); - } + await this.explorerService.applyBulkEdit(resourceFileEdits.map(re => new ResourceFileEdit(re.oldResource, re.newResource, { overwrite: true })), options); } } - // Any other error + + // Any other error: bubble up else { - this.notificationService.error(error); + throw error; } } } diff --git a/lib/vscode/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/lib/vscode/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 2aa047e2cb22..d8176ad5cffc 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -13,7 +13,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IEditorInput, Verbosity, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, Verbosity, EditorResourceAccessor, SideBySideEditor, EditorInputCapabilities, IEditorIdentifier } from 'vs/workbench/common/editor'; import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files'; import { CloseAllEditorsAction, CloseEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; @@ -32,13 +32,12 @@ import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/m import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView'; -import { URI } from 'vs/base/common/uri'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { isWeb } from 'vs/base/common/platform'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -164,6 +163,7 @@ export class OpenEditorsView extends ViewPane { } case GroupChangeKind.EDITOR_DIRTY: case GroupChangeKind.EDITOR_LABEL: + case GroupChangeKind.EDITOR_CAPABILITIES: case GroupChangeKind.EDITOR_STICKY: case GroupChangeKind.EDITOR_PIN: { this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]); @@ -269,7 +269,7 @@ export class OpenEditorsView extends ViewPane { if (element instanceof OpenEditor) { const resource = element.getResource(); this.dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving()); - this.readonlyEditorFocusedContext.set(element.editor.isReadonly()); + this.readonlyEditorFocusedContext.set(element.editor.hasCapability(EditorInputCapabilities.Readonly)); this.resourceContext.set(withUndefinedAsNull(resource)); } else if (!!element) { this.groupFocusedContext.set(true); @@ -653,21 +653,18 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop).elements; - const resources: URI[] = []; + const editors: IEditorIdentifier[] = []; if (items) { - items.forEach(i => { - if (i instanceof OpenEditor) { - const resource = i.getResource(); - if (resource) { - resources.push(resource); - } + for (const item of items) { + if (item instanceof OpenEditor) { + editors.push(item); } - }); + } } - if (resources.length) { + if (editors.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent); + this.instantiationService.invokeFunction(fillEditorsDragData, editors, originalEvent); } } diff --git a/lib/vscode/src/vs/workbench/contrib/files/common/explorerModel.ts b/lib/vscode/src/vs/workbench/contrib/files/common/explorerModel.ts index 4809b811ad6c..82955982e10b 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -30,7 +30,7 @@ export class ExplorerModel implements IDisposable { fileService: IFileService ) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders - .map(folder => new ExplorerItem(folder.uri, fileService, undefined, true, false, folder.name)); + .map(folder => new ExplorerItem(folder.uri, fileService, undefined, true, false, false, folder.name)); setRoots(); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => { @@ -89,6 +89,7 @@ export class ExplorerItem { private _parent: ExplorerItem | undefined, private _isDirectory?: boolean, private _isSymbolicLink?: boolean, + private _readonly?: boolean, private _name: string = basenameOrAuthority(resource), private _mtime?: number, private _unknown = false @@ -124,7 +125,7 @@ export class ExplorerItem { } get isReadonly(): boolean { - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + return this._readonly || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } get mtime(): number | undefined { @@ -179,7 +180,7 @@ export class ExplorerItem { } static create(fileService: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { - const stat = new ExplorerItem(raw.resource, fileService, parent, raw.isDirectory, raw.isSymbolicLink, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory); + const stat = new ExplorerItem(raw.resource, fileService, parent, raw.isDirectory, raw.isSymbolicLink, raw.readonly, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory); // Recursively add children if present if (stat.isDirectory) { diff --git a/lib/vscode/src/vs/workbench/contrib/files/common/files.ts b/lib/vscode/src/vs/workbench/contrib/files/common/files.ts index eb075dbdf2a0..70f2fcf8bb61 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/common/files.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/common/files.ts @@ -83,6 +83,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb enableDragAndDrop: boolean; confirmDelete: boolean; sortOrder: SortOrder; + sortOrderLexicographicOptions: LexicographicOptions; decorations: { colors: boolean; badges: boolean; @@ -105,6 +106,18 @@ export const enum SortOrder { Modified = 'modified' } +export const enum LexicographicOptions { + Default = 'default', + Upper = 'upper', + Lower = 'lower', + Unicode = 'unicode', +} + +export interface ISortOrderConfiguration { + sortOrder: SortOrder; + lexicographicOptions: LexicographicOptions; +} + export class TextFileContentProvider extends Disposable implements ITextModelContentProvider { private readonly fileWatcherDisposable = this._register(new MutableDisposable()); @@ -119,8 +132,8 @@ export class TextFileContentProvider extends Disposable implements ITextModelCon static async open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise { await editorService.openEditor({ - leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource), - rightResource: resource, + originalInput: { resource: TextFileContentProvider.resourceToTextFile(scheme, resource) }, + modifiedInput: { resource }, label, options }); diff --git a/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts b/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts index fd0604b572c6..71724d782505 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts @@ -5,8 +5,8 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorExtensions, EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { EditorExtensions } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { NativeTextFileEditor } from 'vs/workbench/contrib/files/electron-sandbox/textFileEditor'; @@ -19,6 +19,6 @@ Registry.as(EditorExtensions.Editors).registerEditor( nls.localize('textFileEditor', "Text File Editor") ), [ - new SyncDescriptor(FileEditorInput) + new SyncDescriptor(FileEditorInput) ] ); diff --git a/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index a47e3abe916b..bb34adbf9767 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -5,8 +5,7 @@ import { localize } from 'vs/nls'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { FileOperationError, FileOperationResult, IFileService, MIN_MAX_MEMORY_SIZE_MB, FALLBACK_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { createErrorWithActions } from 'vs/base/common/errors'; import { toAction } from 'vs/base/common/actions'; @@ -25,6 +24,7 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { IProductService } from 'vs/platform/product/common/productService'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * An implementation of editor for file system resources. @@ -52,7 +52,7 @@ export class NativeTextFileEditor extends TextFileEditor { super(telemetryService, fileService, viewletService, instantiationService, contextService, storageService, textResourceConfigurationService, editorService, themeService, editorGroupService, textFileService, explorerService, uriIdentityService); } - protected override handleSetInputError(error: Error, input: FileEditorInput, options: EditorOptions | undefined): void { + protected override handleSetInputError(error: Error, input: FileEditorInput, options: ITextEditorOptions | undefined): void { // Allow to restart with higher memory limit if the file is too large if ((error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { diff --git a/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts b/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts index d7b872b7628f..877158e416fb 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts @@ -14,7 +14,7 @@ import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices const fileService = new TestFileService(); function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, false, name, mtime); + return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, false, false, name, mtime); } suite('Files - View Model', function () { @@ -245,19 +245,19 @@ suite('Files - View Model', function () { }); test('Merge Local with Disk', function () { - const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, 'to', Date.now()); - const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, 'to', Date.now()); + const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, false, 'to', Date.now()); + const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, false, 'to', Date.now()); // Merge Properties ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.mtime, merge2.mtime); // Merge Child when isDirectoryResolved=false is a no-op - merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, 'foo.html', Date.now())); + merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, false, 'foo.html', Date.now())); ExplorerItem.mergeLocalWithDisk(merge2, merge1); // Merge Child with isDirectoryResolved=true - const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, 'foo.html', Date.now()); + const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, false, 'foo.html', Date.now()); merge2.removeChild(child); merge2.addChild(child); (merge2)._isDirectoryResolved = true; diff --git a/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts b/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts index 8af54cf4ea73..723981a62385 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts @@ -19,7 +19,7 @@ const $ = dom.$; const fileService = new TestFileService(); function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number, isSymLink = false, isUnknown = false): ExplorerItem { - return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, isSymLink, name, mtime, isUnknown); + return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, isSymLink, false, name, mtime, isUnknown); } suite('Files - ExplorerView', () => { diff --git a/lib/vscode/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/lib/vscode/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index e447235e23f3..d4b44dc22da4 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -6,12 +6,12 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { toResource } from 'vs/base/test/common/utils'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { workbenchInstantiationService, TestServiceAccessor, TestEditorService, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInputFactoryRegistry, Verbosity, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, Verbosity, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; @@ -19,15 +19,16 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Registry } from 'vs/platform/registry/common/platform'; -import { FileEditorInputSerializer } from 'vs/workbench/contrib/files/browser/files'; +import { FileEditorInputSerializer } from 'vs/workbench/contrib/files/browser/editors/fileEditorHandler'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; suite('Files - FileEditorInput', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - function createFileInput(resource: URI, preferredResource?: URI, preferredMode?: string, preferredName?: string, preferredDescription?: string): FileEditorInput { - return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode); + function createFileInput(resource: URI, preferredResource?: URI, preferredMode?: string, preferredName?: string, preferredDescription?: string, preferredContents?: string): FileEditorInput { + return instantiationService.createInstance(FileEditorInput, resource, preferredResource, preferredName, preferredDescription, undefined, preferredMode, preferredContents); } setup(() => { @@ -57,6 +58,14 @@ suite('Files - FileEditorInput', () => { assert.ok(input.getDescription()); assert.ok(input.getTitle(Verbosity.SHORT)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Singleton)); + assert.ok(!input.hasCapability(EditorInputCapabilities.RequiresTrust)); + + const untypedInput = input.asResourceEditorInput(0); + assert.strictEqual(untypedInput.resource.toString(), input.resource.toString()); + assert.strictEqual('file.js', input.getName()); assert.strictEqual(toResource.call(this, '/foo/bar/file.js').fsPath, input.resource.fsPath); @@ -99,6 +108,30 @@ suite('Files - FileEditorInput', () => { } }); + test('reports as untitled without supported file scheme', async function () { + let input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingScheme' })); + + assert.ok(input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); + }); + + test('reports as readonly with readonly file scheme', async function () { + + class ReadonlyInMemoryFileSystemProvider extends InMemoryFileSystemProvider { + override readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly; + } + + const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', new ReadonlyInMemoryFileSystemProvider()); + try { + let input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); + + assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); + assert.ok(input.hasCapability(EditorInputCapabilities.Readonly)); + } finally { + disposable.dispose(); + } + }); + test('preferred resource', function () { const resource = toResource.call(this, '/foo/bar/updatefile.js'); const preferredResource = toResource.call(this, '/foo/bar/UPDATEFILE.js'); @@ -153,6 +186,32 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(model2.textEditorModel!.getModeId(), mode); }); + test('preferred contents', async function () { + const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, undefined, undefined, undefined, 'My contents'); + + const model = await input.resolve() as TextFileEditorModel; + assert.strictEqual(model.textEditorModel!.getValue(), 'My contents'); + assert.strictEqual(input.isDirty(), true); + + const untypedInput = input.asResourceEditorInput(0); + assert.strictEqual(untypedInput.contents, 'My contents'); + + input.setPreferredContents('Other contents'); + await input.resolve(); + assert.strictEqual(model.textEditorModel!.getValue(), 'Other contents'); + + model.textEditorModel?.setValue('Changed contents'); + await input.resolve(); + assert.strictEqual(model.textEditorModel!.getValue(), 'Changed contents'); // preferred contents only used once + + const input2 = createFileInput(toResource.call(this, '/foo/bar/file.js')); + input2.setPreferredContents('My contents'); + + const model2 = await input2.resolve() as TextFileEditorModel; + assert.strictEqual(model2.textEditorModel!.getValue(), 'My contents'); + assert.strictEqual(input2.isDirty(), true); + }); + test('matches', function () { const input1 = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); const input2 = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); @@ -342,4 +401,45 @@ suite('Files - FileEditorInput', () => { fileInput.dispose(); }); + + test('reports readonly changes', async function () { + const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); + + let listenerCount = 0; + const listener = input.onDidChangeCapabilities(() => { + listenerCount++; + }); + + const model = await accessor.textFileService.files.resolve(input.resource); + + assert.strictEqual(model.isReadonly(), false); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + + const stat = await accessor.fileService.resolve(input.resource, { resolveMetadata: true }); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: true }); + await input.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(model.isReadonly(), true); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), true); + assert.strictEqual(listenerCount, 1); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: false }); + await input.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(model.isReadonly(), false); + assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(listenerCount, 2); + + input.dispose(); + listener.dispose(); + }); }); diff --git a/lib/vscode/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/lib/vscode/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index 5df9a37e687e..bd45cd731f01 100644 --- a/lib/vscode/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -25,6 +25,8 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('Files - TextFileEditorTracker', () => { @@ -57,6 +59,7 @@ suite('Files - TextFileEditorTracker', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); @@ -143,15 +146,15 @@ suite('Files - TextFileEditorTracker', () => { test('dirty untitled text file model opens as editor', async function () { const accessor = await createTracker(); - const untitledEditor = accessor.editorService.createEditorInput({ forceUntitled: true }) as UntitledTextEditorInput; - const model = disposables.add(await untitledEditor.resolve()); + const untitledTextEditor = accessor.editorService.createEditorInput({ forceUntitled: true }) as UntitledTextEditorInput; + const model = disposables.add(await untitledTextEditor.resolve()); - assert.ok(!accessor.editorService.isOpened(untitledEditor)); + assert.ok(!accessor.editorService.isOpened(untitledTextEditor)); model.textEditorModel?.setValue('Super Good'); await awaitEditorOpening(accessor.editorService); - assert.ok(accessor.editorService.isOpened(untitledEditor)); + assert.ok(accessor.editorService.isOpened(untitledTextEditor)); }); function awaitEditorOpening(editorService: IEditorService): Promise { diff --git a/lib/vscode/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts b/lib/vscode/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts index 190c892525fa..51965b5220c9 100644 --- a/lib/vscode/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts @@ -13,7 +13,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IWebIssueService, WebIssueService } from 'vs/workbench/contrib/issue/browser/issueService'; -import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; +import { OpenIssueReporterArgs, OpenIssueReporterActionId, OpenIssueReporterApiCommandId } from 'vs/workbench/contrib/issue/common/commands'; class RegisterIssueContribution implements IWorkbenchContribution { @@ -34,6 +34,53 @@ class RegisterIssueContribution implements IWorkbenchContribution { return accessor.get(IWebIssueService).openReporter({ extensionId }); }); + CommandsRegistry.registerCommand({ + id: OpenIssueReporterApiCommandId, + handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { + let extensionId: string | undefined; + if (args) { + if (Array.isArray(args)) { + [extensionId] = args; + } else { + extensionId = args.extensionId; + } + } + + if (!!extensionId && typeof extensionId !== 'string') { + throw new Error(`Invalid argument when running '${OpenIssueReporterApiCommandId}: 'extensionId' must be of type string `); + } + + return accessor.get(IWebIssueService).openReporter({ extensionId }); + }, + description: { + description: 'Open the issue reporter and optionally prefill part of the form.', + args: [ + { + name: 'options', + description: 'Data to use to prefill the issue reporter with.', + isOptional: true, + schema: { + oneOf: [ + { + type: 'string', + description: 'The extension id to preselect.' + }, + { + type: 'object', + properties: { + extensionId: { + type: 'string' + }, + } + + } + ] + } + }, + ] + } + }); + const command: ICommandAction = { id: OpenIssueReporterActionId, title: { value: OpenIssueReporterActionLabel, original: 'Report Issue' }, diff --git a/lib/vscode/src/vs/workbench/contrib/issue/common/commands.ts b/lib/vscode/src/vs/workbench/contrib/issue/common/commands.ts index bcd6e252c88c..a5a9ff998f2d 100644 --- a/lib/vscode/src/vs/workbench/contrib/issue/common/commands.ts +++ b/lib/vscode/src/vs/workbench/contrib/issue/common/commands.ts @@ -5,6 +5,8 @@ export const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; +export const OpenIssueReporterApiCommandId = 'vscode.openIssueReporter'; + export interface OpenIssueReporterArgs { readonly extensionId?: string; readonly issueTitle?: string; diff --git a/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 2313fe40718d..c7f49c38f058 100644 --- a/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Registry } from 'vs/platform/registry/common/platform'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; -import { SyncActionDescriptor, ICommandAction, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { ICommandAction, MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { ReportPerformanceIssueUsingReporterAction, OpenProcessExplorer } from 'vs/workbench/contrib/issue/electron-sandbox/issueActions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; @@ -15,14 +14,10 @@ import { WorkbenchIssueService } from 'vs/workbench/services/issue/electron-sand import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; -import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; - -const workbenchActionsRegistry = Registry.as(Extensions.WorkbenchActions); +import { OpenIssueReporterArgs, OpenIssueReporterActionId, OpenIssueReporterApiCommandId } from 'vs/workbench/contrib/issue/common/commands'; if (!!product.reportIssueUrl) { - workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ReportPerformanceIssueUsingReporterAction), 'Help: Report Performance Issue', CATEGORIES.Help.value); - - const OpenIssueReporterActionLabel = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue..."); + registerAction2(ReportPerformanceIssueUsingReporterAction); CommandsRegistry.registerCommand(OpenIssueReporterActionId, function (accessor, args?: [string] | OpenIssueReporterArgs) { const data: Partial = Array.isArray(args) @@ -32,16 +27,81 @@ if (!!product.reportIssueUrl) { return accessor.get(IWorkbenchIssueService).openReporter(data); }); - const command: ICommandAction = { + CommandsRegistry.registerCommand({ + id: OpenIssueReporterApiCommandId, + handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { + const data: Partial = Array.isArray(args) + ? { extensionId: args[0] } + : args || {}; + + return accessor.get(IWorkbenchIssueService).openReporter(data); + }, + description: { + description: 'Open the issue reporter and optionally prefill part of the form.', + args: [ + { + name: 'options', + description: 'Data to use to prefill the issue reporter with.', + isOptional: true, + schema: { + oneOf: [ + { + type: 'string', + description: 'The extension id to preselect.' + }, + { + type: 'object', + properties: { + extensionId: { + type: 'string' + }, + issueTitle: { + type: 'string' + }, + issueBody: { + type: 'string' + } + } + + } + ] + } + }, + ] + } + }); + + const reportIssue: ICommandAction = { id: OpenIssueReporterActionId, - title: { value: OpenIssueReporterActionLabel, original: 'Report Issue' }, + title: { + value: localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue..."), + original: 'Report Issue...' + }, category: CATEGORIES.Help }; - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: reportIssue }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: OpenIssueReporterActionId, + title: localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") + }, + order: 3 + }); } -workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenProcessExplorer), 'Developer: Open Process Explorer', CATEGORIES.Developer.value); +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'workbench.action.openProcessExplorer', + title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") + }, + order: 2 +}); + +registerAction2(OpenProcessExplorer); registerSingleton(IWorkbenchIssueService, WorkbenchIssueService, true); diff --git a/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts b/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts index 79b6607dd351..5687050db907 100644 --- a/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts @@ -3,41 +3,49 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IssueType } from 'vs/platform/issue/common/issue'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; -export class OpenProcessExplorer extends Action { +export class OpenProcessExplorer extends Action2 { + static readonly ID = 'workbench.action.openProcessExplorer'; - static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); - - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); + + constructor() { + super({ + id: OpenProcessExplorer.ID, + title: { value: localize('openProcessExplorer', "Open Process Explorer"), original: 'Open Process Explorer' }, + category: CATEGORIES.Developer, + f1: true + }); } - override run(): Promise { - return this.issueService.openProcessExplorer(); + override async run(accessor: ServicesAccessor): Promise { + const issueService = accessor.get(IWorkbenchIssueService); + + return issueService.openProcessExplorer(); } } -export class ReportPerformanceIssueUsingReporterAction extends Action { +export class ReportPerformanceIssueUsingReporterAction extends Action2 { + static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; - static readonly LABEL = nls.localize({ key: 'reportPerformanceIssue', comment: [`Here, 'issue' means problem or bug`] }, "Report Performance Issue"); - - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); + + constructor() { + super({ + id: ReportPerformanceIssueUsingReporterAction.ID, + title: { value: localize({ key: 'reportPerformanceIssue', comment: [`Here, 'issue' means problem or bug`] }, "Report Performance Issue"), original: 'Report Performance Issue' }, + category: CATEGORIES.Help, + f1: true + }); } - override run(): Promise { - return this.issueService.openReporter({ issueType: IssueType.PerformanceIssue }); + override async run(accessor: ServicesAccessor): Promise { + const issueService = accessor.get(IWorkbenchIssueService); + + return issueService.openReporter({ issueType: IssueType.PerformanceIssue }); } } diff --git a/lib/vscode/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts b/lib/vscode/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts index 240ce1053f0a..ad4dd8df5665 100644 --- a/lib/vscode/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts @@ -41,7 +41,7 @@ export class ConfigureLocaleAction extends Action { return availableLanguages .map(language => { return { label: language }; }) - .concat({ label: localize('installAdditionalLanguages', "Install additional languages...") }); + .concat({ label: localize('installAdditionalLanguages', "Install Additional Languages...") }); } public override async run(): Promise { diff --git a/lib/vscode/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts b/lib/vscode/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts index 41baffa9e132..3eac5b211555 100644 --- a/lib/vscode/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts +++ b/lib/vscode/src/vs/workbench/contrib/markdown/common/markdownDocumentRenderer.ts @@ -18,6 +18,10 @@ body { margin: 0 auto; } +body *:last-child { + margin-bottom: 0; +} + img { max-width: 100%; max-height: 100%; @@ -153,16 +157,17 @@ function removeEmbeddedSVGs(documentContent: string): string { 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt', 'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote', 'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details', - 'caption', 'figure', 'figcaption', 'abbr', 'bdo', 'cite', 'dfn', 'mark', 'small', 'span', 'time', 'wbr' + 'caption', 'figure', 'figcaption', 'abbr', 'bdo', 'cite', 'dfn', 'mark', 'small', 'span', 'time', 'wbr', 'checkbox', 'checklist', 'vertically-centered' ], allowedAttributes: { '*': [ 'align', ], - img: ['src', 'alt', 'title', 'aria-label', 'width', 'height'], + img: ['src', 'alt', 'title', 'aria-label', 'width', 'height', 'centered'], span: ['class'], + checkbox: ['on-checked', 'checked-on', 'label', 'class'] }, - allowedSchemes: ['http', 'https', 'command',], + allowedSchemes: ['http', 'https', 'command'], filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { return token.tag !== 'svg'; } diff --git a/lib/vscode/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/lib/vscode/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index e0d049f9a566..14b184c6cdb7 100644 --- a/lib/vscode/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -374,7 +374,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); - this.markersStatusItem = this._register(this.statusbarService.addEntry(this.getMarkersItem(), 'status.problems', localize('status.problems', "Problems"), StatusbarAlignment.LEFT, 50 /* Medium Priority */)); + this.markersStatusItem = this._register(this.statusbarService.addEntry(this.getMarkersItem(), 'status.problems', StatusbarAlignment.LEFT, 50 /* Medium Priority */)); this.markerService.onMarkerChanged(() => this.markersStatusItem.update(this.getMarkersItem())); } @@ -382,6 +382,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont const markersStatistics = this.markerService.getStatistics(); const tooltip = this.getMarkersTooltip(markersStatistics); return { + name: localize('status.problems', "Problems"), text: this.getMarkersText(markersStatistics), ariaLabel: tooltip, tooltip, diff --git a/lib/vscode/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/lib/vscode/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index d8852663af1c..9b97b4d6bb1c 100644 --- a/lib/vscode/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/lib/vscode/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -32,7 +32,7 @@ import { Action, IAction } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; -import { fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; +import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; @@ -892,13 +892,13 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { const elements = (data as ElementsDragAndDropData).elements; - const resources: URI[] = elements + const resources = elements .filter(e => e instanceof ResourceMarkers) .map(resourceMarker => (resourceMarker as ResourceMarkers).resource); if (resources.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent); + this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent)); } } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/constants.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/constants.ts deleted file mode 100644 index 06627113262d..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/constants.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Scrollable Element - -export const SCROLLABLE_ELEMENT_PADDING_TOP = 20; -// export const SCROLLABLE_ELEMENT_PADDING_TOP_WITH_TOOLBAR = 8; - -// Code cell layout: -// [CODE_CELL_LEFT_MARGIN][CELL_RUN_GUTTER][editorWidth][CELL_RIGHT_MARGIN] - -// Markdown cell layout: -// [CELL_MARGIN][content][CELL_RIGHT_MARGIN] - -// Markdown editor cell layout: -// [CODE_CELL_LEFT_MARGIN][content][CELL_RIGHT_MARGIN] - -// Cell sizing related -export const CELL_RIGHT_MARGIN = 16; -export const CELL_RUN_GUTTER = 28; -export const CODE_CELL_LEFT_MARGIN = 32; - -export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_GAP = 18; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 22; -export const CELL_STATUSBAR_HEIGHT = 22; - -// Margin above editor -export const CELL_TOP_MARGIN = 6; -export const CELL_BOTTOM_MARGIN = 6; - -export const MARKDOWN_CELL_TOP_MARGIN = 8; -export const MARKDOWN_CELL_BOTTOM_MARGIN = 8; - -// Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` -// export const EDITOR_TOP_PADDING = 12; -export const EDITOR_BOTTOM_PADDING = 4; -export const EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR = 12; - -export const CELL_OUTPUT_PADDING = 14; - -export const COLLAPSED_INDICATOR_HEIGHT = 24; - -export const MARKDOWN_PREVIEW_PADDING = 8; diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts index d3ba97f52d78..ffdc4b2e985a 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts @@ -40,6 +40,12 @@ registerAction2(class extends NotebookCellAction { primary: KeyMod.Alt | KeyCode.UpArrow, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.equals('config.notebook.dragAndDropEnabled', false), + group: CellOverflowToolbarGroups.Edit, + order: 13 } }); } @@ -60,6 +66,12 @@ registerAction2(class extends NotebookCellAction { primary: KeyMod.Alt | KeyCode.DownArrow, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.equals('config.notebook.dragAndDropEnabled', false), + group: CellOverflowToolbarGroups.Edit, + order: 14 } }); } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts index 32671d6b33bc..095ab2e39ffb 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts @@ -17,9 +17,9 @@ suite('CellOperations', () => { test('Move cells - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -34,9 +34,9 @@ suite('CellOperations', () => { test('Move cells - multiple cells in a selection', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -53,9 +53,9 @@ suite('CellOperations', () => { test('Move cells - move with folding ranges', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -80,9 +80,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -99,9 +99,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - target and selection are different, #119769', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -118,9 +118,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - multiple cells in a selection', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -139,9 +139,9 @@ suite('CellOperations', () => { test('Copy/duplicate cells - move with folding ranges', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -168,9 +168,9 @@ suite('CellOperations', () => { test('Join cell with below - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -196,9 +196,9 @@ suite('CellOperations', () => { test('Join cell with above - single cell', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], ['var c = 3;', 'javascript', CellKind.Code, [], {}] ], @@ -445,4 +445,3 @@ suite('CellOperations', () => { }); }); }); - diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts index c881379f4f70..48b600815506 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/test/notebookClipboard.test.ts @@ -38,9 +38,9 @@ suite('Notebook Clipboard', () => { test('Cut multiple selected cells', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } }); @@ -59,12 +59,12 @@ suite('Notebook Clipboard', () => { test('Cut should take folding info into account', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], - ['var c = 3', 'javascript', CellKind.Markdown, [], {}], - ['# header d', 'markdown', CellKind.Markdown, [], {}], + ['var c = 3', 'javascript', CellKind.Markup, [], {}], + ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], async (editor, accessor) => { @@ -91,12 +91,12 @@ suite('Notebook Clipboard', () => { test('Copy should take folding info into account', async function () { await withTestNotebook( [ - ['# header a', 'markdown', CellKind.Markdown, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], ['var b = 1;', 'javascript', CellKind.Code, [], {}], - ['# header b', 'markdown', CellKind.Markdown, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], ['var b = 2;', 'javascript', CellKind.Code, [], {}], - ['var c = 3', 'javascript', CellKind.Markdown, [], {}], - ['# header d', 'markdown', CellKind.Markdown, [], {}], + ['var c = 3', 'javascript', CellKind.Markup, [], {}], + ['# header d', 'markdown', CellKind.Markup, [], {}], ['var e = 4;', 'javascript', CellKind.Code, [], {}], ], async (editor, accessor) => { @@ -129,9 +129,9 @@ suite('Notebook Clipboard', () => { test('#119773, cut last item should not focus on the top first cell', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { override setToCopy() { } }); @@ -148,9 +148,9 @@ suite('Notebook Clipboard', () => { test('#119771, undo paste should restore selections', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { @@ -183,9 +183,9 @@ suite('Notebook Clipboard', () => { test('copy cell from ui still works if the target cell is not part of a selection', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { let _toCopy: NotebookCellTextModel[] = []; @@ -213,10 +213,10 @@ suite('Notebook Clipboard', () => { test('cut cell from ui still works if the target cell is not part of a selection', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 3', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { @@ -255,10 +255,10 @@ suite('Notebook Clipboard', () => { test('cut focus cell still works if the focus is not part of any selection', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 3', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { @@ -280,10 +280,10 @@ suite('Notebook Clipboard', () => { test('cut focus cell still works if the focus is not part of any selection 2', async () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 1', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 2', 'markdown', CellKind.Markdown, [], {}], - ['paragraph 3', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 3', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { accessor.stub(INotebookService, new class extends mock() { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 2d9af9784ce9..dfb33a2cc08e 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -18,21 +18,27 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, ICellEditOperation, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellExecutionState, TransientCellMetadata, TransientDocumentMetadata, SelectionStateType, ICellReplaceEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorsOrder, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { Iterable } from 'vs/base/common/iterator'; +import { flatten, maxIndex, minIndex } from 'vs/base/common/arrays'; +import { Codicon } from 'vs/base/common/codicons'; + +// Kernel Command +export const SELECT_KERNEL_ID = 'notebook.selectKernel'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -56,6 +62,8 @@ const DELETE_CELL_COMMAND_ID = 'notebook.cell.delete'; const CANCEL_CELL_COMMAND_ID = 'notebook.cell.cancelExecution'; const EXECUTE_CELL_SELECT_BELOW = 'notebook.cell.executeAndSelectBelow'; const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; +const EXECUTE_CELL_AND_BELOW = 'notebook.cell.executeCellAndBelow'; +const EXECUTE_CELLS_ABOVE = 'notebook.cell.executeCellsAbove'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; const CENTER_ACTIVE_CELL = 'notebook.centerActiveCell'; @@ -89,7 +97,7 @@ export interface INotebookActionContext { readonly cell?: ICellViewModel; readonly notebookEditor: IActiveNotebookEditor; readonly ui?: boolean; - readonly selectedCells?: ICellViewModel[]; + readonly selectedCells?: readonly ICellViewModel[]; } export interface INotebookCellActionContext extends INotebookActionContext { @@ -175,11 +183,11 @@ export abstract class NotebookAction extends Action2 { super(desc); } - async run(accessor: ServicesAccessor, context?: any): Promise { + async run(accessor: ServicesAccessor, context?: any, ...additionalArgs: any[]): Promise { const isFromUI = !!context; const from = isFromUI ? (this.isNotebookActionContext(context) ? 'notebookToolbar' : 'editorToolbar') : undefined; if (!this.isNotebookActionContext(context)) { - context = this.getEditorContextFromArgsOrActive(accessor, context); + context = this.getEditorContextFromArgsOrActive(accessor, context, ...additionalArgs); if (!context) { return; } @@ -190,7 +198,7 @@ export abstract class NotebookAction extends Action2 { telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: from }); } - this.runWithContext(accessor, context); + return this.runWithContext(accessor, context); } abstract runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise; @@ -199,11 +207,78 @@ export abstract class NotebookAction extends Action2 { return !!context && !!(context as INotebookActionContext).notebookEditor; } - protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any): INotebookActionContext | undefined { + protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any, ...additionalArgs: any[]): INotebookActionContext | undefined { return getContextFromActiveEditor(accessor.get(IEditorService)); } } +// todo@rebornix, replace NotebookAction with this +export abstract class NotebookMultiCellAction extends Action2 { + constructor(desc: IAction2Options) { + if (desc.f1 !== false) { + desc.f1 = false; + const f1Menu = { + id: MenuId.CommandPalette, + when: NOTEBOOK_IS_ACTIVE_EDITOR + }; + + if (!desc.menu) { + desc.menu = []; + } else if (!Array.isArray(desc.menu)) { + desc.menu = [desc.menu]; + } + + desc.menu = [ + ...desc.menu, + f1Menu + ]; + } + + desc.category = NOTEBOOK_ACTIONS_CATEGORY; + + super(desc); + } + + abstract parseArgs(accessor: ServicesAccessor, ...args: any[]): T | undefined; + abstract runWithContext(accessor: ServicesAccessor, context: T): Promise; + + protected isNotebookActionContext(context?: unknown): context is INotebookActionContext { + return !!context && !!(context as INotebookActionContext).notebookEditor; + } + private isEditorContext(context?: unknown): boolean { + return !!context && (context as IEditorCommandsContext).groupId !== undefined; + } + protected getEditorFromArgsOrActivePane(accessor: ServicesAccessor, context?: UriComponents): IActiveNotebookEditor | undefined { + const editorFromUri = getContextFromUri(accessor, context)?.notebookEditor; + + if (editorFromUri) { + return editorFromUri; + } + + const editor = getNotebookEditorFromEditorPane(accessor.get(IEditorService).activeEditorPane); + if (!editor || !editor.hasModel()) { + return; + } + + return editor; + } + + async run(accessor: ServicesAccessor, ...additionalArgs: any[]): Promise { + const context = additionalArgs[0]; + const isFromCellToolbar = this.isNotebookActionContext(context); + const isFromEditorToolbar = this.isEditorContext(context); + const from = isFromCellToolbar ? 'cellToolbar' : (isFromEditorToolbar ? 'editorToolbar' : 'other'); + const parsedArgs = this.parseArgs(accessor, ...additionalArgs); + if (!parsedArgs) { + return; + } + + const telemetryService = accessor.get(ITelemetryService); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: from }); + return this.runWithContext(accessor, parsedArgs); + } +} + export abstract class NotebookCellAction extends NotebookAction { protected isCellActionContext(context?: unknown): context is INotebookCellActionContext { return !!context && !!(context as INotebookCellActionContext).notebookEditor && !!(context as INotebookCellActionContext).cell; @@ -236,19 +311,195 @@ export abstract class NotebookCellAction extends abstract override runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } -const executeCellCondition = ContextKeyExpr.or( - ContextKeyExpr.and( - ContextKeyExpr.or( - ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'idle'), - ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'succeeded'), - ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'failed'), - ), - ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0)), - NOTEBOOK_CELL_TYPE.isEqualTo('markdown')); +const executeCellCondition = ContextKeyExpr.and( + NOTEBOOK_CELL_TYPE.isEqualTo('code'), + ContextKeyExpr.or( + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'idle'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'succeeded'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'failed'), + ), + ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0)); const executeNotebookCondition = ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0); -registerAction2(class ExecuteCell extends NotebookCellAction { +interface IMultiCellArgs { + ranges: ICellRange[]; + document?: URI; +} + +function isMultiCellArgs(arg: unknown): arg is IMultiCellArgs { + if (arg === undefined) { + return false; + } + const ranges = (arg as IMultiCellArgs).ranges; + if (!ranges) { + return false; + } + + if (!Array.isArray(ranges) || ranges.some(range => !isICellRange(range))) { + return false; + } + + if ((arg as IMultiCellArgs).document) { + const uri = URI.revive((arg as IMultiCellArgs).document); + + if (!uri) { + return false; + } + } + + return true; +} + +function isNotebookActionContext(context?: unknown): context is INotebookActionContext { + return !!context && !!(context as INotebookActionContext).notebookEditor; +} + +function getEditorFromArgsOrActivePane(accessor: ServicesAccessor, context?: UriComponents): IActiveNotebookEditor | undefined { + const editorFromUri = getContextFromUri(accessor, context)?.notebookEditor; + + if (editorFromUri) { + return editorFromUri; + } + + const editor = getNotebookEditorFromEditorPane(accessor.get(IEditorService).activeEditorPane); + if (!editor || !editor.hasModel()) { + return; + } + + return editor; +} + +function parseMultiCellExecutionArgs(accessor: ServicesAccessor, ...args: any[]) { + const firstArg = args[0]; + if (isNotebookActionContext(firstArg)) { + // from UI + return firstArg; + } + + // then it's from keybindings or commands + // todo@rebornix assertType + if (isMultiCellArgs(firstArg)) { + const editor = getEditorFromArgsOrActivePane(accessor, firstArg.document); + if (!editor) { + return; + } + + const ranges = firstArg.ranges; + const selectedCells = flatten(ranges.map(range => editor.viewModel.getCells(range).slice(0))); + return { + notebookEditor: editor, + selectedCells + }; + } + + // handle legacy arguments + if (isICellRange(firstArg)) { + // cellRange, document + const secondArg = args[1]; + const editor = getEditorFromArgsOrActivePane(accessor, secondArg); + if (!editor) { + return; + } + + return { + notebookEditor: editor, + selectedCells: editor.viewModel.getCells(firstArg) + }; + } + + // let's just execute the active cell + const context = getContextFromActiveEditor(accessor.get(IEditorService)); + return context; +} + +registerAction2(class ExecuteAboveCells extends NotebookMultiCellAction { + constructor() { + super({ + id: EXECUTE_CELLS_ABOVE, + precondition: executeCellCondition, + title: localize('notebookActions.executeAbove', "Execute Above Cells"), + menu: [ + { + id: MenuId.NotebookCellExecute, + when: executeCellCondition + }, + { + id: MenuId.NotebookCellTitle, + group: 'inline', + when: ContextKeyExpr.and( + executeCellCondition, + ContextKeyExpr.equals('config.notebook.consolidatedRunButton', false)) + } + ], + icon: icons.executeAboveIcon + }); + } + + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + let endCellIdx: number | undefined = undefined; + if (context.ui && context.cell) { + endCellIdx = context.notebookEditor.viewModel.getCellIndex(context.cell); + } else if (context.selectedCells) { + endCellIdx = maxIndex(context.selectedCells, cell => context.notebookEditor.viewModel.getCellIndex(cell)); + } + + if (typeof endCellIdx === 'number') { + const range = { start: 0, end: endCellIdx }; + const cells = context.notebookEditor.viewModel.getCells(range); + context.notebookEditor.executeNotebookCells(cells); + } + } +}); + +registerAction2(class ExecuteCellAndBelow extends NotebookMultiCellAction { + constructor() { + super({ + id: EXECUTE_CELL_AND_BELOW, + precondition: executeCellCondition, + title: localize('notebookActions.executeBelow', "Execute Cell and Below"), + menu: [ + { + id: MenuId.NotebookCellExecute, + when: executeCellCondition, + }, + { + id: MenuId.NotebookCellTitle, + group: 'inline', + when: ContextKeyExpr.and( + executeCellCondition, + ContextKeyExpr.equals('config.notebook.consolidatedRunButton', false)) + } + ], + icon: icons.executeBelowIcon + }); + } + + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + let startCellIdx: number | undefined = undefined; + if (context.ui && context.cell) { + startCellIdx = context.notebookEditor.viewModel.getCellIndex(context.cell); + } else if (context.selectedCells) { + startCellIdx = minIndex(context.selectedCells, cell => context.notebookEditor.viewModel.getCellIndex(cell)); + } + + if (typeof startCellIdx === 'number') { + const range = { start: startCellIdx, end: context.notebookEditor.viewModel.viewCells.length }; + const cells = context.notebookEditor.viewModel.getCells(range); + context.notebookEditor.executeNotebookCells(cells); + } + } +}); + +registerAction2(class ExecuteCell extends NotebookMultiCellAction { constructor() { super({ id: EXECUTE_CELL_COMMAND_ID, @@ -271,25 +522,35 @@ registerAction2(class ExecuteCell extends NotebookCellAction { description: localize('notebookActions.execute', "Execute Cell"), args: [ { - name: 'range', - description: 'The cell range', + name: 'options', + description: 'The cell range options', schema: { 'type': 'object', - 'required': ['start', 'end'], + 'required': ['ranges'], 'properties': { - 'start': { - 'type': 'number' + 'ranges': { + 'type': 'array', + items: [ + { + 'type': 'object', + 'required': ['start', 'end'], + 'properties': { + 'start': { + 'type': 'number' + }, + 'end': { + 'type': 'number' + } + } + } + ] }, - 'end': { - 'type': 'number' + 'document': { + 'type': 'object', + 'description': 'The document uri', } } } - }, - { - name: 'uri', - description: 'The document uri', - constraint: URI } ] }, @@ -297,47 +558,11 @@ registerAction2(class ExecuteCell extends NotebookCellAction { }); } - override getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { - if (!context) { - return; - } - - if (typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { - throw new Error(`The first argument '${context}' is not a valid CellRange`); - } - - if (additionalArgs.length && additionalArgs[0]) { - const uri = URI.revive(additionalArgs[0]); - - if (!uri) { - throw new Error(`The second argument '${uri}' is not a valid Uri`); - } - - const widget = getWidgetFromUri(accessor, uri); - if (widget) { - return { - notebookEditor: widget, - cell: widget.viewModel.cellAt(context.start)! - }; - } else { - throw new Error(`There is no editor opened for resource ${uri}`); - } - } - - const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); - - if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.length) { - return; - } - - // TODO@rebornix, support multiple cells - return { - notebookEditor: activeEditorContext.notebookEditor, - cell: activeEditorContext.notebookEditor.viewModel.cellAt(context.start)! - }; + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { return runCell(accessor, context); } }); @@ -347,7 +572,7 @@ const cellCancelCondition = ContextKeyExpr.or( ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'pending'), ); -registerAction2(class CancelExecuteCell extends NotebookCellAction { +registerAction2(class CancelExecuteCell extends NotebookMultiCellAction { constructor() { super({ id: CANCEL_CELL_COMMAND_ID, @@ -363,65 +588,51 @@ registerAction2(class CancelExecuteCell extends NotebookCellAction { description: localize('notebookActions.cancel', "Stop Cell Execution"), args: [ { - name: 'range', - description: 'The cell range', + name: 'options', + description: 'The cell range options', schema: { 'type': 'object', - 'required': ['start', 'end'], + 'required': ['ranges'], 'properties': { - 'start': { - 'type': 'number' + 'ranges': { + 'type': 'array', + items: [ + { + 'type': 'object', + 'required': ['start', 'end'], + 'properties': { + 'start': { + 'type': 'number' + }, + 'end': { + 'type': 'number' + } + } + } + ] }, - 'end': { - 'type': 'number' + 'document': { + 'type': 'object', + 'description': 'The document uri', } } } - }, - { - name: 'uri', - description: 'The document uri', - constraint: URI } ] }, }); } - override getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { - if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { - return; - } - - if (additionalArgs.length && additionalArgs[0]) { - const uri = URI.revive(additionalArgs[0]); - - if (uri) { - const widget = getWidgetFromUri(accessor, uri); - if (widget) { - return { - notebookEditor: widget, - cell: widget.viewModel.cellAt(context.start)! - }; - } - } - } - - const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); - - if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.length) { - return; - } - - // TODO@rebornix, support multiple cells - return { - notebookEditor: activeEditorContext.notebookEditor, - cell: activeEditorContext.notebookEditor.viewModel.cellAt(context.start)! - }; + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - return context.notebookEditor.cancelNotebookCells(Iterable.single(context.cell)); + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + if (context.ui && context.cell) { + return context.notebookEditor.cancelNotebookCells(Iterable.single(context.cell)); + } else if (context.selectedCells) { + return context.notebookEditor.cancelNotebookCells(context.selectedCells); + } } }); @@ -448,7 +659,7 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { constructor() { super({ id: EXECUTE_CELL_SELECT_BELOW, - precondition: executeCellCondition, + precondition: ContextKeyExpr.or(executeCellCondition, NOTEBOOK_CELL_TYPE.isEqualTo('markup')), title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"), keybinding: { when: NOTEBOOK_CELL_LIST_FOCUSED, @@ -464,20 +675,33 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { return; } - const executionP = runCell(accessor, context); - - // Try to select below, fall back on inserting - const nextCell = context.notebookEditor.viewModel.cellAt(idx + 1); - if (nextCell) { - context.notebookEditor.focusNotebookCell(nextCell, 'container'); + if (context.cell.cellKind === CellKind.Markup) { + const nextCell = context.notebookEditor.viewModel.cellAt(idx + 1); + if (nextCell) { + context.notebookEditor.focusNotebookCell(nextCell, 'container'); + } else { + const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Markup, 'below'); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } + return; } else { - const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); - if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + const executionP = runCell(accessor, context); + + // Try to select below, fall back on inserting + const nextCell = context.notebookEditor.viewModel.cellAt(idx + 1); + if (nextCell) { + context.notebookEditor.focusNotebookCell(nextCell, 'container'); + } else { + const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } } - } - return executionP; + return executionP; + } } }); @@ -508,7 +732,7 @@ registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { } }); -registerAction2(class extends NotebookAction { +registerAction2(class RenderAllMarkdownCellsAction extends NotebookAction { constructor() { super({ id: RENDER_ALL_MARKDOWN_CELLS, @@ -521,28 +745,44 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookAction { +registerAction2(class ExecuteNotebookAction extends NotebookAction { constructor() { super({ id: EXECUTE_NOTEBOOK_COMMAND_ID, - title: localize('notebookActions.executeNotebook', "Execute Notebook (Run all cells)"), + title: localize('notebookActions.executeNotebook', "Run All"), icon: icons.executeAllIcon, description: { - description: localize('notebookActions.executeNotebook', "Execute Notebook (Run all cells)"), + description: localize('notebookActions.executeNotebook', "Run All"), args: [ { name: 'uri', - description: 'The document uri', - constraint: URI + description: 'The document uri' } ] }, - menu: { - id: MenuId.EditorTitle, - order: -1, - group: 'navigation', - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated())), - } + menu: [ + { + id: MenuId.EditorTitle, + order: -1, + group: 'navigation', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + executeNotebookCondition, + ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated()), + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ) + }, + { + id: MenuId.NotebookToolbar, + order: -1, + group: 'navigation/execute', + when: ContextKeyExpr.and( + executeNotebookCondition, + ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated()), + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ) + } + ] }); } @@ -569,7 +809,7 @@ registerAction2(class extends NotebookAction { function renderAllMarkdownCells(context: INotebookActionContext): void { context.notebookEditor.viewModel.viewCells.forEach(cell => { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { cell.updateEditState(CellEditState.Preview, 'renderAllMarkdownCells'); } }); @@ -579,10 +819,10 @@ registerAction2(class CancelNotebook extends NotebookAction { constructor() { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, - title: localize('notebookActions.cancelNotebook', "Stop Notebook Execution"), + title: localize('notebookActions.cancelNotebook', "Stop Execution"), icon: icons.stopIcon, description: { - description: localize('notebookActions.cancelNotebook', "Stop Notebook Execution"), + description: localize('notebookActions.cancelNotebook', "Stop Execution"), args: [ { name: 'uri', @@ -591,12 +831,29 @@ registerAction2(class CancelNotebook extends NotebookAction { } ] }, - menu: { - id: MenuId.EditorTitle, - order: -1, - group: 'navigation', - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL) - } + menu: [ + { + id: MenuId.EditorTitle, + order: -1, + group: 'navigation', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_HAS_RUNNING_CELL, + NOTEBOOK_INTERRUPTIBLE_KERNEL, + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ) + }, + { + id: MenuId.NotebookToolbar, + order: -1, + group: 'navigation/execute', + when: ContextKeyExpr.and( + NOTEBOOK_HAS_RUNNING_CELL, + NOTEBOOK_INTERRUPTIBLE_KERNEL, + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ) + } + ] }); } @@ -623,7 +880,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorContext, { when: NOTEBOOK_EDITOR_FOCUSED }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ChangeCellToCodeAction extends NotebookCellAction { constructor() { super({ id: CHANGE_CELL_TO_CODE_COMMAND_ID, @@ -633,10 +890,10 @@ registerAction2(class extends NotebookCellAction { primary: KeyCode.KEY_Y, weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_CELL_TYPE.isEqualTo('markup')), menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_TYPE.isEqualTo('markup')), group: CellOverflowToolbarGroups.Edit, } }); @@ -647,7 +904,7 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ChangeCellToMarkdownAction extends NotebookCellAction { constructor() { super({ id: CHANGE_CELL_TO_MARKDOWN_COMMAND_ID, @@ -667,14 +924,11 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - await changeCellToKind(CellKind.Markdown, context); + await changeCellToKind(CellKind.Markup, context); } }); -async function runCell(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - if (context.cell.metadata?.runState === NotebookCellExecutionState.Executing) { - return; - } +async function runCell(accessor: ServicesAccessor, context: INotebookActionContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); const group = editorGroupService.activeGroup; @@ -685,11 +939,13 @@ async function runCell(accessor: ServicesAccessor, context: INotebookCellActionC } } - if (context.cell.cellKind === CellKind.Markdown) { - context.notebookEditor.focusNotebookCell(context.cell, 'container'); - return; - } else { + if (context.ui && context.cell) { + if (context.cell.internalMetadata.runState === NotebookCellExecutionState.Executing) { + return; + } return context.notebookEditor.executeNotebookCells(Iterable.single(context.cell)); + } else if (context.selectedCells) { + return context.notebookEditor.executeNotebookCells(context.selectedCells); } } @@ -751,11 +1007,17 @@ abstract class InsertCellCommand extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + if (context.cell) { + context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + } else { + const focusRange = context.notebookEditor.getFocus(); + const next = focusRange.end - 1; + context.notebookEditor.insertNotebookCell(context.notebookEditor.viewModel.viewCells[next], this.kind, this.direction, undefined, true); + } } } -registerAction2(class extends InsertCellCommand { +registerAction2(class InsertCodeCellAboveAction extends InsertCellCommand { constructor() { super( { @@ -776,7 +1038,7 @@ registerAction2(class extends InsertCellCommand { } }); -registerAction2(class extends InsertCellCommand { +registerAction2(class InsertCodeCellBelowAction extends InsertCellCommand { constructor() { super( { @@ -797,7 +1059,7 @@ registerAction2(class extends InsertCellCommand { } }); -registerAction2(class extends NotebookAction { +registerAction2(class InsertCodeCellAtTopAction extends NotebookAction { constructor() { super( { @@ -822,7 +1084,7 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookAction { +registerAction2(class InsertMarkdownCellAtTopAction extends NotebookAction { constructor() { super( { @@ -840,7 +1102,7 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markdown, 'above', undefined, true); + const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markup, 'above', undefined, true); if (newCell) { context.notebookEditor.focusNotebookCell(newCell, 'editor'); } @@ -855,7 +1117,41 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { }, order: 0, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { + command: { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.menu.insertCode.minimalToolbar', "Add Code"), + icon: Codicon.add, + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: 0, + group: 'inline', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.equals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { + command: { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + icon: Codicon.add, + title: localize('notebookActions.menu.insertCode.ontoolbar', "Code"), + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: -5, + group: 'navigation/add', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { @@ -866,10 +1162,28 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { }, order: 0, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) }); -registerAction2(class extends InsertCellCommand { +MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { + command: { + id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.menu.insertCode.minimaltoolbar', "Add Code"), + icon: Codicon.add, + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: 0, + group: 'inline', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.equals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +registerAction2(class InsertMarkdownCellAboveAction extends InsertCellCommand { constructor() { super( { @@ -880,12 +1194,12 @@ registerAction2(class extends InsertCellCommand { order: 2 } }, - CellKind.Markdown, + CellKind.Markup, 'above'); } }); -registerAction2(class extends InsertCellCommand { +registerAction2(class InsertMarkdownCellBelowAction extends InsertCellCommand { constructor() { super( { @@ -896,7 +1210,7 @@ registerAction2(class extends InsertCellCommand { order: 3 } }, - CellKind.Markdown, + CellKind.Markup, 'below'); } }); @@ -909,7 +1223,26 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { }, order: 1, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) +}); + +MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { + command: { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + icon: Codicon.add, + title: localize('notebookActions.menu.insertMarkdown.ontoolbar', "Markdown"), + tooltip: localize('notebookActions.menu.insertMarkdown.tooltip', "Add Markdown Cell") + }, + order: -5, + group: 'navigation/add', + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { @@ -920,10 +1253,13 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { }, order: 1, group: 'inline', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarAlignment', 'left') + ) }); -registerAction2(class extends NotebookCellAction { +registerAction2(class EditCellAction extends NotebookCellAction { constructor() { super( { @@ -937,7 +1273,7 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and( - NOTEBOOK_CELL_TYPE.isEqualTo('markdown'), + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.toNegated(), NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.EditCell, @@ -952,7 +1288,14 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +const quitEditCondition = ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + InputFocusedContext, + EditorContextKeys.hoverVisible.toNegated(), + EditorContextKeys.hasNonEmptySelection.toNegated(), + EditorContextKeys.hasMultipleSelections.toNegated() +); +registerAction2(class QuitEditCellAction extends NotebookCellAction { constructor() { super( { @@ -961,29 +1304,35 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and( - NOTEBOOK_CELL_TYPE.isEqualTo('markdown'), + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EDITABLE), order: CellToolbarOrder.SaveCell, group: CELL_TITLE_CELL_GROUP_ID }, icon: icons.stopEditIcon, - keybinding: { - when: ContextKeyExpr.and( - NOTEBOOK_EDITOR_FOCUSED, - InputFocusedContext, - EditorContextKeys.hoverVisible.toNegated(), - EditorContextKeys.hasNonEmptySelection.toNegated(), - EditorContextKeys.hasMultipleSelections.toNegated() - ), - primary: KeyCode.Escape, - weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - 5 - }, + keybinding: [ + { + when: quitEditCondition, + primary: KeyCode.Escape, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - 5 + }, + { + when: ContextKeyExpr.and( + quitEditCondition, + NOTEBOOK_CELL_TYPE.isEqualTo('markup')), + primary: KeyMod.WinCtrl | KeyCode.Enter, + win: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter + }, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, + ] }); } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - if (context.cell.cellKind === CellKind.Markdown) { + if (context.cell.cellKind === CellKind.Markup) { context.cell.updateEditState(CellEditState.Preview, QUIT_EDIT_CELL_COMMAND_ID); } @@ -1054,7 +1403,7 @@ export function runDeleteAction(viewModel: NotebookViewModel, cell: ICellViewMod } } -registerAction2(class extends NotebookCellAction { +registerAction2(class DeleteCellAction extends NotebookCellAction { constructor() { super( { @@ -1086,17 +1435,23 @@ registerAction2(class extends NotebookCellAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class ClearCellOutputsAction extends NotebookCellAction { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, title: localize('clearCellOutputs', 'Clear Cell Outputs'), - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), executeNotebookCondition, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), - order: CellToolbarOrder.ClearCellOutput, - group: CELL_TITLE_OUTPUT_GROUP_ID - }, + menu: [ + { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), executeNotebookCondition, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON.toNegated()), + order: CellToolbarOrder.ClearCellOutput, + group: CELL_TITLE_OUTPUT_GROUP_ID + }, + { + id: MenuId.NotebookOutputToolbar, + when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE) + }, + ], keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), primary: KeyMod.Alt | KeyCode.Delete, @@ -1121,16 +1476,15 @@ registerAction2(class extends NotebookCellAction { editor.viewModel.notebookDocument.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined); - if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + if (context.cell.internalMetadata.runState !== NotebookCellExecutionState.Executing) { context.notebookEditor.viewModel.notebookDocument.applyEdits([{ - editType: CellEditType.Metadata, index, metadata: { - ...context.cell.metadata, - runState: NotebookCellExecutionState.Idle, - runStartTime: undefined, - runStartTimeAdjustment: undefined, - runEndTime: undefined, - executionOrder: undefined, - lastRunSuccess: undefined + editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { + runState: null, + runStartTime: null, + runStartTimeAdjustment: null, + runEndTime: null, + executionOrder: null, + lastRunSuccess: null } }], true, undefined, () => undefined, undefined); } @@ -1229,7 +1583,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction { let description: string; - if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) { + if (context.cell.cellKind === CellKind.Markup ? (languageId === 'markdown') : (languageId === context.cell.language)) { description = localize('languageDescription', "({0}) - Current Language", languageId); } else { description = localize('languageDescriptionConfigured', "({0})", languageId); @@ -1273,11 +1627,11 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction undefined, undefined); const clearExecutionMetadataEdits = editor.viewModel.notebookDocument.cells.map((cell, index) => { - if (cell.metadata && cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + if (cell.internalMetadata.runState !== NotebookCellExecutionState.Executing) { return { - editType: CellEditType.Metadata, index, metadata: { - ...cell.metadata, - runState: NotebookCellExecutionState.Idle, - runStartTime: undefined, - runStartTimeAdjustment: undefined, - runEndTime: undefined, - executionOrder: undefined + editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { + runState: null, + runStartTime: null, + runStartTimeAdjustment: null, + runEndTime: null, + executionOrder: null, + lastRunSuccess: null } }; } else { @@ -1356,7 +1725,7 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class CenterActiveCellAction extends NotebookCellAction { constructor() { super({ id: CENTER_ACTIVE_CELL, @@ -1400,7 +1769,7 @@ abstract class ChangeNotebookCellMetadataAction extends NotebookCellAction { abstract getMetadataDelta(): NotebookCellMetadata; } -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class CollapseCellInputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: COLLAPSE_CELL_INPUT_COMMAND_ID, @@ -1423,7 +1792,7 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class ExpandCellInputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: EXPAND_CELL_INPUT_COMMAND_ID, @@ -1446,7 +1815,7 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class CollapseCellOutputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: COLLAPSE_CELL_OUTPUT_COMMAND_ID, @@ -1469,7 +1838,7 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -registerAction2(class extends ChangeNotebookCellMetadataAction { +registerAction2(class ExpandCellOuputAction extends ChangeNotebookCellMetadataAction { constructor() { super({ id: EXPAND_CELL_OUTPUT_COMMAND_ID, @@ -1492,16 +1861,69 @@ registerAction2(class extends ChangeNotebookCellMetadataAction { } }); -// Revisit once we have a story for trusted workspace -CommandsRegistry.registerCommand('notebook.trust', (accessor, args) => { - const uri = URI.revive(args as UriComponents); - const notebookService = accessor.get(INotebookService); - - - const document = notebookService.listNotebookDocuments().find(document => document.uri.toString() === uri.toString()); +registerAction2(class NotebookConfigureLayoutAction extends Action2 { + constructor() { + super({ + id: 'workbench.notebook.layout.select', + title: localize('workbench.notebook.layout.select.label', "Select between Notebook Layouts"), + f1: true, + category: NOTEBOOK_ACTIONS_CATEGORY, + menu: [ + { + id: MenuId.EditorTitle, + group: 'notebookLayout', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true), + ContextKeyExpr.equals('config.notebook.experimental.openGettingStarted', true) + ), + order: 0 + }, + { + id: MenuId.NotebookToolbar, + group: 'notebookLayout', + when: ContextKeyExpr.and( + ContextKeyExpr.equals('config.notebook.globalToolbar', true), + ContextKeyExpr.equals('config.notebook.experimental.openGettingStarted', true) + ), + order: 0 + } + ] + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(ICommandService).executeCommand('workbench.action.openWalkthrough', { category: 'notebooks', step: 'notebookProfile' }, true); + } +}); - if (document) { - document.applyEdits([{ editType: CellEditType.DocumentMetadata, metadata: { ...document.metadata, ...{ trusted: true } } }], true, undefined, () => undefined, undefined, false); +registerAction2(class NotebookConfigureLayoutAction extends Action2 { + constructor() { + super({ + id: 'workbench.notebook.layout.configure', + title: localize('workbench.notebook.layout.configure.label', "Customize Notebook Layout"), + f1: true, + category: NOTEBOOK_ACTIONS_CATEGORY, + menu: [ + { + id: MenuId.EditorTitle, + group: 'notebookLayout', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ), + order: 1 + }, + { + id: MenuId.NotebookToolbar, + group: 'notebookLayout', + when: ContextKeyExpr.equals('config.notebook.globalToolbar', true), + order: 1 + } + ] + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IPreferencesService).openSettings(false, '@tag:notebookLayout'); } }); @@ -1512,7 +1934,7 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; }[] => { const notebookService = accessor.get(INotebookService); - const contentProviders = notebookService.getContributedNotebookProviders(); + const contentProviders = notebookService.getContributedNotebookTypes(); return contentProviders.map(provider => { const filenamePatterns = provider.selectors.map(selector => { if (typeof selector === 'string') { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index bf46cd65ad36..29364b4abc7a 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -8,14 +8,9 @@ import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; import * as strings from 'vs/base/common/strings'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, getNotebookEditorFromEditorPane, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, getNotebookEditorFromEditorPane, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Range } from 'vs/editor/common/core/range'; import { MATCHES_LIMIT } from 'vs/editor/contrib/find/findModel'; -import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { ICellModelDeltaDecorations, ICellModelDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget'; @@ -28,11 +23,12 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; -import { INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/findController'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { NLS_MATCHES_LOCATION, NLS_NO_RESULTS } from 'vs/editor/contrib/find/findWidget'; +import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; @@ -42,14 +38,10 @@ let MAX_MATCHES_COUNT_WIDTH = 69; export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; protected _findWidgetFocused: IContextKey; - private _findMatches: CellFindMatch[] = []; - protected _findMatchesStarts: PrefixSumComputer | null = null; - private _currentMatch: number = -1; - private _allMatchesDecorations: ICellModelDecorations[] = []; - private _currentMatchDecorations: ICellModelDecorations[] = []; private _showTimeout: number | null = null; private _hideTimeout: number | null = null; private _previousFocusElement?: HTMLElement; + private _findModel: FindModel; constructor( private readonly _notebookEditor: INotebookEditor, @@ -60,6 +52,8 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote ) { super(contextViewService, contextKeyService, themeService, new FindReplaceState(), true); + this._findModel = new FindModel(this._notebookEditor, this._state, this._configurationService); + DOM.append(this._notebookEditor.getDomNode(), this.getDomNode()); this._findWidgetFocused = KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); @@ -80,90 +74,43 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _onFindInputKeyDown(e: IKeyboardEvent): void { if (e.equals(KeyCode.Enter)) { - if (this._findMatches.length) { - this.find(false); - } else { - this.set(null, true); - } + this._findModel.find(false); e.preventDefault(); return; } else if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - if (this._findMatches.length) { - this.find(true); - } else { - this.set(null, true); - } + this.find(true); e.preventDefault(); return; } } protected onInputChanged(): boolean { - const val = this.inputValue; - const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; - const options: INotebookSearchOptions = { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), wordSeparators: wordSeparators }; - if (val) { - this._findMatches = this._notebookEditor.viewModel!.find(val, options).filter(match => match.matches.length > 0); - this.set(this._findMatches, false); - if (this._findMatches.length) { - return true; - } else { - return false; - } - } else { - this.set([], false); + this._state.change({ searchString: this.inputValue }, false); + // this._findModel.research(); + const findMatches = this._findModel.findMatches; + if (findMatches && findMatches.length) { + return true; } return false; } protected find(previous: boolean): void { - if (!this._findMatches.length) { - return; - } - - // let currCell; - if (!this._findMatchesStarts) { - this.set(this._findMatches, true); - } else { - // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); - // currCell = this._findMatches[currIndex.index].cell; - - const totalVal = this._findMatchesStarts.getTotalValue(); - const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; - this._currentMatch = nextVal; - } - - const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); - // const newFocusedCell = this._findMatches[nextIndex.index].cell; - this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); - this.revealCellRange(nextIndex.index, nextIndex.remainder); - - this._state.changeMatchInfo( - this._currentMatch, - this._findMatches.reduce((p, c) => p + c.matches.length, 0), - undefined - ); - - // if (currCell && currCell !== newFocusedCell && currCell.getEditState() === CellEditState.Editing && currCell.editStateSource === 'find') { - // currCell.updateEditState(CellEditState.Preview, 'find'); - // } - // this._updateMatchesCount(); + this._findModel.find(previous); } protected replaceOne() { - if (!this._findMatches.length) { + if (!this._findModel.findMatches.length) { return; } - if (!this._findMatchesStarts) { - this.set(this._findMatches, true); - } + this._findModel.ensureFindMatches(); - const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); - const cell = this._findMatches[nextIndex.index].cell; - const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder]; + if (this._findModel.currentMatch < 0) { + this._findModel.find(false); + } + const { cell, match } = this._findModel.getCurrentMatch(); this._progressBar.infinite().show(); this._notebookEditor.viewModel!.replaceOne(cell, match.range, this.replaceValue).then(() => { @@ -174,18 +121,11 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected replaceAll() { this._progressBar.infinite().show(); - this._notebookEditor.viewModel!.replaceAll(this._findMatches, this.replaceValue).then(() => { + this._notebookEditor.viewModel!.replaceAll(this._findModel.findMatches, this.replaceValue).then(() => { this._progressBar.stop(); }); } - private revealCellRange(cellIndex: number, matchIndex: number) { - this._findMatches[cellIndex].cell.updateEditState(CellEditState.Editing, 'find'); - this._notebookEditor.focusElement(this._findMatches[cellIndex].cell); - this._notebookEditor.setCellEditorSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); - this._notebookEditor.revealRangeInCenterIfOutsideViewportAsync(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); - } - protected findFirst(): void { } protected onFocusTrackerFocus() { @@ -207,99 +147,9 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected onFindInputFocusTrackerFocus(): void { } protected onFindInputFocusTrackerBlur(): void { } - private constructFindMatchesStarts() { - if (this._findMatches && this._findMatches.length) { - const values = new Uint32Array(this._findMatches.length); - for (let i = 0; i < this._findMatches.length; i++) { - values[i] = this._findMatches[i].matches.length; - } - - this._findMatchesStarts = new PrefixSumComputer(values); - } else { - this._findMatchesStarts = null; - } - } - - private set(cellFindMatches: CellFindMatch[] | null, autoStart: boolean): void { - if (!cellFindMatches || !cellFindMatches.length) { - this._findMatches = []; - this.setAllFindMatchesDecorations([]); - - this.constructFindMatchesStarts(); - this._currentMatch = -1; - this.clearCurrentFindMatchDecoration(); - return; - } - - // all matches - this._findMatches = cellFindMatches; - this.setAllFindMatchesDecorations(cellFindMatches || []); - - // current match - this.constructFindMatchesStarts(); - - if (autoStart) { - this._currentMatch = 0; - this.setCurrentFindMatchDecoration(0, 0); - } - - this._state.changeMatchInfo( - this._currentMatch, - this._findMatches.reduce((p, c) => p + c.matches.length, 0), - undefined - ); - } - - private setCurrentFindMatchDecoration(cellIndex: number, matchIndex: number) { - this._notebookEditor.changeModelDecorations(accessor => { - const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; - - const cell = this._findMatches[cellIndex].cell; - const match = this._findMatches[cellIndex].matches[matchIndex]; - const decorations: IModelDeltaDecoration[] = [ - { range: match.range, options: findMatchesOptions } - ]; - const deltaDecoration: ICellModelDeltaDecorations = { - ownerId: cell.handle, - decorations: decorations - }; - - this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, [deltaDecoration]); - }); - } - - private clearCurrentFindMatchDecoration() { - this._notebookEditor.changeModelDecorations(accessor => { - this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, []); - }); - } - - private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { - this._notebookEditor.changeModelDecorations((accessor) => { - - const findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; - - const deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { - const findMatches = cellFindMatch.matches; - - // Find matches - const newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); - for (let i = 0, len = findMatches.length; i < len; i++) { - newFindMatchesDecorations[i] = { - range: findMatches[i].range, - options: findMatchesOptions - }; - } - - return { ownerId: cellFindMatch.cell.handle, decorations: newFindMatchesDecorations }; - }); - - this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations); - }); - } - override show(initialInput?: string): void { super.show(initialInput); + this._state.change({ searchString: initialInput ?? '', isRevealed: true }, false); this._findInput.select(); if (this._showTimeout === null) { @@ -342,7 +192,8 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote override hide() { super.hide(); - this.set([], false); + this._state.change({ isRevealed: false }, false); + this._findModel.clear(); if (this._hideTimeout === null) { if (this._showTimeout !== null) { @@ -371,7 +222,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } override _updateMatchesCount(): void { - if (!this._findMatches) { + if (!this._findModel || !this._findModel.findMatches) { return; } @@ -390,7 +241,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote if (this._state.matchesCount >= MATCHES_LIMIT) { matchesCount += '+'; } - let matchesPosition: string = this._currentMatch < 0 ? '?' : String((this._currentMatch + 1)); + let matchesPosition: string = this._findModel.currentMatch < 0 ? '?' : String((this._findModel.currentMatch + 1)); label = strings.format(NLS_MATCHES_LOCATION, matchesPosition, matchesCount); } else { label = NLS_NO_RESULTS; @@ -412,18 +263,12 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote // TODO@rebornix, aria for `cell ${index}, line {line}` return localize('ariaSearchNoResultWithLineNumNoCurrentMatch', "{0} found for '{1}'", label, searchString); } - - clear() { - this._currentMatch = -1; - this._findMatches = []; - } - override dispose() { this._notebookEditor?.removeClassName(FIND_SHOW_TRANSITION); this._notebookEditor?.removeClassName(FIND_HIDE_TRANSITION); + this._findModel.dispose(); super.dispose(); } - } registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); @@ -481,7 +326,7 @@ registerAction2(class extends Action2 { } }); -StartFindAction.addImplementation(100, (accessor: ServicesAccessor, args: any) => { +StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); @@ -494,7 +339,7 @@ StartFindAction.addImplementation(100, (accessor: ServicesAccessor, args: any) = return true; }); -StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, args: any) => { +StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts new file mode 100644 index 000000000000..3e6d07b78223 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -0,0 +1,340 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { INotebookEditor, CellFindMatch, CellEditState, CellFindMatchWithIndex } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { ICellModelDeltaDecorations, ICellModelDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { FindReplaceState } from 'vs/editor/contrib/find/findState'; +import { CellKind, INotebookSearchOptions, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { findFirstInSorted } from 'vs/base/common/arrays'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + + +export class FindModel extends Disposable { + private _findMatches: CellFindMatch[] = []; + protected _findMatchesStarts: PrefixSumComputer | null = null; + private _currentMatch: number = -1; + private _allMatchesDecorations: ICellModelDecorations[] = []; + private _currentMatchDecorations: ICellModelDecorations[] = []; + private _modelDisposable = new DisposableStore(); + + get findMatches() { + return this._findMatches; + } + + get currentMatch() { + return this._currentMatch; + } + + constructor( + private readonly _notebookEditor: INotebookEditor, + private readonly _state: FindReplaceState, + @IConfigurationService private readonly _configurationService: IConfigurationService + ) { + super(); + + this._register(_state.onFindReplaceStateChange(e => { + if (e.searchString || (e.isRevealed && this._state.isRevealed)) { + this.research(); + } + + if (e.isRevealed && !this._state.isRevealed) { + this.clear(); + } + })); + + this._register(this._notebookEditor.onDidChangeModel(e => { + this._registerModelListener(e); + })); + + if (this._notebookEditor.hasModel()) { + this._registerModelListener(this._notebookEditor.textModel); + } + } + + ensureFindMatches() { + if (!this._findMatchesStarts) { + this.set(this._findMatches, true); + } + } + + getCurrentMatch() { + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + const cell = this._findMatches[nextIndex.index].cell; + const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder]; + + return { + cell, + match + }; + } + + find(previous: boolean) { + if (!this.findMatches.length) { + return; + } + + // let currCell; + if (!this._findMatchesStarts) { + this.set(this._findMatches, true); + } else { + // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + // currCell = this._findMatches[currIndex.index].cell; + const totalVal = this._findMatchesStarts.getTotalValue(); + if (this._currentMatch === -1) { + this._currentMatch = previous ? totalVal - 1 : 0; + } else { + const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; + this._currentMatch = nextVal; + } + } + + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + // const newFocusedCell = this._findMatches[nextIndex.index].cell; + this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); + this.revealCellRange(nextIndex.index, nextIndex.remainder); + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + } + + private revealCellRange(cellIndex: number, matchIndex: number) { + this._findMatches[cellIndex].cell.updateEditState(CellEditState.Editing, 'find'); + this._notebookEditor.focusElement(this._findMatches[cellIndex].cell); + this._notebookEditor.setCellEditorSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); + this._notebookEditor.revealRangeInCenterIfOutsideViewportAsync(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); + } + + private _registerModelListener(notebookTextModel?: NotebookTextModel) { + this._modelDisposable.clear(); + + if (notebookTextModel) { + this._modelDisposable.add(notebookTextModel.onDidChangeContent((e) => { + if (!e.rawEvents.some(event => event.kind === NotebookCellsChangeType.ChangeCellContent || event.kind === NotebookCellsChangeType.ModelChange)) { + return; + } + + this.research(); + })); + } + + this.research(); + } + + research() { + if (!this._state.isRevealed) { + this.set([], false); + return; + } + + const findMatches = this._getFindMatches(); + if (!findMatches) { + return; + } + + if (this._currentMatch === -1) { + // no active current match + this.set(findMatches, false); + return; + } + + const oldCurrIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + const oldCurrCell = this._findMatches[oldCurrIndex.index].cell; + const oldCurrMatchCellIndex = this._notebookEditor.viewModel!.getCellIndex(oldCurrCell); + + if (oldCurrMatchCellIndex < 0) { + // the cell containing the active match is deleted + const focusedCell = this._notebookEditor.viewModel!.viewCells[this._notebookEditor.viewModel!.getFocus().start]; + + if (!focusedCell) { + this.set(findMatches, false); + return; + } + + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= oldCurrMatchCellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + return; + } + + // the cell still exist + const cell = this._notebookEditor.viewModel!.viewCells[oldCurrMatchCellIndex]; + if (cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Preview) { + // find the nearest match above this cell + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= oldCurrMatchCellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + return; + } + + if ((cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Editing) || cell.cellKind === CellKind.Code) { + // check if there is monaco editor selection and find the first match, otherwise find the first match above current cell + // this._findMatches[cellIndex].matches[matchIndex].range + const currentMatchDecorationId = this._currentMatchDecorations.find(decoration => decoration.ownerId === cell.handle); + + if (currentMatchDecorationId) { + const currMatchRangeInEditor = (cell.editorAttached && currentMatchDecorationId.decorations[0] ? cell.getCellDecorationRange(currentMatchDecorationId.decorations[0]) : null) + ?? this._findMatches[oldCurrIndex.index].matches[oldCurrIndex.remainder].range; + + // not attached, just use the range + const matchAfterSelection = findFirstInSorted(findMatches, match => match.index >= oldCurrMatchCellIndex); + if (findMatches[matchAfterSelection].index > oldCurrMatchCellIndex) { + // there is no search result in curr cell anymore + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + } else { + // findMatches[matchAfterSelection].index === currMatchCellIndex + const cellMatch = findMatches[matchAfterSelection]; + const matchAfterOldSelection = findFirstInSorted(cellMatch.matches, match => Range.compareRangesUsingStarts(match.range, currMatchRangeInEditor) >= 0); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection) + matchAfterOldSelection); + } + } else { + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= oldCurrMatchCellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + } + + return; + } + + this.set(findMatches, false); + } + + private set(cellFindMatches: CellFindMatch[] | null, autoStart: boolean): void { + if (!cellFindMatches || !cellFindMatches.length) { + this._findMatches = []; + this.setAllFindMatchesDecorations([]); + + this.constructFindMatchesStarts(); + this._currentMatch = -1; + this.clearCurrentFindMatchDecoration(); + return; + } + + // all matches + this._findMatches = cellFindMatches; + this.setAllFindMatchesDecorations(cellFindMatches || []); + + // current match + this.constructFindMatchesStarts(); + + if (autoStart) { + this._currentMatch = 0; + this.setCurrentFindMatchDecoration(0, 0); + } + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + } + + private _getFindMatches(): CellFindMatchWithIndex[] | null { + const val = this._state.searchString; + const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; + + const options: INotebookSearchOptions = { regex: this._state.isRegex, wholeWord: this._state.wholeWord, caseSensitive: this._state.matchCase, wordSeparators: wordSeparators }; + if (!val) { + return null; + } + + const findMatches = this._notebookEditor.viewModel!.find(val, options).filter(match => match.matches.length > 0); + return findMatches; + } + + private _updateCurrentMatch(findMatches: CellFindMatchWithIndex[], currentMatchesPosition: number) { + this.set(findMatches, false); + this._currentMatch = currentMatchesPosition; + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + } + + private _matchesCountBeforeIndex(findMatches: CellFindMatchWithIndex[], index: number) { + let prevMatchesCount = 0; + for (let i = 0; i < index; i++) { + prevMatchesCount += findMatches[i].matches.length; + } + + return prevMatchesCount; + } + + private constructFindMatchesStarts() { + if (this._findMatches && this._findMatches.length) { + const values = new Uint32Array(this._findMatches.length); + for (let i = 0; i < this._findMatches.length; i++) { + values[i] = this._findMatches[i].matches.length; + } + + this._findMatchesStarts = new PrefixSumComputer(values); + } else { + this._findMatchesStarts = null; + } + } + + private setCurrentFindMatchDecoration(cellIndex: number, matchIndex: number) { + this._notebookEditor.changeModelDecorations(accessor => { + const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; + + const cell = this._findMatches[cellIndex].cell; + const match = this._findMatches[cellIndex].matches[matchIndex]; + const decorations: IModelDeltaDecoration[] = [ + { range: match.range, options: findMatchesOptions } + ]; + const deltaDecoration: ICellModelDeltaDecorations = { + ownerId: cell.handle, + decorations: decorations + }; + + this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, [deltaDecoration]); + }); + } + + private clearCurrentFindMatchDecoration() { + this._notebookEditor.changeModelDecorations(accessor => { + this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, []); + }); + } + + private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { + this._notebookEditor.changeModelDecorations((accessor) => { + + const findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; + + const deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { + const findMatches = cellFindMatch.matches; + + // Find matches + const newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); + for (let i = 0, len = findMatches.length; i < len; i++) { + newFindMatchesDecorations[i] = { + range: findMatches[i].range, + options: findMatchesOptions + }; + } + + return { ownerId: cellFindMatch.cell.handle, decorations: newFindMatchesDecorations }; + }); + + this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations); + }); + } + + + clear() { + this.set([], false); + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts new file mode 100644 index 000000000000..b70d58c31dbb --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/find/test/find.test.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextBuffer, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; +import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { FindReplaceState } from 'vs/editor/contrib/find/findState'; +import { IConfigurationService, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { IActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellModelDecorations, ICellModelDeltaDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellEditType, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; + +suite('Notebook Find', () => { + const configurationValue: IConfigurationValue = { + value: USUAL_WORD_SEPARATORS + }; + const configurationService = new class extends TestConfigurationService { + override inspect() { + return configurationValue; + } + }(); + + const setupEditorForTest = (editor: IActiveNotebookEditor) => { + editor.changeModelDecorations = (callback) => { + return callback({ + deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]) => { + const ret: ICellModelDecorations[] = []; + newDecorations.forEach(dec => { + const cell = editor.viewModel.viewCells.find(cell => cell.handle === dec.ownerId); + const decorations = cell?.deltaModelDecorations([], dec.decorations) ?? []; + + if (decorations.length > 0) { + ret.push({ ownerId: dec.ownerId, decorations: decorations }); + } + }); + + return ret; + } + }); + }; + }; + + test('Update find matches basics', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + assert.strictEqual(model.findMatches.length, 2); + assert.strictEqual(model.currentMatch, -1); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + model.find(false); + assert.strictEqual(model.currentMatch, 1); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + + assert.strictEqual(editor.textModel.length, 3); + + editor.textModel.applyEdits([{ + editType: CellEditType.Replace, index: 3, count: 0, cells: [ + new TestCell(editor.viewModel.viewType, 3, '# next paragraph 1', 'markdown', CellKind.Code, [], accessor.get(IModeService)), + ] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(editor.textModel.length, 4); + assert.strictEqual(model.findMatches.length, 3); + assert.strictEqual(model.currentMatch, 0); + }); + }); + + test('Update find matches basics 2', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + setupEditorForTest(editor); + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + // find matches is not necessarily find results + assert.strictEqual(model.findMatches.length, 4); + assert.strictEqual(model.currentMatch, -1); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + model.find(false); + assert.strictEqual(model.currentMatch, 1); + model.find(false); + assert.strictEqual(model.currentMatch, 2); + + editor.textModel.applyEdits([{ + editType: CellEditType.Replace, index: 2, count: 1, cells: [] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(model.findMatches.length, 3); + + assert.strictEqual(model.currentMatch, 2); + model.find(true); + assert.strictEqual(model.currentMatch, 1); + model.find(false); + assert.strictEqual(model.currentMatch, 2); + model.find(false); + assert.strictEqual(model.currentMatch, 3); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + }); + }); + + test('Update find matches basics 3', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + setupEditorForTest(editor); + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + // find matches is not necessarily find results + assert.strictEqual(model.findMatches.length, 4); + assert.strictEqual(model.currentMatch, -1); + model.find(true); + assert.strictEqual(model.currentMatch, 4); + + editor.textModel.applyEdits([{ + editType: CellEditType.Replace, index: 2, count: 1, cells: [] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(model.findMatches.length, 3); + assert.strictEqual(model.currentMatch, 3); + model.find(false); + assert.strictEqual(model.currentMatch, 0); + model.find(true); + assert.strictEqual(model.currentMatch, 3); + model.find(true); + assert.strictEqual(model.currentMatch, 2); + }); + }); + + test('Update find matches, #112748', async function () { + await withTestNotebook( + [ + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.1', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.2', 'markdown', CellKind.Markup, [], {}], + ['paragraph 1.3', 'markdown', CellKind.Markup, [], {}], + ['paragraph 2', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, accessor) => { + setupEditorForTest(editor); + accessor.stub(IConfigurationService, configurationService); + const state = new FindReplaceState(); + const model = new FindModel(editor, state, accessor.get(IConfigurationService)); + state.change({ isRevealed: true }, true); + state.change({ searchString: '1' }, true); + // find matches is not necessarily find results + assert.strictEqual(model.findMatches.length, 4); + assert.strictEqual(model.currentMatch, -1); + model.find(false); + model.find(false); + model.find(false); + assert.strictEqual(model.currentMatch, 2); + (editor.viewModel.viewCells[1].textBuffer as ITextBuffer).applyEdits([ + new ValidAnnotatedEditOperation(null, new Range(1, 1, 1, 14), '', false, false, false) + ], false, true); + // cell content updates, recompute + model.research(); + assert.strictEqual(model.currentMatch, 1); + }); + }); +}); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 04f01b0575d0..73e57fe89215 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -38,8 +38,8 @@ export class FoldingController extends Disposable implements INotebookEditorCont return; } - this._localStore.add(this._notebookEditor.viewModel.eventDispatcher.onDidChangeCellState(e => { - if (e.source.editStateChanged && e.cell.cellKind === CellKind.Markdown) { + this._localStore.add(this._notebookEditor.viewModel.viewContext.eventDispatcher.onDidChangeCellState(e => { + if (e.source.editStateChanged && e.cell.cellKind === CellKind.Markup) { this._foldingModel?.recompute(); // this._updateEditorFoldingRanges(); } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts index 7457d5f02479..db6ed0d83c15 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts @@ -16,13 +16,13 @@ suite('Notebook Folding', () => { test('Folding based on markdown cells', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -43,13 +43,13 @@ suite('Notebook Folding', () => { test('Top level header in a cell wins', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1\n# header3', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1\n# header3', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -75,13 +75,13 @@ suite('Notebook Folding', () => { test('Folding', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -97,13 +97,13 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['## header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -120,13 +120,13 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -145,13 +145,13 @@ suite('Notebook Folding', () => { test('Nested Folding', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -200,18 +200,18 @@ suite('Notebook Folding', () => { test('Folding Memento', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -229,18 +229,18 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -262,18 +262,18 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -297,18 +297,18 @@ suite('Notebook Folding', () => { test('View Index', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; @@ -334,18 +334,18 @@ suite('Notebook Folding', () => { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], - ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], - ['body 2', 'markdown', CellKind.Markdown, [], {}], - ['body 3', 'markdown', CellKind.Markdown, [], {}], - ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], - ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markup, [], {}], + ['body 2', 'markdown', CellKind.Markup, [], {}], + ['body 3', 'markdown', CellKind.Markup, [], {}], + ['## header 2.2', 'markdown', CellKind.Markup, [], {}], + ['var e = 7;', 'markdown', CellKind.Markup, [], {}], ], (editor) => { const viewModel = editor.viewModel; diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts new file mode 100644 index 000000000000..da940be17a7e --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Memento } from 'vs/workbench/common/memento'; +import { HAS_OPENED_NOTEBOOK } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +const hasOpenedNotebookKey = 'hasOpenedNotebook'; +const hasShownGettingStartedKey = 'hasShownNotebookGettingStarted'; + +const showGettingStartedSetting = 'notebook.experimental.openGettingStarted'; + +/** + * Sets a context key when a notebook has ever been opened by the user + */ +export class NotebookGettingStarted extends Disposable implements IWorkbenchContribution { + + constructor( + @IEditorService _editorService: IEditorService, + @IStorageService _storageService: IStorageService, + @IContextKeyService _contextKeyService: IContextKeyService, + @ICommandService _commandService: ICommandService, + @IConfigurationService _configurationService: IConfigurationService, + ) { + super(); + + const hasOpenedNotebook = HAS_OPENED_NOTEBOOK.bindTo(_contextKeyService); + const memento = new Memento('notebookGettingStarted2', _storageService); + const storedValue = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + if (storedValue[hasOpenedNotebookKey]) { + hasOpenedNotebook.set(true); + } + + const needToShowGettingStarted = _configurationService.getValue(showGettingStartedSetting) && !storedValue[hasShownGettingStartedKey]; + if (!storedValue[hasOpenedNotebookKey] || needToShowGettingStarted) { + const onDidOpenNotebook = () => { + hasOpenedNotebook.set(true); + storedValue[hasOpenedNotebookKey] = true; + + if (needToShowGettingStarted) { + _commandService.executeCommand('workbench.action.openWalkthrough', { category: 'notebooks', step: 'notebookProfile' }, true); + storedValue[hasShownGettingStartedKey] = true; + } + + memento.saveMemento(); + }; + + if (_editorService.activeEditor?.typeId === NotebookEditorInput.ID) { + // active editor is notebook + onDidOpenNotebook(); + return; + } + + const listener = this._register(_editorService.onDidActiveEditorChange(() => { + if (_editorService.activeEditor?.typeId === NotebookEditorInput.ID) { + listener.dispose(); + onDidOpenNotebook(); + } + })); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookGettingStarted, LifecyclePhase.Restored); + +registerAction2(class NotebookClearNotebookLayoutAction extends Action2 { + constructor() { + super({ + id: 'workbench.notebook.layout.gettingStarted', + title: localize('workbench.notebook.layout.gettingStarted.label', "Reset notebook getting started"), + f1: true, + category: CATEGORIES.Developer, + }); + } + run(accessor: ServicesAccessor): void { + const storageService = accessor.get(IStorageService); + const memento = new Memento('notebookGettingStarted', storageService); + + const storedValue = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + storedValue[hasOpenedNotebookKey] = undefined; + memento.saveMemento(); + } +}); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts index b9cadaff9682..1448a578b402 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts @@ -8,7 +8,7 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { INotebookActionContext, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { CellToolbarLocKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const TOGGLE_CELL_TOOLBAR_POSITION = 'notebook.toggleCellToolbarPosition'; @@ -33,9 +33,9 @@ export class ToggleCellToolbarPositionAction extends Action2 { // from toolbar const viewType = editor.viewModel.viewType; const configurationService = accessor.get(IConfigurationService); - const toolbarPosition = configurationService.getValue(CellToolbarLocKey); + const toolbarPosition = configurationService.getValue(CellToolbarLocation); const newConfig = this.togglePosition(viewType, toolbarPosition); - await configurationService.updateValue(CellToolbarLocKey, newConfig); + await configurationService.updateValue(CellToolbarLocation, newConfig); } } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index 703fa826ea7d..e42a72f336fd 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -68,7 +68,7 @@ registerAction2(class extends NotebookCellAction { return; } - const newFocusMode = newCell.cellKind === CellKind.Markdown && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; + const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; editor.focusNotebookCell(newCell, newFocusMode); editor.cursorNavigationMode = true; } @@ -115,7 +115,7 @@ registerAction2(class extends NotebookCellAction { return; } - const newFocusMode = newCell.cellKind === CellKind.Markdown && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; + const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; editor.focusNotebookCell(newCell, newFocusMode); editor.cursorNavigationMode = true; } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index f4299895a2f2..0b74fe883264 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -396,7 +396,7 @@ export class NotebookCellOutline implements IOutline { for (let i = 0; i < viewModel.length; i++) { const cell = viewModel.viewCells[i]; - const isMarkdown = cell.cellKind === CellKind.Markdown; + const isMarkdown = cell.cellKind === CellKind.Markup; if (!isMarkdown && !includeCodeCells) { continue; } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts index f1e7fe845ba5..0a5f80bc2782 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/outline/test/notebookOutline.test.ts @@ -49,7 +49,7 @@ suite('Notebook Outline', function () { test('special characters in heading', async function () { await withNotebookOutline([ - ['# Hellรถ & Hรคllo', 'md', CellKind.Markdown] + ['# Hellรถ & Hรคllo', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); @@ -57,7 +57,7 @@ suite('Notebook Outline', function () { }); await withNotebookOutline([ - ['# bold', 'md', CellKind.Markdown] + ['# bold', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); @@ -67,7 +67,7 @@ suite('Notebook Outline', function () { test('Heading text defines entry label', async function () { return await withNotebookOutline([ - ['foo\n # h1', 'md', CellKind.Markdown] + ['foo\n # h1', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); @@ -77,7 +77,7 @@ suite('Notebook Outline', function () { test('Notebook outline ignores markdown headings #115200', async function () { await withNotebookOutline([ - ['## h2 \n# h1', 'md', CellKind.Markdown] + ['## h2 \n# h1', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); @@ -86,8 +86,8 @@ suite('Notebook Outline', function () { }); await withNotebookOutline([ - ['## h2', 'md', CellKind.Markdown], - ['# h1', 'md', CellKind.Markdown] + ['## h2', 'md', CellKind.Markup], + ['# h1', 'md', CellKind.Markup] ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts new file mode 100644 index 000000000000..339665e267eb --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { CellToolbarLocation, CompactView, ConsolidatedRunButton, FocusIndicator, GlobalToolbar, InsertToolbarLocation, ShowCellStatusBar, UndoRedoPerCell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export enum NotebookProfileType { + default = 'default', + jupyter = 'jupyter', + colab = 'colab' +} + +const profiles = { + [NotebookProfileType.default]: { + [FocusIndicator]: 'gutter', + [InsertToolbarLocation]: 'both', + [GlobalToolbar]: true, + [CellToolbarLocation]: { default: 'right' }, + [CompactView]: true, + [ShowCellStatusBar]: 'visible', + [ConsolidatedRunButton]: true, + [UndoRedoPerCell]: false + }, + [NotebookProfileType.jupyter]: { + [FocusIndicator]: 'gutter', + [InsertToolbarLocation]: 'notebookToolbar', + [GlobalToolbar]: true, + [CellToolbarLocation]: { default: 'left' }, + [CompactView]: true, + [ShowCellStatusBar]: 'visible', + [ConsolidatedRunButton]: false, + [UndoRedoPerCell]: true + }, + [NotebookProfileType.colab]: { + [FocusIndicator]: 'border', + [InsertToolbarLocation]: 'betweenCells', + [GlobalToolbar]: false, + [CellToolbarLocation]: { default: 'right' }, + [CompactView]: false, + [ShowCellStatusBar]: 'hidden', + [ConsolidatedRunButton]: true, + [UndoRedoPerCell]: false + } +}; + +async function applyProfile(configService: IConfigurationService, profile: Record): Promise { + const promises = []; + for (let settingKey in profile) { + promises.push(configService.updateValue(settingKey, profile[settingKey])); + } + + await Promise.all(promises); +} + +export interface ISetProfileArgs { + profile: NotebookProfileType; +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.setProfile', + title: localize('setProfileTitle', "Set Profile") + }); + } + + async run(accessor: ServicesAccessor, args: unknown): Promise { + if (!isSetProfileArgs(args)) { + return; + } + + const configService = accessor.get(IConfigurationService); + return applyProfile(configService, profiles[args.profile]); + } +}); + +function isSetProfileArgs(args: unknown): args is ISetProfileArgs { + const setProfileArgs = args as ISetProfileArgs; + return setProfileArgs.profile === NotebookProfileType.colab || + setProfileArgs.profile === NotebookProfileType.default || + setProfileArgs.profile === NotebookProfileType.jupyter; +} + +export class NotebookProfileContribution extends Disposable { + constructor(@IConfigurationService configService: IConfigurationService, @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService) { + super(); + + if (this.experimentService) { + this.experimentService.getTreatment('notebookprofile').then(treatment => { + if (treatment === undefined) { + return; + } else { + // check if settings are already modified + const focusIndicator = configService.getValue(FocusIndicator); + const insertToolbarPosition = configService.getValue(InsertToolbarLocation); + const globalToolbar = configService.getValue(GlobalToolbar); + // const cellToolbarLocation = configService.getValue(CellToolbarLocation); + const compactView = configService.getValue(CompactView); + const showCellStatusBar = configService.getValue(ShowCellStatusBar); + const consolidatedRunButton = configService.getValue(ConsolidatedRunButton); + if (focusIndicator === 'border' + && insertToolbarPosition === 'both' + && globalToolbar === false + // && cellToolbarLocation === undefined + && compactView === true + && showCellStatusBar === 'visible' + && consolidatedRunButton === true + ) { + applyProfile(configService, profiles[treatment] ?? profiles[NotebookProfileType.default]); + } + } + }); + } + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookProfileContribution, LifecyclePhase.Ready); + diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index cc3f20a71017..512f64673481 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -5,34 +5,56 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputButton, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { getNotebookEditorFromEditorPane, INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_ACTIONS_CATEGORY, SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { getNotebookEditorFromEditorPane, INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { combinedDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { INotebookKernel, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { HoverProviderRegistry } from 'vs/editor/common/modes'; +import { Schemas } from 'vs/base/common/network'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; registerAction2(class extends Action2 { constructor() { super({ - id: 'notebook.selectKernel', + id: SELECT_KERNEL_ID, category: NOTEBOOK_ACTIONS_CATEGORY, title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: selectKernelIcon, f1: true, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ), + group: 'navigation', + order: -10 + }, { + id: MenuId.NotebookToolbar, + when: ContextKeyExpr.and( + NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ), + group: 'status', + order: -10 + }], description: { description: nls.localize('notebookActions.selectKernel.args', "Notebook Kernel Args"), args: [ @@ -54,7 +76,6 @@ registerAction2(class extends Action2 { } ] }, - }); } @@ -144,15 +165,65 @@ registerAction2(class extends Action2 { } }); + +class ImplictKernelSelector implements IDisposable { + + readonly dispose: () => void; + + constructor( + notebook: NotebookTextModel, + suggested: INotebookKernel, + @INotebookKernelService notebookKernelService: INotebookKernelService, + @ILogService logService: ILogService + ) { + const disposables = new DisposableStore(); + this.dispose = disposables.dispose.bind(disposables); + + const selectKernel = () => { + disposables.clear(); + notebookKernelService.selectKernelForNotebook(suggested, notebook); + }; + + // IMPLICITLY select a suggested kernel when the notebook has been changed + // e.g change cell source, move cells, etc + disposables.add(notebook.onDidChangeContent(e => { + for (let event of e.rawEvents) { + switch (event.kind) { + case NotebookCellsChangeType.ChangeCellContent: + case NotebookCellsChangeType.ModelChange: + case NotebookCellsChangeType.Move: + case NotebookCellsChangeType.ChangeLanguage: + logService.trace('IMPLICIT kernel selection because of change event', event.kind); + selectKernel(); + break; + } + } + })); + + + // IMPLICITLY select a suggested kernel when users start to hover. This should + // be a strong enough hint that the user wants to interact with the notebook. Maybe + // add more triggers like goto-providers or completion-providers + disposables.add(HoverProviderRegistry.register({ scheme: Schemas.vscodeNotebookCell, pattern: notebook.uri.path }, { + provideHover() { + logService.trace('IMPLICIT kernel selection because of hover'); + selectKernel(); + return undefined; + } + })); + } +} + export class KernelStatus extends Disposable implements IWorkbenchContribution { private readonly _editorDisposables = this._register(new DisposableStore()); - private readonly _kernelInfoElement = this._register(new MutableDisposable()); + private readonly _kernelInfoElement = this._register(new DisposableStore()); constructor( @IEditorService private readonly _editorService: IEditorService, @IStatusbarService private readonly _statusbarService: IStatusbarService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @ILogService private readonly _logService: ILogService, ) { super(); this._register(this._editorService.onDidActiveEditorChange(() => this._updateStatusbar())); @@ -169,6 +240,12 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } const updateStatus = () => { + if (activeEditor.notebookOptions.getLayoutConfiguration().globalToolbar) { + // kernel info rendered in the notebook toolbar already + this._kernelInfoElement.clear(); + return; + } + const notebook = activeEditor.viewModel?.notebookDocument; if (notebook) { this._showKernelStatus(notebook); @@ -181,62 +258,68 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookKernelBinding(updateStatus)); this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookAffinity(updateStatus)); this._editorDisposables.add(activeEditor.onDidChangeModel(updateStatus)); + this._editorDisposables.add(activeEditor.notebookOptions.onDidChangeOptions(updateStatus)); updateStatus(); } - private _showKernelStatus(notebook: INotebookTextModel) { + private _showKernelStatus(notebook: NotebookTextModel) { + + this._kernelInfoElement.clear(); - let { selected, all } = this._notebookKernelService.getMatchingKernel(notebook); + let { selected, suggested, all } = this._notebookKernelService.getMatchingKernel(notebook); let isSuggested = false; if (all.length === 0) { // no kernel -> no status - this._kernelInfoElement.clear(); return; - } else if (selected || all.length === 1) { + } else if (selected || suggested) { // selected or single kernel - if (!selected) { - selected = all[0]; + let kernel = selected; + + if (!kernel) { + // proceed with suggested kernel - show UI and install handler that selects the kernel + // when non trivial interactions with the notebook happen. + kernel = suggested!; isSuggested = true; + this._kernelInfoElement.add(new ImplictKernelSelector(notebook, kernel, this._notebookKernelService, this._logService)); } - const text = `$(notebook-kernel-select) ${selected.label}`; - const tooltip = selected.description ?? selected.detail ?? selected.label; - const registration = this._statusbarService.addEntry( + const tooltip = kernel.description ?? kernel.detail ?? kernel.label; + this._kernelInfoElement.add(this._statusbarService.addEntry( { - text, - ariaLabel: selected.label, + name: nls.localize('notebook.info', "Notebook Kernel Info"), + text: `$(notebook-kernel-select) ${kernel.label}`, + ariaLabel: kernel.label, tooltip: isSuggested ? nls.localize('tooltop', "{0} (suggestion)", tooltip) : tooltip, - command: 'notebook.selectKernel', + command: SELECT_KERNEL_ID, }, 'notebook.selectKernel', - nls.localize('notebook.info', "Notebook Kernel Info"), StatusbarAlignment.RIGHT, - 1000 - ); - const listener = selected.onDidChange(() => this._showKernelStatus(notebook)); - this._kernelInfoElement.value = combinedDisposable(listener, registration); + 10 + )); + + this._kernelInfoElement.add(kernel.onDidChange(() => this._showKernelStatus(notebook))); + } else { // multiple kernels -> show selection hint - const registration = this._statusbarService.addEntry( + this._kernelInfoElement.add(this._statusbarService.addEntry( { + name: nls.localize('notebook.select', "Notebook Kernel Selection"), text: nls.localize('kernel.select.label', "Select Kernel"), ariaLabel: nls.localize('kernel.select.label', "Select Kernel"), - command: 'notebook.selectKernel', + command: SELECT_KERNEL_ID, backgroundColor: { id: 'statusBarItem.prominentBackground' } }, 'notebook.selectKernel', - nls.localize('notebook.select', "Notebook Kernel Selection"), StatusbarAlignment.RIGHT, - 1000 - ); - this._kernelInfoElement.value = registration; + 10 + )); } } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Restored); export class ActiveCellStatus extends Disposable implements IWorkbenchContribution { @@ -276,14 +359,14 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi return; } - const entry = { text: newText, ariaLabel: newText }; + const entry = { name: nls.localize('notebook.activeCellStatusName', "Notebook Editor Selections"), text: newText, ariaLabel: newText }; if (!this._accessor.value) { this._accessor.value = this._statusbarService.addEntry( entry, 'notebook.activeCellStatus', - nls.localize('notebook.activeCellStatusName', "Notebook Editor Selections"), StatusbarAlignment.RIGHT, - 100); + 100 + ); } else { this._accessor.value.update(entry); } @@ -297,10 +380,11 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi const idxFocused = vm.getCellIndex(activeCell) + 1; const numSelected = vm.getSelections().reduce((prev, range) => prev + (range.end - range.start), 0); + const totalCells = vm.getCells().length; return numSelected > 1 ? nls.localize('notebook.multiActiveCellIndicator', "Cell {0} ({1} selected)", idxFocused, numSelected) : - nls.localize('notebook.singleActiveCellIndicator', "Cell {0}", idxFocused); + nls.localize('notebook.singleActiveCellIndicator', "Cell {0} of {1}", idxFocused, totalCells); } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ActiveCellStatus, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ActiveCellStatus, LifecyclePhase.Restored); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts deleted file mode 100644 index edce9fb60fbf..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts +++ /dev/null @@ -1,142 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { flatten } from 'vs/base/common/arrays'; -import { Throttler } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { INotebookCellStatusBarItemList } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; - -export class NotebookStatusBarController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.statusBar'; - - private readonly _visibleCells = new Map(); - - private readonly _viewModelDisposables = new DisposableStore(); - - constructor( - private readonly _notebookEditor: INotebookEditor, - @INotebookCellStatusBarService private readonly _notebookCellStatusBarService: INotebookCellStatusBarService - ) { - super(); - this._updateVisibleCells(); - this._register(this._notebookEditor.onDidChangeVisibleRanges(this._updateVisibleCells, this)); - this._register(this._notebookEditor.onDidChangeModel(this._onModelChange, this)); - this._register(this._notebookCellStatusBarService.onDidChangeProviders(this._updateEverything, this)); - this._register(this._notebookCellStatusBarService.onDidChangeItems(this._updateEverything, this)); - } - - private _onModelChange() { - this._viewModelDisposables.clear(); - const vm = this._notebookEditor.viewModel; - if (!vm) { - return; - } - - this._viewModelDisposables.add(vm.onDidChangeViewCells(() => this._updateEverything())); - this._updateEverything(); - } - - private _updateEverything(): void { - this._visibleCells.forEach(cell => cell.dispose()); - this._visibleCells.clear(); - this._updateVisibleCells(); - } - - private _updateVisibleCells(): void { - const vm = this._notebookEditor.viewModel; - if (!vm) { - return; - } - - const newVisibleCells = new Set(); - const rangesWithEnd = this._notebookEditor.visibleRanges - .map(range => ({ start: range.start, end: range.end + 1 })); - cellRangesToIndexes(rangesWithEnd) - .map(index => vm.cellAt(index)) - .filter((cell: CellViewModel | undefined): cell is CellViewModel => !!cell) - .map(cell => { - if (!this._visibleCells.has(cell.handle)) { - const helper = new CellStatusBarHelper(vm, cell, this._notebookCellStatusBarService); - this._visibleCells.set(cell.handle, helper); - } - newVisibleCells.add(cell.handle); - }); - - for (let handle of this._visibleCells.keys()) { - if (!newVisibleCells.has(handle)) { - this._visibleCells.get(handle)?.dispose(); - this._visibleCells.delete(handle); - } - } - } - - override dispose(): void { - this._visibleCells.forEach(cell => cell.dispose()); - this._visibleCells.clear(); - } -} - -class CellStatusBarHelper extends Disposable { - private _currentItemIds: string[] = []; - private _currentItemLists: INotebookCellStatusBarItemList[] = []; - - private readonly _cancelTokenSource: CancellationTokenSource; - - private readonly _updateThrottler = new Throttler(); - - constructor( - private readonly _notebookViewModel: NotebookViewModel, - private readonly _cell: ICellViewModel, - private readonly _notebookCellStatusBarService: INotebookCellStatusBarService - ) { - super(); - - this._cancelTokenSource = new CancellationTokenSource(); - this._register(toDisposable(() => this._cancelTokenSource.dispose(true))); - - this._updateSoon(); - this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); - this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); - this._register(this._cell.model.onDidChangeMetadata(() => this._updateSoon())); - this._register(this._cell.model.onDidChangeOutputs(() => this._updateSoon())); - } - - private _updateSoon(): void { - this._updateThrottler.queue(() => this._update()); - } - - private async _update() { - const cellIndex = this._notebookViewModel.getCellIndex(this._cell); - const docUri = this._notebookViewModel.notebookDocument.uri; - const viewType = this._notebookViewModel.notebookDocument.viewType; - const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._cancelTokenSource.token); - if (this._cancelTokenSource.token.isCancellationRequested) { - itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - return; - } - - const items = flatten(itemLists.map(itemList => itemList.items)); - const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); - - this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - this._currentItemLists = itemLists; - this._currentItemIds = newIds; - } - - override dispose() { - super.dispose(); - - this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); - this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - } -} - -registerNotebookContribution(NotebookStatusBarController.id, NotebookStatusBarController); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts index 4358ff31a02d..a625c2e73304 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { flatten } from 'vs/base/common/arrays'; -import { Throttler } from 'vs/base/common/async'; +import { disposableTimeout, Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; @@ -15,7 +15,7 @@ import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/com import { INotebookCellStatusBarItemList } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class ContributedStatusBarItemController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.statusBar'; + static id: string = 'workbench.notebook.statusBar.contributed'; private readonly _visibleCells = new Map(); @@ -69,7 +69,7 @@ class CellStatusBarHelper extends Disposable { private _currentItemIds: string[] = []; private _currentItemLists: INotebookCellStatusBarItemList[] = []; - private readonly _cancelTokenSource: CancellationTokenSource; + private _activeToken: CancellationTokenSource | undefined; private readonly _updateThrottler = new Throttler(); @@ -80,29 +80,31 @@ class CellStatusBarHelper extends Disposable { ) { super(); - this._cancelTokenSource = new CancellationTokenSource(); - this._register(toDisposable(() => this._cancelTokenSource.dispose(true))); - + this._register(toDisposable(() => this._activeToken?.dispose(true))); this._updateSoon(); this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); this._register(this._cell.model.onDidChangeMetadata(() => this._updateSoon())); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._updateSoon())); this._register(this._cell.model.onDidChangeOutputs(() => this._updateSoon())); } private _updateSoon(): void { // Wait a tick to make sure that the event is fired to the EH before triggering status bar providers - setTimeout(() => { + this._register(disposableTimeout(() => { this._updateThrottler.queue(() => this._update()); - }, 0); + }, 0)); } private async _update() { const cellIndex = this._notebookViewModel.getCellIndex(this._cell); const docUri = this._notebookViewModel.notebookDocument.uri; const viewType = this._notebookViewModel.notebookDocument.viewType; - const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._cancelTokenSource.token); - if (this._cancelTokenSource.token.isCancellationRequested) { + + this._activeToken?.dispose(true); + const tokenSource = this._activeToken = new CancellationTokenSource(); + const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, tokenSource.token); + if (tokenSource.token.isCancellationRequested) { itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); return; } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts index 438389b3780d..5b5a85e9e03b 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts @@ -4,19 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; -import { Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; -import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EXECUTE_CELL_COMMAND_ID, ICellViewModel, INotebookEditor, INotebookEditorContribution, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { cellStatusIconError, cellStatusIconSuccess } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookStatusBarController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.statusBar'; + static id: string = 'workbench.notebook.statusBar.exec'; private readonly _visibleCells = new Map(); @@ -24,6 +28,7 @@ export class NotebookStatusBarController extends Disposable implements INotebook constructor( private readonly _notebookEditor: INotebookEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._observer = this._register(new NotebookVisibleCellObserver(this._notebookEditor)); @@ -46,8 +51,9 @@ export class NotebookStatusBarController extends Disposable implements INotebook for (let newCell of e.added) { const helpers = [ - new ExecutionStateCellStatusBarHelper(vm, newCell), - new TimerCellStatusBarHelper(vm, newCell) + this._instantiationService.createInstance(ExecutionStateCellStatusBarHelper, vm, newCell), + this._instantiationService.createInstance(TimerCellStatusBarHelper, vm, newCell), + this._instantiationService.createInstance(KeybindingPlaceholderStatusBarHelper, vm, newCell), ]; this._visibleCells.set(newCell.handle, helpers); } @@ -83,7 +89,7 @@ class ExecutionStateCellStatusBarHelper extends Disposable { super(); this._update(); - this._register(this._cell.model.onDidChangeMetadata(() => this._update())); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); } private async _update() { @@ -101,13 +107,13 @@ class ExecutionStateCellStatusBarHelper extends Disposable { return; } - const item = this._getItemForState(cell.metadata?.runState, cell.metadata?.lastRunSuccess); + const item = this._getItemForState(cell.internalMetadata.runState, cell.internalMetadata.lastRunSuccess); // Show the execution spinner for a minimum time - if (cell.metadata?.runState === NotebookCellExecutionState.Executing) { + if (cell.internalMetadata.runState === NotebookCellExecutionState.Executing) { this._currentExecutingStateTimer = setTimeout(() => { this._currentExecutingStateTimer = undefined; - if (cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + if (cell.internalMetadata.runState !== NotebookCellExecutionState.Executing) { this._update(); } }, ExecutionStateCellStatusBarHelper.MIN_SPINNER_TIME); @@ -117,7 +123,7 @@ class ExecutionStateCellStatusBarHelper extends Disposable { } private _getItemForState(runState: NotebookCellExecutionState | undefined, lastRunSuccess: boolean | undefined): INotebookCellStatusBarItem | undefined { - if (runState === NotebookCellExecutionState.Idle && lastRunSuccess) { + if (!runState && lastRunSuccess) { return { text: '$(notebook-state-success)', color: themeColorFromId(cellStatusIconSuccess), @@ -125,7 +131,7 @@ class ExecutionStateCellStatusBarHelper extends Disposable { alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER }; - } else if (runState === NotebookCellExecutionState.Idle && lastRunSuccess === false) { + } else if (!runState && lastRunSuccess === false) { return { text: '$(notebook-state-error)', color: themeColorFromId(cellStatusIconError), @@ -173,23 +179,22 @@ class TimerCellStatusBarHelper extends Disposable { this._scheduler = this._register(new RunOnceScheduler(() => this._update(), TimerCellStatusBarHelper.UPDATE_INTERVAL)); this._update(); - this._register( - Event.filter(this._cell.model.onDidChangeMetadata, e => !!e.runStateChanged) - (() => this._update())); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); } private async _update() { let item: INotebookCellStatusBarItem | undefined; - if (this._cell.metadata?.runState === NotebookCellExecutionState.Executing) { - const startTime = this._cell.metadata.runStartTime; - const adjustment = this._cell.metadata.runStartTimeAdjustment; + const state = this._cell.internalMetadata.runState; + if (state === NotebookCellExecutionState.Executing) { + const startTime = this._cell.internalMetadata.runStartTime; + const adjustment = this._cell.internalMetadata.runStartTimeAdjustment; if (typeof startTime === 'number') { item = this._getTimeItem(startTime, Date.now(), adjustment); this._scheduler.schedule(); } - } else if (this._cell.metadata?.runState === NotebookCellExecutionState.Idle) { - const startTime = this._cell.metadata.runStartTime; - const endTime = this._cell.metadata.runEndTime; + } else if (!state) { + const startTime = this._cell.internalMetadata.runStartTime; + const endTime = this._cell.internalMetadata.runEndTime; if (typeof startTime === 'number' && typeof endTime === 'number') { item = this._getTimeItem(startTime, endTime); } @@ -222,4 +227,89 @@ class TimerCellStatusBarHelper extends Disposable { } } +/** + * Shows a keybinding hint for the execute command + */ +class KeybindingPlaceholderStatusBarHelper extends Disposable { + private _currentItemIds: string[] = []; + private readonly _codeContextKeyService: IContextKeyService; + private readonly _markupContextKeyService: IContextKeyService; + + constructor( + private readonly _notebookViewModel: NotebookViewModel, + private readonly _cell: ICellViewModel, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService _contextKeyService: IContextKeyService, + ) { + super(); + + // Create a fake ContextKeyService, and look up the keybindings within this context. + const commonContextKeyService = this._register(_contextKeyService.createScoped(document.createElement('div'))); + InputFocusedContext.bindTo(commonContextKeyService).set(true); + EditorContextKeys.editorTextFocus.bindTo(commonContextKeyService).set(true); + EditorContextKeys.focus.bindTo(commonContextKeyService).set(true); + EditorContextKeys.textInputFocus.bindTo(commonContextKeyService).set(true); + NOTEBOOK_CELL_EXECUTION_STATE.bindTo(commonContextKeyService).set('idle'); + NOTEBOOK_CELL_LIST_FOCUSED.bindTo(commonContextKeyService).set(true); + NOTEBOOK_EDITOR_FOCUSED.bindTo(commonContextKeyService).set(true); + + this._codeContextKeyService = this._register(commonContextKeyService.createScoped(document.createElement('div'))); + NOTEBOOK_CELL_TYPE.bindTo(this._codeContextKeyService).set('code'); + + this._markupContextKeyService = this._register(commonContextKeyService.createScoped(document.createElement('div'))); + NOTEBOOK_CELL_TYPE.bindTo(this._markupContextKeyService).set('markup'); + + this._update(); + this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); + } + + private async _update() { + const items = this._getItemsForCell(this._cell); + if (Array.isArray(items)) { + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + } + } + + private _getItemsForCell(cell: ICellViewModel): INotebookCellStatusBarItem[] { + if (typeof cell.internalMetadata.runState !== 'undefined' || typeof cell.internalMetadata.lastRunSuccess !== 'undefined') { + return []; + } + + let text: string; + if (cell.cellKind === CellKind.Code) { + const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_CELL_COMMAND_ID, this._codeContextKeyService)?.getLabel(); + if (!keybinding) { + return []; + } + + text = localize('notebook.cell.status.codeExecuteTip', "Press {0} to execute cell", keybinding); + } else { + const keybinding = this._keybindingService.lookupKeybinding(QUIT_EDIT_CELL_COMMAND_ID, this._markupContextKeyService)?.getLabel(); + if (!keybinding) { + return []; + } + + text = localize('notebook.cell.status.markdownExecuteTip', "Press {0} to stop editing", keybinding); + } + + const item = { + text, + tooltip: text, + alignment: CellStatusbarAlignment.Left, + opacity: '0.7', + onlyShowWhenActive: true, + priority: 100 + }; + + return [item]; + } + + + override dispose() { + super.dispose(); + + this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); + } +} + registerNotebookContribution(NotebookStatusBarController.id, NotebookStatusBarController); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts index 31b6e4282d2d..c18498f3c6de 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts @@ -5,7 +5,6 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; -import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; @@ -15,52 +14,12 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } fr import { CHANGE_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -class CellStatusBarPlaceholderProvider implements INotebookCellStatusBarItemProvider { - readonly selector: NotebookSelector = { - pattern: '**/*' - }; - - constructor( - @INotebookService private readonly _notebookService: INotebookService, - ) { } - - async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise { - const doc = this._notebookService.getNotebookTextModel(uri); - const cell = doc?.cells[index]; - if (!cell || typeof cell.metadata.runState !== 'undefined' || typeof cell.metadata.lastRunSuccess !== 'undefined') { - return; - } - - let text: string; - if (cell.cellKind === CellKind.Code) { - text = isWindows ? - localize('notebook.cell.status.codeExecuteTipWin', "Press Ctrl+Alt+Enter to execute cell") : - localize('notebook.cell.status.codeExecuteTipNotWin', "Press Ctrl+Enter to execute cell"); - } else { - text = localize('notebook.cell.status.markdownExecuteTip', "Press Escape to stop editing"); - } - - const item = { - text, - tooltip: text, - alignment: CellStatusbarAlignment.Left, - opacity: '0.7', - onlyShowWhenActive: true - }; - return { - items: [item] - }; - } -} - class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemProvider { - readonly selector: NotebookSelector = { - pattern: '**/*' - }; + + readonly viewType = '*'; constructor( @INotebookService private readonly _notebookService: INotebookService, @@ -74,7 +33,7 @@ class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemP return; } - const modeId = cell.cellKind === CellKind.Markdown ? + const modeId = cell.cellKind === CellKind.Markup ? 'markdown' : (this._modeService.getModeIdForLanguageName(cell.language) || cell.language); const text = this._modeService.getLanguageName(modeId) || this._modeService.getLanguageName('plaintext'); @@ -98,7 +57,6 @@ class BuiltinCellStatusBarProviders extends Disposable { super(); const builtinProviders = [ - CellStatusBarPlaceholderProvider, CellStatusBarLanguagePickerProvider, ]; builtinProviders.forEach(p => { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts index b198ae8de101..c2e5a97057e6 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { CellEditState, getNotebookEditorFromEditorPane, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; class NotebookUndoRedoContribution extends Disposable { @@ -24,12 +24,12 @@ class NotebookUndoRedoContribution extends Disposable { return editor.viewModel.undo().then(cellResources => { if (cellResources?.length) { editor?.viewModel?.viewCells.forEach(cell => { - if (cell.cellKind === CellKind.Markdown && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { + if (cell.cellKind === CellKind.Markup && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { cell.updateEditState(CellEditState.Editing, 'undo'); } }); - editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true })); + editor?.setOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true }); } }); } @@ -43,12 +43,12 @@ class NotebookUndoRedoContribution extends Disposable { return editor.viewModel.redo().then(cellResources => { if (cellResources?.length) { editor?.viewModel?.viewCells.forEach(cell => { - if (cell.cellKind === CellKind.Markdown && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { + if (cell.cellKind === CellKind.Markup && cellResources.find(resource => resource.fragment === cell.model.uri.fragment)) { cell.updateEditState(CellEditState.Editing, 'redo'); } }); - editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true })); + editor?.setOptions({ cellOptions: { resource: cellResources[0] }, preserveFocus: true }); } }); } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts index 5c4358699baf..f9c76ebf4b99 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/test/notebookUndoRedo.test.ts @@ -12,8 +12,8 @@ suite('Notebook Undo/Redo', () => { test('Basics', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { const modeService = accessor.get(IModeService); @@ -57,8 +57,8 @@ suite('Notebook Undo/Redo', () => { test('Invalid replace count should not throw', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { const modeService = accessor.get(IModeService); @@ -81,8 +81,8 @@ suite('Notebook Undo/Redo', () => { test('Replace beyond length', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor) => { const viewModel = editor.viewModel; @@ -100,8 +100,8 @@ suite('Notebook Undo/Redo', () => { test('Invalid replace count should not affect undo/redo', async function () { await withTestNotebook( [ - ['# header 1', 'markdown', CellKind.Markdown, [], {}], - ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markup, [], {}], + ['body', 'markdown', CellKind.Markup, [], {}], ], async (editor, accessor) => { const modeService = accessor.get(IModeService); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts index ac8c52e30f90..d9838fa18699 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts @@ -13,7 +13,7 @@ import { BUILTIN_RENDERER_ID, CellKind } from 'vs/workbench/contrib/notebook/com import { cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -class NotebookClipboardContribution extends Disposable implements INotebookEditorContribution { +class NotebookViewportContribution extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.viewportCustomMarkdown'; private readonly _warmupViewport: RunOnceScheduler; @@ -22,18 +22,26 @@ class NotebookClipboardContribution extends Disposable implements INotebookEdito super(); this._warmupViewport = new RunOnceScheduler(() => this._warmupViewportNow(), 200); - + this._register(this._warmupViewport); this._register(this._notebookEditor.onDidScroll(() => { this._warmupViewport.schedule(); })); } private _warmupViewportNow() { + if (this._notebookEditor.isDisposed) { + return; + } + + if (!this._notebookEditor.hasModel()) { + return; + } + const visibleRanges = this._notebookEditor.getVisibleRangesPlusViewportAboveBelow(); cellRangesToIndexes(visibleRanges).forEach(index => { const cell = this._notebookEditor.viewModel?.viewCells[index]; - if (cell?.cellKind === CellKind.Markdown && cell?.getEditState() === CellEditState.Preview && !cell.metadata?.inputCollapsed) { + if (cell?.cellKind === CellKind.Markup && cell?.getEditState() === CellEditState.Preview && !cell.metadata.inputCollapsed) { this._notebookEditor.createMarkdownPreview(cell); } else if (cell?.cellKind === CellKind.Code) { const viewCell = (cell as CodeCellViewModel); @@ -50,10 +58,14 @@ class NotebookClipboardContribution extends Disposable implements INotebookEdito return; } + if (!this._notebookEditor.hasModel()) { + return; + } + if (pickedMimeTypeRenderer.rendererId === BUILTIN_RENDERER_ID) { const renderer = this._notebookEditor.getOutputRenderer().getContribution(pickedMimeTypeRenderer.mimeType); if (renderer?.getType() === RenderOutputType.Html) { - const renderResult = renderer!.render(output, output.model.outputs.filter(op => op.mime === pickedMimeTypeRenderer.mimeType), DOM.$(''), undefined) as IInsetRenderOutput; + const renderResult = renderer.render(output, output.model.outputs.filter(op => op.mime === pickedMimeTypeRenderer.mimeType), DOM.$(''), this._notebookEditor.viewModel.uri) as IInsetRenderOutput; this._notebookEditor.createOutput(viewCell, renderResult, 0); } return; @@ -72,4 +84,4 @@ class NotebookClipboardContribution extends Disposable implements INotebookEdito } } -registerNotebookContribution(NotebookClipboardContribution.id, NotebookClipboardContribution); +registerNotebookContribution(NotebookViewportContribution.id, NotebookViewportContribution); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index da02d0400db0..ba85ba7c48dd 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -5,11 +5,11 @@ import * as DOM from 'vs/base/browser/dom'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DiffElementViewModelBase, getFormatedMetadataJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; -import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -25,7 +25,6 @@ import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/men import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Delayer } from 'vs/base/common/async'; import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; -import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { OutputContainer } from 'vs/workbench/contrib/notebook/browser/diff/diffElementOutputs'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; @@ -41,11 +40,12 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +const fixedEditorPadding = { + top: 12, + bottom: 12 +}; export const fixedEditorOptions: IEditorOptions = { - padding: { - top: 12, - bottom: 12 - }, + padding: fixedEditorPadding, scrollBeyondLastLine: false, scrollbar: { verticalScrollbarSize: 14, @@ -491,22 +491,6 @@ abstract class AbstractElementRenderer extends Disposable { } break; - case 'executionOrder': - // number - if (typeof newMetadataObj[key] === 'number') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'runState': - // enum - if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; default: result[key] = newMetadataObj[key]; break; @@ -550,8 +534,8 @@ abstract class AbstractElementRenderer extends Disposable { this._metadataEditorContainer?.classList.add('diff'); - const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.originalDocument.uri, this.cell.original!.handle)); - const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle)); + const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellUri(this.cell.originalDocument.uri, this.cell.original!.handle, Schemas.vscodeNotebookCellMetadata)); + const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle, Schemas.vscodeNotebookCellMetadata)); this._metadataEditor.setModel({ original: originalMetadataModel.object.textEditorModel, modified: modifiedMetadataModel.object.textEditorModel @@ -613,7 +597,7 @@ abstract class AbstractElementRenderer extends Disposable { ? this.cell.modified!.handle : this.cell.original!.handle; - const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const modelUri = CellUri.generateCellUri(uri, handle, Schemas.vscodeNotebookCellMetadata); const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); this._metadataEditor.setModel(metadataModel); this._metadataEditorDisposeStore.add(metadataModel); @@ -979,7 +963,7 @@ export class DeletedElement extends SingleSideDiffElement { const originalCell = this.cell.original!; const lineCount = originalCell.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editor = this.templateData.sourceEditor; this._editor.layout({ @@ -1130,7 +1114,7 @@ export class InsertElement extends SingleSideDiffElement { const modifiedCell = this.cell.modified!; const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editor = this.templateData.sourceEditor; this._editor.layout( @@ -1468,7 +1452,8 @@ export class ModifiedElement extends AbstractElementRenderer { const modifiedCell = this.cell.modified!; const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; this._editorContainer = this.templateData.editorContainer; this._editor = this.templateData.sourceEditor; diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts index 160389b88141..013bdbd92c68 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -14,7 +14,6 @@ import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/r import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { BUILTIN_RENDERER_ID, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -90,7 +89,7 @@ export class OutputElement extends Disposable { result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri); } - this.output.pickedMimeType = pick; + this.output.pickedMimeType = pickedMimeTypeRenderer; } this.domNode = outputItemDiv; @@ -202,7 +201,7 @@ export class OutputElement extends Disposable { ); } - viewModel.pickedMimeType = pick; + viewModel.pickedMimeType = mimeTypes[pick]; this.render(index, nextElement as HTMLElement); } } @@ -250,9 +249,7 @@ export class OutputContainer extends Disposable { private _outputContainer: HTMLElement, @INotebookService private _notebookService: INotebookService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IOpenerService readonly _openerService: IOpenerService, - @ITextFileService readonly _textFileService: ITextFileService, - + @IOpenerService readonly _openerService: IOpenerService ) { super(); this._register(this._diffElementViewModel.onDidLayoutChange(() => { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index ededf65643a0..edd615095787 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -45,15 +45,17 @@ registerAction2(class extends Action2 { const activeEditor = editorService.activeEditorPane; if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { const diffEditorInput = activeEditor.input as NotebookDiffEditorInput; - const leftResource = diffEditorInput.originalResource; - const rightResource = diffEditorInput.resource; - const options = { - preserveFocus: false - }; - - const label = diffEditorInput.textDiffName; - const input = editorService.createEditorInput({ leftResource, rightResource, label, options }); - await editorService.openEditor(input, { override: EditorOverride.DISABLED }, viewColumnToEditorGroup(editorGroupService, undefined)); + + await editorService.openEditor( + { + originalInput: { resource: diffEditorInput.originalResource }, + modifiedInput: { resource: diffEditorInput.resource }, + label: diffEditorInput.textDiffName, + options: { + preserveFocus: false, + override: EditorOverride.DISABLED + } + }, viewColumnToEditorGroup(editorGroupService, undefined)); } } }); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 0ca043aacff0..9685efbaec3c 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -15,6 +15,7 @@ import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; export enum DiffSide { Original = 0, @@ -26,6 +27,7 @@ export interface IDiffCellInfo extends ICommonCellInfo { } export interface INotebookTextDiffEditor extends ICommonNotebookEditor { + notebookOptions: NotebookOptions; readonly textModel?: NotebookTextModel; onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>; onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel, output: ICellOutputViewModel }>; diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 03d33ccf4969..6b6904662873 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; @@ -20,10 +20,10 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; -import { CellEditState, ICellOutputViewModel, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo, NOTEBOOK_DIFF_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, ICellOutputViewModel, IDisplayOutputLayoutUpdateRequest, IGenericCellViewModel, IInsetRenderOutput, INotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_DIFF_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -38,9 +38,9 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CELL_OUTPUT_PADDING, MARKDOWN_PREVIEW_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; const $ = DOM.$; @@ -72,9 +72,15 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _revealFirst: boolean; private readonly _insetModifyQueueByOutputId = new SequencerByKey(); - protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: ICellOutputViewModel }>(); + protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: ICellOutputViewModel; }>(); onDidDynamicOutputRendered = this._onDidDynamicOutputRendered.event; + private _notebookOptions: NotebookOptions; + + get notebookOptions() { + return this._notebookOptions; + } + private readonly _localStore = this._register(new DisposableStore()); private _isDisposed: boolean = false; @@ -93,10 +99,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @IStorageService storageService: IStorageService, ) { super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); - const editorOptions = this.configurationService.getValue('editor'); + this._notebookOptions = new NotebookOptions(this.configurationService); + this._register(this._notebookOptions); + const editorOptions = this.configurationService.getValue('editor'); this._fontInfo = readFontInfo(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio())); this._revealFirst = true; - this._outputRenderer = new OutputRenderer(this, this.instantiationService); } @@ -136,10 +143,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD setMarkdownCellEditState(cellId: string, editState: CellEditState): void { // throw new Error('Method not implemented.'); } - markdownCellDragStart(cellId: string, position: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number; }): void { // throw new Error('Method not implemented.'); } - markdownCellDrag(cellId: string, position: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number; }): void { // throw new Error('Method not implemented.'); } markdownCellDragEnd(cellId: string): void { @@ -285,7 +292,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } } - override async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: NotebookDiffEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); const model = await input.resolve(); @@ -366,21 +373,12 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD })); } - private readonly webviewOptions = { - outputNodePadding: CELL_OUTPUT_PADDING, - outputNodeLeftPadding: 32, - previewNodePadding: MARKDOWN_PREVIEW_PADDING, - leftMargin: 0, - rightMargin: 0, - runGutter: 0 - }; - private async _createModifiedWebview(id: string, resource: URI): Promise { if (this._modifiedWebview) { this._modifiedWebview.dispose(); } - this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this.webviewOptions) as BackLayerWebView; + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); await this._modifiedWebview.createWebview(); @@ -393,7 +391,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview.dispose(); } - this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this.webviewOptions) as BackLayerWebView; + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); await this._originalWebview.createWebview(); @@ -413,16 +411,43 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview?.removeInsets([...this._originalWebview?.insetMapping.keys()]); this._modifiedWebview?.removeInsets([...this._modifiedWebview?.insetMapping.keys()]); - this._diffElementViewModels = viewModels; - this._list.splice(0, this._list.length, this._diffElementViewModels); + this._setViewModel(viewModels); + // this._diffElementViewModels = viewModels; + // this._list.splice(0, this._list.length, this._diffElementViewModels); - if (this._revealFirst && firstChangeIndex !== -1) { + if (this._revealFirst && firstChangeIndex !== -1 && firstChangeIndex < this._list.length) { this._revealFirst = false; this._list.setFocus([firstChangeIndex]); this._list.reveal(firstChangeIndex, 0.3); } } + private _setViewModel(viewModels: DiffElementViewModelBase[]) { + let isSame = true; + if (this._diffElementViewModels.length === viewModels.length) { + for (let i = 0; i < viewModels.length; i++) { + const a = this._diffElementViewModels[i]; + const b = viewModels[i]; + + if (a.original?.textModel.getHashValue() !== b.original?.textModel.getHashValue() + || a.modified?.textModel.getHashValue() !== b.modified?.textModel.getHashValue()) { + isSame = false; + break; + } + } + } else { + isSame = false; + } + + if (isSame) { + return; + } + + this._diffElementViewModels = viewModels; + this._list.splice(0, this._list.length, this._diffElementViewModels); + + } + /** * making sure that swapping cells are always translated to `insert+delete`. */ @@ -741,6 +766,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedResourceDisposableStore.clear(); this._list?.splice(0, this._list?.length || 0); + this._model = null; + this._diffElementViewModels = []; } getOutputRenderer(): OutputRenderer { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index 1c7fe9d20093..3e827183b54b 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -6,73 +6,58 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority, NotebookRendererEntrypoint, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; namespace NotebookEditorContribution { - export const viewType = 'viewType'; + export const type = 'type'; export const displayName = 'displayName'; export const selector = 'selector'; export const priority = 'priority'; } export interface INotebookEditorContribution { - readonly [NotebookEditorContribution.viewType]: string; + readonly [NotebookEditorContribution.type]: string; readonly [NotebookEditorContribution.displayName]: string; readonly [NotebookEditorContribution.selector]?: readonly { filenamePattern?: string; excludeFileNamePattern?: string; }[]; readonly [NotebookEditorContribution.priority]?: string; } namespace NotebookRendererContribution { - export const viewType = 'viewType'; + export const id = 'id'; export const displayName = 'displayName'; export const mimeTypes = 'mimeTypes'; export const entrypoint = 'entrypoint'; export const hardDependencies = 'dependencies'; export const optionalDependencies = 'optionalDependencies'; + export const requiresMessaging = 'requiresMessaging'; } export interface INotebookRendererContribution { readonly [NotebookRendererContribution.id]?: string; - readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]: string; + readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint; readonly [NotebookRendererContribution.hardDependencies]: readonly string[]; readonly [NotebookRendererContribution.optionalDependencies]: readonly string[]; -} - -enum NotebookMarkupRendererContribution { - id = 'id', - displayName = 'displayName', - entrypoint = 'entrypoint', - dependsOn = 'dependsOn', - mimeTypes = 'mimeTypes', -} - -export interface INotebookMarkupRendererContribution { - readonly [NotebookMarkupRendererContribution.id]?: string; - readonly [NotebookMarkupRendererContribution.displayName]: string; - readonly [NotebookMarkupRendererContribution.entrypoint]: string; - readonly [NotebookMarkupRendererContribution.dependsOn]: string | undefined; - readonly [NotebookMarkupRendererContribution.mimeTypes]: string[] | undefined; + readonly [NotebookRendererContribution.requiresMessaging]: RendererMessagingSpec; } const notebookProviderContribution: IJSONSchema = { description: nls.localize('contributes.notebook.provider', 'Contributes notebook document provider.'), type: 'array', - defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], + defaultSnippets: [{ body: [{ type: '', displayName: '', 'selector': [{ 'filenamePattern': '' }] }] }], items: { type: 'object', required: [ - NotebookEditorContribution.viewType, + NotebookEditorContribution.type, NotebookEditorContribution.displayName, NotebookEditorContribution.selector, ], properties: { - [NotebookEditorContribution.viewType]: { + [NotebookEditorContribution.type]: { type: 'string', - description: nls.localize('contributes.notebook.provider.viewType', 'Unique identifier of the notebook.'), + description: nls.localize('contributes.notebook.provider.viewType', 'Type of the notebook.'), }, [NotebookEditorContribution.displayName]: { type: 'string', @@ -129,11 +114,6 @@ const notebookRendererContribution: IJSONSchema = { type: 'string', description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), }, - [NotebookRendererContribution.viewType]: { - type: 'string', - deprecationMessage: nls.localize('contributes.notebook.provider.viewType.deprecated', 'Rename `viewType` to `id`.'), - description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), - }, [NotebookRendererContribution.displayName]: { type: 'string', description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'), @@ -146,8 +126,27 @@ const notebookRendererContribution: IJSONSchema = { } }, [NotebookRendererContribution.entrypoint]: { - type: 'string', description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + oneOf: [ + { + type: 'string', + }, + // todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption + // { + // type: 'object', + // required: ['extends', 'path'], + // properties: { + // extends: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), + // }, + // path: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + // }, + // } + // } + ] }, [NotebookRendererContribution.hardDependencies]: { type: 'array', @@ -161,60 +160,33 @@ const notebookRendererContribution: IJSONSchema = { items: { type: 'string' }, markdownDescription: nls.localize('contributes.notebook.renderer.optionalDependencies', 'List of soft kernel dependencies the renderer can make use of. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer will be preferred over renderers that don\'t interact with the kernel.'), }, - } - } -}; -const notebookMarkupRendererContribution: IJSONSchema = { - description: nls.localize('contributes.notebook.markdownRenderer', 'Contributes a renderer for markdown cells in notebooks.'), - type: 'array', - defaultSnippets: [{ body: [{ id: '', displayName: '', entrypoint: '' }] }], - items: { - type: 'object', - required: [ - NotebookMarkupRendererContribution.id, - NotebookMarkupRendererContribution.displayName, - NotebookMarkupRendererContribution.entrypoint, - ], - properties: { - [NotebookMarkupRendererContribution.id]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.id', 'Unique identifier of the notebook markdown renderer.'), - }, - [NotebookMarkupRendererContribution.displayName]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.displayName', 'Human readable name of the notebook markdown renderer.'), - }, - [NotebookMarkupRendererContribution.entrypoint]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.entrypoint', 'File to load in the webview to render the extension.'), - }, - [NotebookMarkupRendererContribution.mimeTypes]: { - type: 'array', - items: { type: 'string' }, - description: nls.localize('contributes.notebook.markdownRenderer.mimeTypes', 'The mime type that the renderer handles.'), - }, - [NotebookMarkupRendererContribution.dependsOn]: { - type: 'string', - description: nls.localize('contributes.notebook.markdownRenderer.dependsOn', 'If specified, this renderer augments another renderer instead of providing full rendering.'), + [NotebookRendererContribution.requiresMessaging]: { + default: 'never', + enum: [ + 'always', + 'optional', + 'never', + ], + + enumDescriptions: [ + nls.localize('contributes.notebook.renderer.requiresMessaging.always', 'Messaging is required. The renderer will only be used when it\'s part of an extension that can be run in an extension host.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not requried.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.never', 'The renderer does not require messaging.'), + ], + description: nls.localize('contributes.notebook.renderer.requiresMessaging', 'Defines how and if the renderer needs to communicate with an extension host, via `createRendererMessaging`. Renderers with stronger messaging requirements may not work in all environments.'), }, } } }; -export const notebookProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint( +export const notebooksExtensionPoint = ExtensionsRegistry.registerExtensionPoint( { - extensionPoint: 'notebookProvider', + extensionPoint: 'notebooks', jsonSchema: notebookProviderContribution }); export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint( { - extensionPoint: 'notebookOutputRenderer', + extensionPoint: 'notebookRenderer', jsonSchema: notebookRendererContribution }); - -export const notebookMarkupRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint( - { - extensionPoint: 'notebookMarkupRenderers', - jsonSchema: notebookMarkupRendererContribution - }); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 128c3f17ad88..788360ba69d3 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -11,23 +11,81 @@ position: relative; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar { +.monaco-workbench .notebookOverlay .notebook-toolbar-container { width: 100%; - display: inline-flex; - padding-left: 8px; - margin-top: 4px; + display: none; + margin-top: 2px; + margin-bottom: 2px; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item { - width: 24px; +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item { height: 22px; display: flex; align-items: center; + border-radius: 5px; + margin-right: 8px; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item .action-label { +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element { + flex: 1; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element .notebook-toolbar-left { + padding: 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-right { + display: flex; + padding: 0px 0px 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .kernel-label { background-size: 16px; - margin: 4px 4px 0 4px; + padding: 0px 5px 0px 3px; + border-radius: 5px; + font-size: 13px; + height: 22px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left .monaco-action-bar .action-item .action-label.separator { + margin: 5px 0px !important; + padding: 0px !important; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item:hover { + background-color: var(--code-toolbarHoverBackground); +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .action-label { + background-size: 16px; + padding-left: 2px; +} + +.monaco-workbench .notebook-action-view-item .action-label { + display: inline-flex; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .notebook-label { + background-size: 16px; + padding: 0px 5px 0px 2px; + border-radius: 5px; + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item.disabled .notebook-label { + opacity: 0.4; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active .action-label:not(.disabled) { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: unset; } .monaco-workbench .cell.markdown { @@ -162,6 +220,7 @@ .monaco-workbench .notebookOverlay .output { position: absolute; height: 0px; + font-size: var(--notebook-cell-output-font-size); user-select: text; -webkit-user-select: text; -ms-user-select: text; @@ -200,18 +259,21 @@ color: red; /*TODO@rebornix theme color*/ } -.monaco-workbench .notebookOverlay .cell-drag-image .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .cell-drag-image .output .cell-output-toolbar { display: none; } -.monaco-workbench .notebookOverlay .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .output .cell-output-toolbar { position: absolute; top: 4px; - left: -30px; - width: 16px; + left: -32px; height: 16px; cursor: pointer; - padding: 6px; + padding: 6px 0px; +} + +.monaco-workbench .notebookOverlay .output .cell-output-toolbar .actions-container { + justify-content: center; } .monaco-workbench .notebookOverlay .output pre { @@ -293,44 +355,17 @@ display: none; } -/* top and bottom borders on cells */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { - content: ""; - position: absolute; - width: 100%; - height: 1px; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - content: ""; - position: absolute; - width: 1px; - height: 100%; - z-index: 10; -} - -/* top border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { - border-top: 1px solid transparent; -} - -/* left border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { - border-left: 1px solid transparent; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .notebook-folding-indicator.mouseover .codicon { + opacity: 0; + transition: opacity 0.s; } -/* bottom border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { - border-bottom: 1px solid transparent; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover .notebook-folding-indicator.mouseover .codicon { + opacity: 1; } -/* right border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - border-right: 1px solid transparent; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .notebook-folding-indicator.mouseover .codicon { + opacity: 1; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { @@ -368,19 +403,7 @@ z-index: 50; } -.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - right: 44px; -} - -.monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - left: 76px; -} - -.monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - display: none; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item.menu-entry { width: 24px; height: 24px; display: flex; @@ -410,7 +433,7 @@ overflow: hidden; } -.monaco-workbench .notebookOverlay.cell-statusbar-hidden .cell-statusbar-container { +.monaco-workbench .notebookOverlay .cell-statusbar-hidden .cell-statusbar-container { display: none; } @@ -471,7 +494,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { position: absolute; flex-shrink: 0; - z-index: 27; /* Above the drag handle */ + z-index: 29; /* Above the drag handle, output, and toolbars */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar { @@ -479,8 +502,7 @@ height: initial; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .codicon { - margin: 0 4px 0 0; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .action-item:not(.monaco-dropdown-with-primary) .codicon { padding: 6px; } @@ -490,6 +512,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .run-button-container .monaco-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-run-toolbar-dropdown-active .run-button-container .monaco-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .run-button-container .monaco-toolbar { visibility: visible; } @@ -503,12 +526,14 @@ opacity: .6; /* Sizing hacks */ - left: 26px; - width: 35px; bottom: 0px; text-align: center; } +.monaco-workbench .notebookOverlay>.cell-list-container>.monaco-list>.monaco-scrollable-element>.monaco-list-rows>.monaco-list-row .cell-statusbar-hidden .execution-count-label { + line-height: 15px; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .output-collapsed .execution-count-label { bottom: 24px; } @@ -530,14 +555,24 @@ height: 2px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover.cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar:hover, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { +/* toolbar visible on hover */ +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover.cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar:hover, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { + opacity: 1; +} + +/* toolbar visible on click */ +.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + visibility: hidden; +} +.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell-title-toolbar { opacity: 1; + visibility: visible; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { @@ -574,6 +609,26 @@ right: 0px; } +/** cell border colors */ + +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-bottom:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container.cell-editor-focus:before { + border-color: var(notebook-selected-cell-border-color) !important; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before { + border-color: var(--notebook-inactive-focused-cell-border-color) !important; +} + +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-bottom:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { + border-color: var(--notebook-focused-cell-border-color) !important; +} + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-drag-handle { position: absolute; top: 0px; @@ -851,7 +906,7 @@ position: absolute; top: 10px; - left: 8px; + left: 6px; display: flex; justify-content: center; align-items: center; @@ -865,7 +920,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { visibility: visible; height: 16px; - padding: 4px 4px 4px 6px; + padding: 4px 4px 4px 4px; } /** Theming */ @@ -954,3 +1009,4 @@ .hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-bottom:before { border-bottom-style: dotted; } .hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before { border-left-style: dotted; } .hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { border-right-style: dotted; } + diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css b/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css new file mode 100644 index 000000000000..5f394da2d1f1 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .kernel-action-view-item { + border-radius: 5px; +} +.monaco-workbench .kernel-action-view-item:hover { + background-color: var(--code-toolbarHoverBackground); +} + +.monaco-workbench .kernel-action-view-item .action-label { + display: inline-flex; +} + +.monaco-workbench .kernel-action-view-item .kernel-label { + font-size: 11px; + padding: 3px 5px 3px 3px; + border-radius: 5px; + height: 16px; + display: inline-flex; + vertical-align: text-bottom; +} diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 2d2887db0149..4bacef840b79 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { parse } from 'vs/base/common/marshalling'; import { isEqual } from 'vs/base/common/resources'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; import { ITextModel, ITextBufferFactory, DefaultEndOfLine, ITextBuffer } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Extensions, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -22,13 +24,13 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { EditorInput, ICustomEditorInputFactory, IEditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, IEditorInputWithOptions, EditorExtensions } from 'vs/workbench/common/editor'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IEditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, IEditorInputWithOptions, EditorExtensions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, ExperimentalUseMarkdownRenderer, getCellUndoRedoComparisonKey, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookTextDiffEditorPreview, NOTEBOOK_WORKING_COPY_TYPE_PREFIX, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocation, CellToolbarVisibility, CellUri, DisplayOrderKey, UndoRedoPerCell, ExperimentalUseMarkdownRenderer, getCellUndoRedoComparisonKey, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookTextDiffEditorPreview, NotebookWorkingCopyTypeIdentifier, ShowCellStatusBar, CompactView, FocusIndicator, InsertToolbarLocation, GlobalToolbar, ConsolidatedOutputButton, ShowFoldingControls, DragAndDropEnabled, NotebookCellEditorOptionsCustomizations, ConsolidatedRunButton } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; @@ -41,16 +43,22 @@ import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/brow import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Event } from 'vs/base/common/event'; import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; -import { IWorkingCopyIdentifier, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { EditorOverride } from 'vs/platform/editor/common/editor'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; @@ -58,10 +66,12 @@ import 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; +import 'vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted'; import 'vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow'; import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; +import 'vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController'; @@ -71,12 +81,12 @@ import 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperati import 'vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown'; import 'vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout'; - // Diff Editor Contribution import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; +import { editorOptionsRegistry } from 'vs/editor/common/config/editorOptions'; /*--------------------------------------------------------------------------------------------- */ @@ -120,7 +130,7 @@ class NotebookDiffEditorSerializer implements IEditorInputSerializer { } deserialize(instantiationService: IInstantiationService, raw: string) { - type Data = { resource: URI, originalResource: URI, name: string, originalName: string, viewType: string, textDiffName: string | undefined, group: number }; + type Data = { resource: URI, originalResource: URI, name: string, originalName: string, viewType: string, textDiffName: string | undefined, group: number; }; const data = parse(raw); if (!data) { return undefined; @@ -154,7 +164,7 @@ class NotebookEditorSerializer implements IEditorInputSerializer { }); } deserialize(instantiationService: IInstantiationService, raw: string) { - type Data = { resource: URI, viewType: string, group: number }; + type Data = { resource: URI, viewType: string, group: number; }; const data = parse(raw); if (!data) { return undefined; @@ -174,35 +184,6 @@ Registry.as(EditorExtensions.EditorInputFactories). NotebookEditorSerializer ); -Registry.as(EditorExtensions.EditorInputFactories).registerCustomEditorInputFactory( - Schemas.vscodeNotebook, - new class implements ICustomEditorInputFactory { - async createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { - return instantiationService.invokeFunction(async accessor => { - const workingCopyBackupService = accessor.get(IWorkingCopyBackupService); - - const backup = await workingCopyBackupService.resolve({ resource, typeId: NO_TYPE_ID }); - if (!backup?.meta) { - throw new Error(`No backup found for Notebook editor: ${resource}`); - } - - const input = NotebookEditorInput.create(instantiationService, resource, backup.meta.viewType, { startDirty: true }); - return input; - }); - } - - canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { - if (editorInput instanceof NotebookEditorInput) { - if (isEqual(URI.from({ scheme: Schemas.vscodeNotebook, path: editorInput.resource.toString() }), backupResource)) { - return true; - } - } - - return false; - } - } -); - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputSerializer( NotebookDiffEditorInput.ID, NotebookDiffEditorSerializer @@ -211,12 +192,15 @@ Registry.as(EditorExtensions.EditorInputFactories). export class NotebookContribution extends Disposable implements IWorkbenchContribution { constructor( @IUndoRedoService undoRedoService: IUndoRedoService, + @IConfigurationService configurationService: IConfigurationService, ) { super(); + const undoRedoPerCell = configurationService.getValue(UndoRedoPerCell); + this._register(undoRedoService.registerUriComparisonKeyComputer(CellUri.scheme, { getComparisonKey: (uri: URI): string => { - return getCellUndoRedoComparisonKey(uri); + return getCellUndoRedoComparisonKey(uri, undoRedoPerCell); } })); } @@ -265,7 +249,7 @@ class CellContentProvider implements ITextModelContentProvider { return cell.textBuffer.getLineContent(1).substr(0, limit); } }; - const language = cell.cellKind === CellKind.Markdown ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.textBuffer.getLineContent(1))); + const language = cell.cellKind === CellKind.Markup ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.textBuffer.getLineContent(1))); result = this._modelService.createModel( bufferFactory, language, @@ -276,7 +260,7 @@ class CellContentProvider implements ITextModelContentProvider { } if (result) { - const once = result.onWillDispose(() => { + const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { once.dispose(); ref.dispose(); }); @@ -286,29 +270,52 @@ class CellContentProvider implements ITextModelContentProvider { } } -class CellMetadataContentProvider implements ITextModelContentProvider { - private readonly _registration: IDisposable; +class CellInfoContentProvider { + private readonly _disposables: IDisposable[] = []; constructor( @ITextModelService textModelService: ITextModelService, @IModelService private readonly _modelService: IModelService, @IModeService private readonly _modeService: IModeService, + @ILabelService private readonly _labelService: ILabelService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, ) { - this._registration = textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellMetadata, this); + this._disposables.push(textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellMetadata, { + provideTextContent: this.provideMetadataTextContent.bind(this) + })); + + this._disposables.push(textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellOutput, { + provideTextContent: this.provideOutputTextContent.bind(this) + })); + + this._disposables.push(this._labelService.registerFormatter({ + scheme: Schemas.vscodeNotebookCellMetadata, + formatting: { + label: '${path} (metadata)', + separator: '/' + } + })); + + this._disposables.push(this._labelService.registerFormatter({ + scheme: Schemas.vscodeNotebookCellOutput, + formatting: { + label: '${path} (output)', + separator: '/' + } + })); } dispose(): void { - this._registration.dispose(); + this._disposables.forEach(d => d.dispose()); } - async provideTextContent(resource: URI): Promise { + async provideMetadataTextContent(resource: URI): Promise { const existing = this._modelService.getModel(resource); if (existing) { return existing; } - const data = CellUri.parseCellMetadataUri(resource); - // const data = parseCellUri(resource); + + const data = CellUri.parseCellUri(resource, Schemas.vscodeNotebookCellMetadata); if (!data) { return null; } @@ -320,7 +327,7 @@ class CellMetadataContentProvider implements ITextModelContentProvider { for (const cell of ref.object.notebook.cells) { if (cell.handle === data.handle) { - const metadataSource = getFormatedMetadataJSON(ref.object.notebook, cell.metadata || {}, cell.language); + const metadataSource = getFormatedMetadataJSON(ref.object.notebook, cell.metadata, cell.language); result = this._modelService.createModel( metadataSource, mode, @@ -339,6 +346,46 @@ class CellMetadataContentProvider implements ITextModelContentProvider { return result; } + + async provideOutputTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + + const data = CellUri.parseCellUri(resource, Schemas.vscodeNotebookCellOutput); + if (!data) { + return null; + } + + const ref = await this._notebookModelResolverService.resolve(data.notebook); + let result: ITextModel | null = null; + + const mode = this._modeService.create('json'); + + for (const cell of ref.object.notebook.cells) { + if (cell.handle === data.handle) { + const content = JSON.stringify(cell.outputs); + const edits = format(content, undefined, {}); + const outputSource = applyEdits(content, edits); + result = this._modelService.createModel( + outputSource, + mode, + resource + ); + break; + } + } + + if (result) { + const once = result.onWillDispose(() => { + once.dispose(); + ref.dispose(); + }); + } + + return result; + } } class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution { @@ -374,27 +421,37 @@ class RegisterSchemasContribution extends Disposable implements IWorkbenchContri } } -// makes sure that every dirty notebook gets an editor -class NotebookFileTracker implements IWorkbenchContribution { +class NotebookEditorManager implements IWorkbenchContribution { - private readonly _dirtyListener: IDisposable; + private readonly _disposables = new DisposableStore(); constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorModelResolverService private readonly _notebookEditorModelService: INotebookEditorModelResolverService, + @INotebookService notebookService: INotebookService, + @IEditorGroupsService editorGroups: IEditorGroupsService, ) { + // OPEN notebook editor for models that have turned dirty without being visible in an editor type E = IResolvedNotebookEditorModel; - this._dirtyListener = Event.debounce( + this._disposables.add(Event.debounce( this._notebookEditorModelService.onDidChangeDirty, (last, current) => !last ? [current] : [...last, current], 100 - )(this._openMissingDirtyNotebookEditors, this); + )(this._openMissingDirtyNotebookEditors, this)); + + // CLOSE notebook editor for models that have no more serializer + this._disposables.add(notebookService.onWillRemoveViewType(e => { + for (const group of editorGroups.groups) { + const staleInputs = group.editors.filter(input => input instanceof NotebookEditorInput && input.viewType === e); + group.closeEditors(staleInputs); + } + })); } dispose(): void { - this._dirtyListener.dispose(); + this._disposables.dispose(); } private _openMissingDirtyNotebookEditors(models: IResolvedNotebookEditorModel[]): void { @@ -413,7 +470,7 @@ class NotebookFileTracker implements IWorkbenchContribution { } } -class NotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { +class SimpleNotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -429,28 +486,62 @@ class NotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchC await this._extensionService.whenInstalledExtensionsRegistered(); this._register(this._workingCopyEditorService.registerHandler({ - handles: workingCopy => typeof this.getViewType(workingCopy) === 'string', - isOpen: (workingCopy, editor) => editor instanceof NotebookEditorInput && editor.viewType === this.getViewType(workingCopy), - createEditor: workingCopy => NotebookEditorInput.create(this._instantiationService, workingCopy.resource, this.getViewType(workingCopy)!) + handles: workingCopy => typeof this._getViewType(workingCopy) === 'string', + isOpen: (workingCopy, editor) => editor instanceof NotebookEditorInput && editor.viewType === this._getViewType(workingCopy), + createEditor: workingCopy => NotebookEditorInput.create(this._instantiationService, workingCopy.resource, this._getViewType(workingCopy)!) })); } - private getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { - if (workingCopy.typeId.startsWith(NOTEBOOK_WORKING_COPY_TYPE_PREFIX)) { - return workingCopy.typeId.substr(NOTEBOOK_WORKING_COPY_TYPE_PREFIX.length); - } + private _getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { + return NotebookWorkingCopyTypeIdentifier.parse(workingCopy.typeId); + } +} - return undefined; +class ComplexNotebookWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly _workingCopyEditorService: IWorkingCopyEditorService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService + ) { + super(); + + this._installHandler(); + } + + private async _installHandler(): Promise { + await this._extensionService.whenInstalledExtensionsRegistered(); + + this._register(this._workingCopyEditorService.registerHandler({ + handles: workingCopy => workingCopy.resource.scheme === Schemas.vscodeNotebook, + isOpen: (workingCopy, editor) => editor instanceof NotebookEditorInput && isEqual(URI.from({ scheme: Schemas.vscodeNotebook, path: editor.resource.toString() }), workingCopy.resource), + createEditor: async workingCopy => { + // TODO this is really bad and should adopt the `typeId` + // for backups instead of storing that information in the + // backup. + // But since complex notebooks are deprecated, not worth + // pushing for it and should eventually delete this code + // entirely. + const backup = await this._workingCopyBackupService.resolve(workingCopy); + if (!backup?.meta) { + throw new Error(`No backup found for Notebook editor: ${workingCopy.resource}`); + } + + return NotebookEditorInput.create(this._instantiationService, workingCopy.resource, backup.meta.viewType, { startDirty: true }); + } + })); } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); -workbenchContributionsRegistry.registerWorkbenchContribution(CellMetadataContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(CellInfoContentProvider, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookEditorManager, LifecyclePhase.Ready); +workbenchContributionsRegistry.registerWorkbenchContribution(SimpleNotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); +workbenchContributionsRegistry.registerWorkbenchContribution(ComplexNotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); registerSingleton(INotebookService, NotebookService); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); @@ -458,6 +549,45 @@ registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServ registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); registerSingleton(INotebookEditorService, NotebookEditorWidgetService, true); registerSingleton(INotebookKernelService, NotebookKernelService, true); +registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, true); + +const schemas: IJSONSchemaMap = {}; +function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema; }): x is IConfigurationPropertySchema { + return (typeof x.type !== 'undefined' || typeof x.anyOf !== 'undefined'); +} +for (const editorOption of editorOptionsRegistry) { + const schema = editorOption.schema; + if (schema) { + if (isConfigurationPropertySchema(schema)) { + schemas[`editor.${editorOption.name}`] = schema; + } else { + for (let key in schema) { + if (Object.hasOwnProperty.call(schema, key)) { + schemas[key] = schema[key]; + } + } + } + } +} + +const editorOptionsCustomizationSchema: IConfigurationPropertySchema = { + description: nls.localize('notebook.editorOptions.experimentalCustomization', 'Settings for code editors used in notebooks. This can be used to customize most editor.* settings.'), + default: {}, + allOf: [ + { + properties: schemas, + } + // , { + // patternProperties: { + // '^\\[.*\\]$': { + // type: 'object', + // default: {}, + // properties: schemas + // } + // } + // } + ] +}; const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ @@ -474,7 +604,7 @@ configurationRegistry.registerConfiguration({ }, default: [] }, - [CellToolbarLocKey]: { + [CellToolbarLocation]: { description: nls.localize('notebook.cellToolbarLocation.description', "Where the cell toolbar should be shown, or whether it should be hidden."), type: 'object', additionalProperties: { @@ -484,12 +614,19 @@ configurationRegistry.registerConfiguration({ }, default: { 'default': 'right' - } + }, + tags: ['notebookLayout'] }, - [ShowCellStatusBarKey]: { + [ShowCellStatusBar]: { description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell status bar should be shown."), - type: 'boolean', - default: true + type: 'string', + enum: ['hidden', 'visible', 'visibleAfterExecute'], + enumDescriptions: [ + nls.localize('notebook.showCellStatusbar.hidden.description', "The cell status bar is always hidden."), + nls.localize('notebook.showCellStatusbar.visible.description', "The cell status bar is always visible."), + nls.localize('notebook.showCellStatusbar.visibleAfterExecute.description', "The cell status bar is hidden until the cell has executed. Then it becomes visible to show the execution status.")], + default: 'visible', + tags: ['notebookLayout'] }, [NotebookTextDiffEditorPreview]: { description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."), @@ -501,5 +638,69 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, + [CellToolbarVisibility]: { + markdownDescription: nls.localize('notebook.cellToolbarVisibility.description', "Whether the cell toolbar should appear on hover or click."), + type: 'string', + enum: ['hover', 'click'], + default: 'click', + tags: ['notebookLayout'] + }, + [UndoRedoPerCell]: { + description: nls.localize('notebook.undoRedoPerCell.description', "Whether to use separate undo/redo stack for each cell."), + type: 'boolean', + default: false + }, + [CompactView]: { + description: nls.localize('notebook.compactView.description', "Control whether the notebook editor should be rendered in a compact form. "), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [FocusIndicator]: { + description: nls.localize('notebook.focusIndicator.description', "Control whether to render the focus indicator as cell borders or a highlight bar on the left gutter"), + type: 'string', + enum: ['border', 'gutter'], + default: 'gutter', + tags: ['notebookLayout'] + }, + [InsertToolbarLocation]: { + description: nls.localize('notebook.insertToolbarPosition.description', "Control where the insert cell actions should be rendered."), + type: 'string', + enum: ['betweenCells', 'notebookToolbar', 'both', 'hidden'], + default: 'both', + tags: ['notebookLayout'] + }, + [GlobalToolbar]: { + description: nls.localize('notebook.globalToolbar.description', "Control whether to render a global toolbar inside the notebook editor."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [ConsolidatedOutputButton]: { + description: nls.localize('notebook.consolidatedOutputButton.description', "Control whether outputs action should be rendered in the output toolbar."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [ShowFoldingControls]: { + description: nls.localize('notebook.showFoldingControls.description', "Controls when the folding controls are shown."), + type: 'string', + enum: ['always', 'mouseover'], + default: 'mouseover', + tags: ['notebookLayout'] + }, + [DragAndDropEnabled]: { + description: nls.localize('notebook.dragAndDrop.description', "Control whether the notebook editor should allow moving cells through drag and drop."), + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [ConsolidatedRunButton]: { + description: nls.localize('notebook.consolidatedRunButton.description', "Control whether extra actions are shown in a dropdown next to the run button."), + type: 'boolean', + default: false, + tags: ['notebookLayout'] + }, + [NotebookCellEditorOptionsCustomizations]: editorOptionsCustomizationSchema } }); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 01ea269fc931..ce0d5b5f2665 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -8,7 +8,7 @@ import { IListContextMenuEvent, IListEvent, IListMouseEvent } from 'vs/base/brow import { IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; @@ -16,26 +16,28 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; +import { FindMatch, IModelDeltaDecoration, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookKernel, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookKernel, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto, INotebookCellStatusBarItem, NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; -import { EditorOptions, IEditorPane } from 'vs/workbench/common/editor'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { INotebookWebviewMessage } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; //#region Context Keys +export const HAS_OPENED_NOTEBOOK = new RawContextKey('userHasOpenedNotebook', false); export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); // Is Notebook @@ -48,10 +50,11 @@ export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCe export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); +export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = new RawContextKey('notebookUseConsolidatedOutputButton', false); // Cell keys -export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); -export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown +export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookType', undefined); +export const NOTEBOOK_CELL_TYPE = new RawContextKey<'code' | 'markup'>('notebookCellType', undefined); export const NOTEBOOK_CELL_EDITABLE = new RawContextKey('notebookCellEditable', false); // bool export const NOTEBOOK_CELL_FOCUSED = new RawContextKey('notebookCellFocused', false); // bool export const NOTEBOOK_CELL_EDITOR_FOCUSED = new RawContextKey('notebookCellEditorFocused', false); // bool @@ -64,8 +67,11 @@ export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('noteboo export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool // Kernels export const NOTEBOOK_KERNEL_COUNT = new RawContextKey('notebookKernelCount', 0); +export const NOTEBOOK_KERNEL_SELECTED = new RawContextKey('notebookKernelSelected', false); export const NOTEBOOK_INTERRUPTIBLE_KERNEL = new RawContextKey('notebookInterruptibleKernel', false); +export const NOTEBOOK_HAS_OUTPUTS = new RawContextKey('notebookHasOutputs', false); + //#endregion //#region Shared commands @@ -87,6 +93,7 @@ export interface IRenderMainframeOutput { type: RenderOutputType.Mainframe; supportAppend?: boolean; initHeight?: number; + disposable?: IDisposable; } export interface IRenderPlainHtmlOutput { @@ -112,14 +119,14 @@ export interface ICellOutputViewModel { */ model: ICellOutput; resolveMimeTypes(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined): [readonly IOrderedMimeType[], number]; - pickedMimeType: number; + pickedMimeType: IOrderedMimeType | undefined; supportAppend(): boolean; + hasMultiMimeType(): boolean; toRawJSON(): any; } export interface IDisplayOutputViewModel extends ICellOutputViewModel { resolveMimeTypes(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined): [readonly IOrderedMimeType[], number]; - pickedMimeType: number; } @@ -131,7 +138,7 @@ export interface IGenericCellViewModel { id: string; handle: number; uri: URI; - metadata: NotebookCellMetadata | undefined; + metadata: NotebookCellMetadata; outputIsHovered: boolean; outputIsFocused: boolean; outputsViewModels: ICellOutputViewModel[]; @@ -168,16 +175,16 @@ export interface ICommonNotebookEditor { triggerScroll(event: IMouseWheelEvent): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; getCellById(cellId: string): IGenericCellViewModel | undefined; - toggleNotebookCellSelection(cell: IGenericCellViewModel): void; + toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean): void; setMarkdownCellEditState(cellId: string, editState: CellEditState): void; - markdownCellDragStart(cellId: string, position: { clientY: number }): void; - markdownCellDrag(cellId: string, position: { clientY: number }): void; - markdownCellDrop(cellId: string, position: { clientY: number, ctrlKey: boolean, altKey: boolean }): void; + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }): void; markdownCellDragEnd(cellId: string): void; } @@ -230,6 +237,7 @@ export interface MarkdownCellLayoutInfo { readonly fontInfo: FontInfo | null; readonly editorWidth: number; readonly editorHeight: number; + readonly previewHeight: number; readonly bottomToolbarOffset: number; readonly totalHeight: number; } @@ -237,6 +245,8 @@ export interface MarkdownCellLayoutInfo { export interface MarkdownCellLayoutChangeEvent { font?: FontInfo; outerWidth?: number; + editorHeight?: number; + previewHeight?: number; totalHeight?: number; } @@ -248,6 +258,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly onDidChangeLayout: Event<{ totalHeight?: boolean | number; outerWidth?: number; }>; readonly onDidChangeCellStatusBarItems: Event; readonly editStateSource: string; + readonly editorAttached: boolean; dragging: boolean; handle: number; uri: URI; @@ -259,7 +270,8 @@ export interface ICellViewModel extends IGenericCellViewModel { getText(): string; getTextLength(): number; getHeight(lineHeight: number): number; - metadata: NotebookCellMetadata | undefined; + metadata: NotebookCellMetadata; + internalMetadata: NotebookCellInternalMetadata; textModel: ITextModel | undefined; hasModel(): this is IEditableCellViewModel; resolveTextModel(): Promise; @@ -268,6 +280,8 @@ export interface ICellViewModel extends IGenericCellViewModel { getCellStatusBarItems(): INotebookCellStatusBarItem[]; getEditState(): CellEditState; updateEditState(state: CellEditState, source: string): void; + deltaModelDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; + getCellDecorationRange(id: string): Range | null; } export interface IEditableCellViewModel extends ICellViewModel { @@ -311,23 +325,10 @@ export interface INotebookDeltaCellStatusBarItems { items: INotebookCellStatusBarItem[]; } -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; +export interface INotebookEditorOptions extends ITextEditorOptions { + readonly cellOptions?: ITextResourceEditorInput; readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - this.cellSelections = options.cellSelections; - this.isReadOnly = options.isReadOnly; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } } export type INotebookEditorContributionCtor = IConstructorSignature1; @@ -344,6 +345,7 @@ export interface INotebookEditorCreationOptions { export interface IActiveNotebookEditor extends INotebookEditor { viewModel: NotebookViewModel; + textModel: NotebookTextModel; getFocus(): ICellRange; } @@ -377,6 +379,7 @@ export interface INotebookEditor extends ICommonNotebookEditor { readonly onDidScroll: Event; readonly onDidChangeActiveCell: Event; + readonly notebookOptions: NotebookOptions; isDisposed: boolean; dispose(): void; @@ -395,7 +398,7 @@ export interface INotebookEditor extends ICommonNotebookEditor { hasWebviewFocus(): boolean; hasOutputTextSelection(): boolean; - setOptions(options: NotebookEditorOptions | undefined): Promise; + setOptions(options: INotebookEditorOptions | undefined): Promise; /** * Select & focus cell @@ -498,7 +501,7 @@ export interface INotebookEditor extends ICommonNotebookEditor { /** * Send message to the webview for outputs. */ - postMessage(forRendererId: string | undefined, message: any): void; + postMessage(message: any): void; /** * Remove class name on the notebook editor root DOM node. @@ -761,7 +764,7 @@ export interface IOutputTransformContribution { * This call is allowed to have side effects, such as placing output * directly into the container element. */ - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput; + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput; } export interface CellFindMatch { @@ -769,6 +772,12 @@ export interface CellFindMatch { matches: FindMatch[]; } +export interface CellFindMatchWithIndex { + cell: CellViewModel; + index: number; + matches: FindMatch[]; +} + export enum CellRevealType { Line, Range @@ -809,6 +818,7 @@ export enum CursorAtBoundary { export interface CellViewModelStateChangeEvent { readonly metadataChanged?: boolean; + readonly internalMetadataChanged?: boolean; readonly runStateChanged?: boolean; readonly selectionChanged?: boolean; readonly focusModeChanged?: boolean; @@ -897,20 +907,6 @@ export function getNotebookEditorFromEditorPane(editorPane?: IEditorPane): INote return editorPane?.getId() === NOTEBOOK_EDITOR_ID ? editorPane.getControl() as INotebookEditor | undefined : undefined; } -let EDITOR_TOP_PADDING = 12; -const editorTopPaddingChangeEmitter = new Emitter(); - -export const EditorTopPaddingChangeEvent = editorTopPaddingChangeEmitter.event; - -export function updateEditorTopPadding(top: number) { - EDITOR_TOP_PADDING = top; - editorTopPaddingChangeEmitter.fire(); -} - -export function getEditorTopPadding() { - return EDITOR_TOP_PADDING; -} - /** * ranges: model selections * this will convert model selections to view indexes first, and then include the hidden ranges in the list view diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts index 05d2a1fc52a4..cb27b64cd422 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts @@ -10,7 +10,6 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { URI } from 'vs/base/common/uri'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { score } from 'vs/workbench/contrib/notebook/common/notebookSelector'; export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { @@ -43,7 +42,7 @@ export class NotebookCellStatusBarService extends Disposable implements INoteboo } async getStatusBarItemsForCell(docUri: URI, cellIndex: number, viewType: string, token: CancellationToken): Promise { - const providers = this._providers.filter(p => score(p.selector, docUri, viewType) > 0); + const providers = this._providers.filter(p => p.viewType === viewType || p.viewType === '*'); return await Promise.all(providers.map(async p => { try { return await p.provideCellStatusBarItems(docUri, cellIndex, token) ?? { items: [] }; diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts index 7151038410aa..b8a16e1cf8bd 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; -import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorModel } from 'vs/workbench/common/editor'; +import { IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; @@ -14,6 +16,7 @@ import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebo import { IReference } from 'vs/base/common/lifecycle'; import { INotebookDiffEditorModel, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Schemas } from 'vs/base/common/network'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; interface NotebookEditorInputOptions { startDirty?: boolean; @@ -68,7 +71,8 @@ export class NotebookDiffEditorInput extends EditorInput { public readonly options: NotebookEditorInputOptions, @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IFileService private readonly _fileService: IFileService ) { super(); this._defaultDirtyState = !!options.startDirty; @@ -78,6 +82,26 @@ export class NotebookDiffEditorInput extends EditorInput { return NotebookDiffEditorInput.ID; } + override get capabilities(): EditorInputCapabilities { + let capabilities = EditorInputCapabilities.None; + + if (this._modifiedTextModel?.object.resource.scheme === Schemas.untitled) { + capabilities |= EditorInputCapabilities.Untitled; + } + + if (this._modifiedTextModel) { + if (this._modifiedTextModel.object.isReadonly()) { + capabilities |= EditorInputCapabilities.Readonly; + } + } else { + if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.Readonly; + } + } + + return capabilities; + } + override getName(): string { return this.textDiffName; } @@ -89,18 +113,10 @@ export class NotebookDiffEditorInput extends EditorInput { return this._modifiedTextModel.object.isDirty(); } - override isUntitled(): boolean { - return this._modifiedTextModel?.object.resource.scheme === Schemas.untitled; - } - - override isReadonly() { - return false; - } - override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this._modifiedTextModel) { - if (this.isUntitled()) { + if (this.hasCapability(EditorInputCapabilities.Untitled)) { return this.saveAs(group, options); } else { await this._modifiedTextModel.object.save(); @@ -117,7 +133,7 @@ export class NotebookDiffEditorInput extends EditorInput { return undefined; } - const provider = this._notebookService.getContributedNotebookProvider(this.viewType!); + const provider = this._notebookService.getContributedNotebookType(this.viewType!); if (!provider) { return undefined; @@ -158,7 +174,7 @@ ${patterns} // called when users rename a notebook document override rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { if (this._modifiedTextModel) { - const contributedNotebookProviders = this._notebookService.getContributedNotebookProviders(target); + const contributedNotebookProviders = this._notebookService.getContributedNotebookTypes(target); if (contributedNotebookProviders.find(provider => provider.id === this._modifiedTextModel!.object.viewType)) { return this._move(group, target); @@ -199,8 +215,18 @@ ${patterns} return new NotebookDiffEditorModel(this._originalTextModel.object, this._modifiedTextModel.object); } + override asResourceEditorInput(group: GroupIdentifier): IResourceDiffEditorInput { + return { + originalInput: { resource: this.originalResource }, + modifiedInput: { resource: this.resource }, + options: { + override: this.viewType + } + }; + } + override matches(otherInput: unknown): boolean { - if (this === otherInput) { + if (super.matches(otherInput)) { return true; } if (otherInput instanceof NotebookDiffEditorInput) { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 989cec381bc4..f88c3f92d2de 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/notebook'; import { localize } from 'vs/nls'; import { extname } from 'vs/base/common/resources'; @@ -18,17 +18,21 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { NotebookEditorOptions, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditorOptions, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { clearMarks, getAndClearMarks, mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { IFileService } from 'vs/platform/files/common/files'; +import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction } from 'vs/base/common/actions'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -37,11 +41,13 @@ export class NotebookEditor extends EditorPane { private readonly _editorMemento: IEditorMemento; private readonly _groupListener = this._register(new DisposableStore()); - private readonly _widgetDisposableStore: DisposableStore = new DisposableStore(); + private readonly _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); private _widget: IBorrowValue = { value: undefined }; private _rootElement!: HTMLElement; private _dimension?: DOM.Dimension; + private readonly inputListener = this._register(new MutableDisposable()); + // todo@rebornix is there a reason that `super.fireOnDidFocus` isn't used? private readonly _onDidFocusWidget = this._register(new Emitter()); override get onDidFocus(): Event { return this._onDidFocusWidget.event; } @@ -65,13 +71,25 @@ export class NotebookEditor extends EditorPane { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); + } + + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input instanceof NotebookEditorInput && this.input.resource?.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: NotebookEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } } - private onDidFileSystemProviderChange(scheme: string): void { - if (this.input?.resource?.scheme === scheme && this._widget.value) { - this._widget.value.setOptions(new NotebookEditorOptions({ isReadOnly: this.input.isReadonly() })); + private updateReadonly(input: NotebookEditorInput): void { + if (this._widget.value) { + this._widget.value.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } } @@ -103,6 +121,14 @@ export class NotebookEditor extends EditorPane { return this._rootElement; } + override getActionViewItem(action: IAction): IActionViewItem | undefined { + if (action.id === SELECT_KERNEL_ID) { + // this is being disposed by the consumer + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this); + } + return undefined; + } + override getControl(): NotebookEditorWidget | undefined { return this._widget.value; } @@ -140,11 +166,13 @@ export class NotebookEditor extends EditorPane { return !!value && (DOM.isAncestor(activeElement, value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode()))); } - override async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { clearMarks(input.resource); mark(input.resource, 'startTime'); const group = this.group!; + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + this._saveEditorViewState(this.input); this._widgetDisposableStore.clear(); @@ -193,8 +221,8 @@ export class NotebookEditor extends EditorPane { this._widget.value?.setParentContextKeyService(this._contextKeyService); await this._widget.value!.setModel(model.notebook, viewState); - const isReadonly = input.isReadonly(); - await this._widget.value!.setOptions(options instanceof NotebookEditorOptions ? options.with({ isReadOnly: isReadonly }) : new NotebookEditorOptions({ isReadOnly: isReadonly })); + const isReadOnly = input.hasCapability(EditorInputCapabilities.Readonly); + await this._widget.value!.setOptions({ ...options, isReadOnly }); this._widgetDisposableStore.add(this._widget.value!.onDidFocus(() => this._onDidFocusWidget.fire())); this._widgetDisposableStore.add(this._editorDropService.createEditorDropTarget(this._widget.value!.getDomNode(), { @@ -258,6 +286,8 @@ export class NotebookEditor extends EditorPane { } override clearInput(): void { + this.inputListener.clear(); + if (this._widget.value) { this._saveEditorViewState(this.input); this._widget.value.onWillHide(); @@ -265,10 +295,8 @@ export class NotebookEditor extends EditorPane { super.clearInput(); } - override setOptions(options: EditorOptions | undefined): void { - if (options instanceof NotebookEditorOptions) { - this._widget.value?.setOptions(options); - } + override setOptions(options: INotebookEditorOptions | undefined): void { + this._widget.value?.setOptions(options); super.setOptions(options); } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts index 597865448d1a..2f97e8579929 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts @@ -6,10 +6,11 @@ import * as nls from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, INotebookKernel, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookKernel, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; export class NotebookEditorKernelManager extends Disposable { @@ -24,32 +25,19 @@ export class NotebookEditorKernelManager extends Disposable { getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined { // returns SELECTED or the ONLY available kernel const info = this._notebookKernelService.getMatchingKernel(notebook); - if (info.selected) { - return info.selected; - } - if (info.all.length === 1) { - return info.all[0]; - } - return undefined; + return info.selected ?? info.suggested; } async executeNotebookCells(notebook: INotebookTextModel, cells: Iterable): Promise { const message = nls.localize('notebookRunTrust', "Executing a notebook cell will run code from this workspace."); - const trust = await this._workspaceTrustRequestService.requestWorkspaceTrust({ - modal: true, - message - }); + const trust = await this._workspaceTrustRequestService.requestWorkspaceTrust({ message }); if (!trust) { return; } - if (!notebook.metadata.trusted) { - return; - } - let kernel = this.getSelectedOrSuggestedKernel(notebook); if (!kernel) { - await this._commandService.executeCommand('notebook.selectKernel'); + await this._commandService.executeCommand(SELECT_KERNEL_ID); kernel = this.getSelectedOrSuggestedKernel(notebook); } @@ -59,7 +47,7 @@ export class NotebookEditorKernelManager extends Disposable { const cellHandles: number[] = []; for (const cell of cells) { - if (cell.cellKind !== CellKind.Code) { + if (cell.cellKind !== CellKind.Code || cell.internalMetadata.runState === NotebookCellExecutionState.Pending || cell.internalMetadata.runState === NotebookCellExecutionState.Executing) { continue; } if (!kernel.supportedLanguages.includes(cell.language)) { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts index 17bcdd78ecd6..c48fcbcf6f8e 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts @@ -52,6 +52,7 @@ export class NotebookEditorWidgetService implements INotebookEditorService { value.token = undefined; this._disposeWidget(value.widget); widgets.delete(e.editor.resource); + value.widget = (undefined); // unset the widget so that others that still hold a reference don't harm us })); listeners.push(group.onWillMoveEditor(e => { if (e.editor instanceof NotebookEditorInput) { diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts new file mode 100644 index 000000000000..424eecf4304c --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { toolbarActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; +import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { GlobalToolbar } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; + +export class NotebookEditorToolbar extends Disposable { + // private _editorToolbarContainer!: HTMLElement; + private _leftToolbarScrollable!: DomScrollableElement; + private _notebookTopLeftToolbarContainer!: HTMLElement; + private _notebookTopRightToolbarContainer!: HTMLElement; + private _notebookGlobalActionsMenu!: IMenu; + private _notebookLeftToolbar!: ToolBar; + private _notebookRightToolbar!: ToolBar; + private _useGlobalToolbar: boolean = false; + + private readonly _onDidChangeState = this._register(new Emitter()); + onDidChangeState: Event = this._onDidChangeState.event; + + get useGlobalToolbar(): boolean { + return this._useGlobalToolbar; + } + + private _pendingLayout: IDisposable | undefined; + + constructor( + readonly notebookEditor: INotebookEditor, + readonly contextKeyService: IContextKeyService, + readonly domNode: HTMLElement, + @IInstantiationService readonly instantiationService: IInstantiationService, + @IConfigurationService readonly configurationService: IConfigurationService, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IEditorService private readonly editorService: IEditorService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + ) { + super(); + + this._buildBody(); + + this._register(this.editorService.onDidActiveEditorChange(() => { + if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { + const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; + if (notebookEditor === this.notebookEditor) { + // this is the active editor + this._showNotebookActionsinEditorToolbar(); + return; + } + } + })); + + this._reigsterNotebookActionsToolbar(); + } + + private _buildBody() { + this._notebookTopLeftToolbarContainer = document.createElement('div'); + this._notebookTopLeftToolbarContainer.classList.add('notebook-toolbar-left'); + this._leftToolbarScrollable = new DomScrollableElement(this._notebookTopLeftToolbarContainer, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Auto, + horizontalScrollbarSize: 3, + useShadows: false, + scrollYToX: true + }); + this._register(this._leftToolbarScrollable); + + DOM.append(this.domNode, this._leftToolbarScrollable.getDomNode()); + this._notebookTopRightToolbarContainer = document.createElement('div'); + this._notebookTopRightToolbarContainer.classList.add('notebook-toolbar-right'); + DOM.append(this.domNode, this._notebookTopRightToolbarContainer); + } + + private _reigsterNotebookActionsToolbar() { + const cellMenu = this.instantiationService.createInstance(CellMenus); + this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.contextKeyService)); + this._register(this._notebookGlobalActionsMenu); + + this._useGlobalToolbar = this.configurationService.getValue(GlobalToolbar) ?? false; + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(GlobalToolbar)) { + this._useGlobalToolbar = this.configurationService.getValue(GlobalToolbar); + this._showNotebookActionsinEditorToolbar(); + } + })); + + const context = { + ui: true, + notebookEditor: this.notebookEditor + }; + + const actionProvider = (action: IAction) => { + if (action.id === SELECT_KERNEL_ID) { + // // this is being disposed by the consumer + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + } + + return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action) : undefined; + }; + + this._notebookLeftToolbar = new ToolBar(this._notebookTopLeftToolbarContainer, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: actionProvider, + renderDropdownAsChildElement: true + }); + this._register(this._notebookLeftToolbar); + this._notebookLeftToolbar.context = context; + + this._notebookRightToolbar = new ToolBar(this._notebookTopRightToolbarContainer, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: actionProvider, + renderDropdownAsChildElement: true + }); + this._register(this._notebookRightToolbar); + this._notebookRightToolbar.context = context; + + this._showNotebookActionsinEditorToolbar(); + this._register(this._notebookGlobalActionsMenu.onDidChange(() => { + this._showNotebookActionsinEditorToolbar(); + })); + + if (this.experimentService) { + this.experimentService.getTreatment('nbtoolbarineditor').then(treatment => { + if (treatment === undefined) { + return; + } + if (this._useGlobalToolbar !== treatment) { + this._useGlobalToolbar = treatment; + this._showNotebookActionsinEditorToolbar(); + } + }); + } + } + + private _showNotebookActionsinEditorToolbar() { + // when there is no view model, just ignore. + if (!this.notebookEditor.hasModel()) { + return; + } + + if (!this._useGlobalToolbar) { + this.domNode.style.display = 'none'; + } else { + this._notebookLeftToolbar.setActions([], []); + const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true, renderShortTitle: true }); + this.domNode.style.display = 'flex'; + const primaryLeftGroups = groups.filter(group => /^navigation/.test(group[0])); + let primaryActions: IAction[] = []; + primaryLeftGroups.sort((a, b) => { + if (a[0] === 'navigation') { + return 1; + } + + if (b[0] === 'navigation') { + return -1; + } + + return 0; + }).forEach((group, index) => { + primaryActions.push(...group[1]); + if (index < primaryLeftGroups.length - 1) { + primaryActions.push(new Separator()); + } + }); + const primaryRightGroup = groups.find(group => /^status/.test(group[0])); + const primaryRightActions = primaryRightGroup ? primaryRightGroup[1] : []; + const secondaryActions = groups.filter(group => !/^navigation/.test(group[0]) && !/^status/.test(group[0])).reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []); + + this._notebookLeftToolbar.setActions(primaryActions, secondaryActions); + this._notebookRightToolbar.setActions(primaryRightActions, []); + this._updateScrollbar(); + } + + this._onDidChangeState.fire(); + } + + layout() { + this._updateScrollbar(); + } + + private _updateScrollbar() { + this._pendingLayout?.dispose(); + + this._pendingLayout = DOM.measure(() => { + DOM.measure(() => { // double RAF + this._leftToolbarScrollable.setRevealOnScroll(false); + this._leftToolbarScrollable.scanDomNode(); + this._leftToolbarScrollable.setRevealOnScroll(true); + }); + }); + } + + override dispose() { + this._pendingLayout?.dispose(); + super.dispose(); + } +} + +registerThemingParticipant((theme, collector) => { + const toolbarActiveBackgroundColor = theme.getColor(toolbarActiveBackground); + if (toolbarActiveBackgroundColor) { + collector.addRule(` + .monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: ${toolbarActiveBackgroundColor}; + } + `); + } +}); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 65e8ed9b1be2..5f94dbb1c9fe 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -25,12 +25,12 @@ import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; -import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -42,8 +42,7 @@ import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_OUTPUT_PADDING, CELL_RIGHT_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, MARKDOWN_CELL_BOTTOM_MARGIN, MARKDOWN_CELL_TOP_MARGIN, MARKDOWN_PREVIEW_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, INotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookDecorationCSSRules, NotebookRefCountedStyleSheet } from 'vs/workbench/contrib/notebook/browser/notebookEditorDecorations'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager'; @@ -59,22 +58,20 @@ import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbenc import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellToolbarLocKey, ExperimentalUseMarkdownRenderer, SelectionStateType, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ExperimentalUseMarkdownRenderer, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isWeb } from 'vs/base/common/platform'; import { mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; +import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/notebookEditorToolbar'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; const $ = DOM.$; @@ -202,17 +199,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private static readonly EDITOR_MEMENTOS = new Map>(); private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; + private _notebookTopToolbar!: NotebookEditorToolbar; private _body!: HTMLElement; + private _styleElement!: HTMLStyleElement; private _overflowContainer!: HTMLElement; private _webview: BackLayerWebView | null = null; private _webviewResolvePromise: Promise | null> | null = null; private _webviewTransparentCover: HTMLElement | null = null; + private _listDelegate: NotebookCellListDelegate | null = null; private _list!: INotebookCellList; private _listViewInfoAccessor!: ListViewInfoAccessor; private _dndController: CellDragAndDropController | null = null; private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); - private _eventDispatcher: NotebookEventDispatcher | undefined; + private _viewContext: ViewContext; private _notebookViewModel: NotebookViewModel | undefined; private _localStore: DisposableStore = this._register(new DisposableStore()); private _localCellStateListeners: DisposableStore[] = []; @@ -305,15 +305,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; + private readonly _notebookOptions: NotebookOptions; + + get notebookOptions() { + return this._notebookOptions; + } constructor( readonly creationOptions: INotebookEditorCreationOptions, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IAccessibilityService accessibilityService: IAccessibilityService, + @INotebookRendererMessagingService private readonly notebookRendererMessaging: INotebookRendererMessagingService, @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, - @INotebookKernelService notebookKernelService: INotebookKernelService, - @IEditorService private readonly editorService: IEditorService, + @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @ILayoutService private readonly layoutService: ILayoutService, @@ -321,14 +326,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IMenuService private readonly menuService: IMenuService, @IThemeService private readonly themeService: IThemeService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IModeService private readonly modeService: IModeService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + @IModeService private readonly modeService: IModeService ) { super(); this.isEmbedded = creationOptions.isEmbedded || false; - this.useRenderer = !isWeb && !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); + this.useRenderer = !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); + this._notebookOptions = new NotebookOptions(this.configurationService); + this._register(this._notebookOptions); + this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); this._overlayContainer = document.createElement('div'); this.scopedContextKeyService = contextKeyService.createScoped(this._overlayContainer); @@ -345,21 +351,33 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._memento = new Memento(NOTEBOOK_EDITOR_ID, storageService); - this._outputRenderer = new OutputRenderer(this, this.instantiationService); + this._outputRenderer = this._register(new OutputRenderer(this, this.instantiationService)); this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); if (this._dimension && this._isVisible) { this.layout(this._dimension); } } + })); - if (e.affectsConfiguration(CellToolbarLocKey) || e.affectsConfiguration(ShowCellStatusBarKey)) { + this._register(this._notebookOptions.onDidChangeOptions(e => { + if (e.cellStatusBarVisibility || e.cellToolbarLocation || e.cellToolbarInteraction) { this._updateForNotebookConfiguration(); } - }); + + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.insertToolbarAlignment) { + this._styleElement?.remove(); + this._createLayoutStyles(); + this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + } + + if (this._dimension && this._isVisible) { + this.layout(this._dimension); + } + })); this.notebookEditorService.addNotebookEditor(this); @@ -384,12 +402,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); } for (const desc of contributions) { + let contribution: INotebookEditorContribution | undefined; try { - const contribution = this.instantiationService.createInstance(desc.ctor, this); - this._contributions.set(desc.id, contribution); + contribution = this.instantiationService.createInstance(desc.ctor, this); } catch (err) { onUnexpectedError(err); } + if (contribution) { + if (!this._contributions.has(desc.id)) { + this._contributions.set(desc.id, contribution); + } else { + contribution.dispose(); + throw new Error(`DUPLICATE notebook editor contribution: '${desc.id}'`); + } + } } this._updateForNotebookConfiguration(); @@ -479,41 +505,22 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); this._overlayContainer.classList.remove('cell-title-toolbar-left'); this._overlayContainer.classList.remove('cell-title-toolbar-right'); this._overlayContainer.classList.remove('cell-title-toolbar-hidden'); + const cellToolbarLocation = this._notebookOptions.computeCellToolbarLocation(this.viewModel?.viewType); + this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); - if (typeof cellToolbarLocation === 'string') { - if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right' || cellToolbarLocation === 'hidden') { - this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); - } - } else { - if (this.viewModel) { - const notebookSpecificSetting = cellToolbarLocation[this.viewModel.viewType] ?? cellToolbarLocation['default']; - let cellToolbarLocationForCurrentView = 'right'; - - switch (notebookSpecificSetting) { - case 'left': - cellToolbarLocationForCurrentView = 'left'; - break; - case 'right': - cellToolbarLocationForCurrentView = 'right'; - case 'hidden': - cellToolbarLocationForCurrentView = 'hidden'; - default: - cellToolbarLocationForCurrentView = 'right'; - break; - } + const cellToolbarInteraction = this._notebookOptions.getLayoutConfiguration().cellToolbarInteraction; + let cellToolbarInteractionState = 'hover'; + this._overlayContainer.classList.remove('cell-toolbar-hover'); + this._overlayContainer.classList.remove('cell-toolbar-click'); - this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocationForCurrentView}`); - } else { - this._overlayContainer.classList.add(`cell-title-toolbar-right`); - } + if (cellToolbarInteraction === 'hover' || cellToolbarInteraction === 'click') { + cellToolbarInteractionState = cellToolbarInteraction; } + this._overlayContainer.classList.add(`cell-toolbar-${cellToolbarInteractionState}`); - const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); - this._overlayContainer.classList.toggle('cell-statusbar-hidden', !showCellStatusBar); } private _generateFontInfo(): void { @@ -523,19 +530,248 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _createBody(parent: HTMLElement): void { this._notebookTopToolbarContainer = document.createElement('div'); - this._notebookTopToolbarContainer.classList.add('notebook-top-toolbar'); + this._notebookTopToolbarContainer.classList.add('notebook-toolbar-container'); this._notebookTopToolbarContainer.style.display = 'none'; DOM.append(parent, this._notebookTopToolbarContainer); this._body = document.createElement('div'); + DOM.append(parent, this._body); this._body.classList.add('cell-list-container'); + this._createLayoutStyles(); this._createCellList(); - DOM.append(parent, this._body); this._overflowContainer = document.createElement('div'); this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); } + private _createLayoutStyles(): void { + this._styleElement = DOM.createStyleSheet(this._body); + const { + cellRightMargin, + cellTopMargin, + cellRunGutter, + cellBottomMargin, + codeCellLeftMargin, + markdownCellGutter, + markdownCellLeftMargin, + markdownCellBottomMargin, + markdownCellTopMargin, + // bottomToolbarGap: bottomCellToolbarGap, + // bottomToolbarHeight: bottomCellToolbarHeight, + collapsedIndicatorHeight, + compactView, + focusIndicator, + insertToolbarPosition, + insertToolbarAlignment, + fontSize, + focusIndicatorLeftMargin + } = this._notebookOptions.getLayoutConfiguration(); + + const { bottomToolbarGap, bottomToolbarHeight } = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); + + const styleSheets: string[] = []; + + styleSheets.push(` + :root { + --notebook-cell-output-font-size: ${fontSize}px; + } + `); + + if (compactView) { + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); + } else { + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin}px; }`); + } + + // focus indicator + if (focusIndicator === 'border') { + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { + content: ""; + position: absolute; + width: 100%; + height: 1px; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 1px; + height: 100%; + z-index: 10; + } + + /* top border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { + border-top: 1px solid transparent; + } + + /* left border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + border-left: 1px solid transparent; + } + + /* bottom border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { + border-bottom: 1px solid transparent; + } + + /* right border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + border-right: 1px solid transparent; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) + }`); + } else { + // gutter + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: 0px; height: 100%; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.selected .cell-focus-indicator-left:before { + border-left: 3px solid transparent; + border-radius: 2px; + margin-left: ${focusIndicatorLeftMargin}px; + }`); + + // boder should always show + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container .cell-focus-indicator-left:before { + border-color: var(--notebook-focused-cell-border-color) !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-inner-container .cell-focus-indicator-left:before { + border-color: var(--notebook-inactive-focused-cell-border-color) !important; + } + `); + } + + // between cell insert toolbar + if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: flex; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: flex; }`); + } else { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: none; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: none; }`); + } + + if (insertToolbarAlignment === 'left') { + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { + margin-right: 0px !important; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { + padding: 0px !important; + justify-content: center; + border-radius: 4px; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { + align-items: flex-start; + justify-content: left; + margin: 0 16px 0 ${8 + codeCellLeftMargin}px; + }`); + + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, + .notebookOverlay .cell-bottom-toolbar-container .action-item { + border: 0px; + }`); + } + + // top insert toolbar + const topInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${topInsertToolbarHeight}px }`); + styleSheets.push(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { + padding-top: ${topInsertToolbarHeight}px; + box-sizing: border-box; + }`); + + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${markdownCellBottomMargin}px; padding-top: ${markdownCellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container.webview-backed-markdown-cell { padding: 0; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell.markdown-cell-edit-mode .cell.code { padding-bottom: ${markdownCellBottomMargin}px; padding-top: ${markdownCellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .output { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .output { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); + + // output toolbar + styleSheets.push(`.monaco-workbench .notebookOverlay .output .cell-output-toolbar { left: -${cellRunGutter}px; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay .output .cell-output-toolbar { width: ${cellRunGutter}px; }`); + + styleSheets.push(`.notebookOverlay .output-show-more-container { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .output-show-more-container { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); + styleSheets.push(`.notebookOverlay .cell .run-button-container { width: ${cellRunGutter}px; left: ${codeCellLeftMargin}px }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .execution-count-label { left: ${codeCellLeftMargin}px; width: ${cellRunGutter}px; }`); + + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${cellRunGutter}px; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { left: ${(markdownCellGutter - 20) / 2 + markdownCellLeftMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row :not(.webview-backed-markdown-cell) .cell-focus-indicator-top { height: ${cellTopMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${bottomToolbarGap}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left, + .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-drag-handle { width: ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${codeCellLeftMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${cellRightMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${cellBottomMargin}px; }`); + styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); + + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${codeCellLeftMargin + cellRunGutter}px; height: ${collapsedIndicatorHeight}px; }`); + + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${bottomToolbarHeight}px }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { height: ${bottomToolbarHeight}px }`); + + // cell toolbar + styleSheets.push(`.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + right: ${cellRightMargin + 26}px; + } + .monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + left: ${codeCellLeftMargin + cellRunGutter + 16}px; + } + .monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + display: none; + }`); + + this._styleElement.textContent = styleSheets.join('\n'); + } + private _createCellList(): void { this._body.classList.add('cell-list-container'); @@ -546,12 +782,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.instantiationService.createInstance(MarkdownCellRenderer, this, this._dndController, this._renderedEditors, getScopedContextKeyService, { useRenderer: this.useRenderer }), ]; + renderers.forEach(renderer => { + this._register(renderer); + }); + + this._listDelegate = this.instantiationService.createInstance(NotebookCellListDelegate); + this._register(this._listDelegate); + this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', this._overlayContainer, this._body, - this.instantiationService.createInstance(NotebookCellListDelegate), + this._viewContext, + this._listDelegate, renderers, this.scopedContextKeyService, { @@ -592,7 +836,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const index = this.viewModel.getCellIndex(element); if (index >= 0) { - return `Cell ${index}, ${element.cellKind === CellKind.Markdown ? 'markdown' : 'code'} cell`; + return `Cell ${index}, ${element.cellKind === CellKind.Markup ? 'markdown' : 'code'} cell`; } return ''; @@ -673,19 +917,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(widgetFocusTracker.onDidBlur(() => this._onDidBlurEmitter.fire())); this._reigsterNotebookActionsToolbar(); - this._register(this.editorService.onDidActiveEditorChange(() => { - if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; - if (notebookEditor === this) { - // this is the active editor - this._showNotebookActionsinEditorToolbar(); - return; - } - } - this._editorToolbarDisposable.clear(); - this._toolbarActionDisposable.clear(); - })); } private showListContextMenu(e: IListContextMenuEvent) { @@ -701,126 +933,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); } - private _notebookGlobalActionsMenu!: IMenu; - private _toolbarActionDisposable = this._register(new DisposableStore()); - private _topToolbar!: ToolBar; - private _useGlobalToolbar: boolean = false; - private _editorToolbarDisposable = this._register(new DisposableStore()); private _reigsterNotebookActionsToolbar() { - const cellMenu = this.instantiationService.createInstance(CellMenus); - this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.scopedContextKeyService)); - this._register(this._notebookGlobalActionsMenu); - - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar') ?? false; - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.experimental.globalToolbar')) { - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar'); - this._showNotebookActionsinEditorToolbar(); + this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorToolbar, this, this.scopedContextKeyService, this._notebookTopToolbarContainer)); + this._register(this._notebookTopToolbar.onDidChangeState(() => { + if (this._dimension && this._isVisible) { + this.layout(this._dimension); } })); - - this._topToolbar = new ToolBar(this._notebookTopToolbarContainer, this.contextMenuService, { - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - actionViewItemProvider: action => { - return createActionViewItem(this.instantiationService, action); - }, - renderDropdownAsChildElement: true - }); - this._register(this._topToolbar); - this._topToolbar.context = { - ui: true, - notebookEditor: this - }; - - this._showNotebookActionsinEditorToolbar(); - this._register(this._notebookGlobalActionsMenu.onDidChange(() => { - this._showNotebookActionsinEditorToolbar(); - })); - - if (this.experimentService) { - this.experimentService.getTreatment('nbtoolbarineditor').then(treatment => { - if (treatment === undefined) { - return; - } - if (this._useGlobalToolbar !== treatment) { - this._useGlobalToolbar = treatment; - this._showNotebookActionsinEditorToolbar(); - } - }); - } - } - - private _showNotebookActionsinEditorToolbar() { - // when there is no view model, just ignore. - if (!this.viewModel) { - return; - } - - if (!this._useGlobalToolbar) { - // schedule actions registration in next frame, otherwise we are seeing duplicated notbebook actions temporarily - this._editorToolbarDisposable.clear(); - this._editorToolbarDisposable.add(DOM.scheduleAtNextAnimationFrame(() => { - const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); - this._toolbarActionDisposable.clear(); - this._topToolbar.setActions([], []); - if (!this.viewModel) { - return; - } - - if (!this._isVisible) { - return; - } - - if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; - if (notebookEditor !== this) { - // clear actions but not recreate because it is not active editor - return; - } - } - - groups.forEach(group => { - const groupName = group[0]; - const actions = group[1]; - - let order = groupName === 'navigation' ? -10 : 0; - for (let i = 0; i < actions.length; i++) { - const menuItemAction = actions[i] as MenuItemAction; - this._toolbarActionDisposable.add(MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: menuItemAction.item.id, - title: menuItemAction.item.title, - category: menuItemAction.item.category, - tooltip: menuItemAction.item.tooltip, - icon: menuItemAction.item.icon, - precondition: menuItemAction.item.precondition, - toggled: menuItemAction.item.toggled, - }, - title: menuItemAction.item.title + ' ' + this.viewModel?.uri.scheme, - group: groupName, - order: order - })); - order++; - } - }); - })); - - this._notebookTopToolbarContainer.style.display = 'none'; - } else { - this._toolbarActionDisposable.clear(); - this._topToolbar.setActions([], []); - const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); - this._notebookTopToolbarContainer.style.display = 'flex'; - const primaryGroup = groups.find(group => group[0] === 'navigation'); - const primaryActions = primaryGroup ? primaryGroup[1] : []; - const secondaryActions = groups.filter(group => group[0] !== 'navigation').reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []); - - this._topToolbar.setActions(primaryActions, secondaryActions); - } - - if (this._dimension && this._isVisible) { - this.layout(this._dimension); - } } private _updateForCursorNavigationMode(applyFocusChange: () => void): void { @@ -858,9 +977,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor async setModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined): Promise { if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { + const oldTopInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + const oldBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); this._detachModel(); await this._attachModel(textModel, viewState); - + const newTopInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + const newBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); + + if (oldTopInsertToolbarHeight !== newTopInsertToolbarHeight + || oldBottomToolbarDimensions.bottomToolbarGap !== newBottomToolbarDimensions.bottomToolbarGap + || oldBottomToolbarDimensions.bottomToolbarHeight !== newBottomToolbarDimensions.bottomToolbarHeight) { + this._styleElement?.remove(); + this._createLayoutStyles(); + this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + } type WorkbenchNotebookOpenClassification = { scheme: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; @@ -910,7 +1040,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - async setOptions(options: NotebookEditorOptions | undefined) { + async setOptions(options: INotebookEditorOptions | undefined) { if (!this.hasModel()) { return; } @@ -925,7 +1055,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { this.focusElement(cell); - await this.revealInCenterIfOutsideViewportAsync(cell); + const selection = cellOptions.options?.selection; + if (selection) { + await this.revealLineInCenterIfOutsideViewportAsync(cell, selection.startLineNumber); + } else { + await this.revealInCenterIfOutsideViewportAsync(cell); + } + const editor = this._renderedEditors.get(cell)!; if (editor) { if (cellOptions.options?.selection) { @@ -1043,14 +1179,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async _createWebview(id: string, resource: URI): Promise { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { - outputNodePadding: CELL_OUTPUT_PADDING, - outputNodeLeftPadding: CELL_OUTPUT_PADDING, - previewNodePadding: MARKDOWN_PREVIEW_PADDING, - leftMargin: CODE_CELL_LEFT_MARGIN, - rightMargin: CELL_RIGHT_MARGIN, - runGutter: CELL_RUN_GUTTER, - }); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeWebviewOptions(), this.notebookRendererMessaging.getScoped(this._uuid)); this._webview.element.style.width = '100%'; // attach the webview container to the DOM tree first @@ -1059,10 +1188,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { await this._createWebview(this.getId(), textModel.uri); - - this._eventDispatcher = new NotebookEventDispatcher(); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._eventDispatcher, this.getLayoutInfo()); - this._eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo()); + this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this._updateForOptions(); this._updateForNotebookConfiguration(); @@ -1118,7 +1245,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const deletedCells: MarkdownCellViewModel[] = []; for (const cell of cells) { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { const mdCell = cell as MarkdownCellViewModel; if (this.viewModel?.viewCells.find(cell => cell.handle === mdCell.handle)) { // Cell has been folded but is still in model @@ -1161,7 +1288,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); if (this._dimension) { - this._list.layout(this._dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, this._dimension.width); + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + this._list.layout(this._dimension.height - topInserToolbarHeight, this._dimension.width); } else { this._list.layout(); } @@ -1191,7 +1319,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); } - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { store.add((cell as MarkdownCellViewModel).onDidHideInput(() => { this.hideMarkdownPreviews([(cell as MarkdownCellViewModel)]); })); @@ -1245,7 +1373,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor offset += (totalHeightCache ? totalHeightCache[i] : 0); continue; } else { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { requests.push([cell, offset]); } } @@ -1257,19 +1385,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - await this._webview!.initializeMarkdown(requests - .map(request => ({ cellId: request[0].id, cellHandle: request[0].handle, content: request[0].getText(), offset: request[1] }))); + await this._webview!.initializeMarkdown(requests.map(request => ({ + cellId: request[0].id, + cellHandle: request[0].handle, + content: request[0].getText(), + offset: request[1], + visible: false, + }))); } else { - const initRequests = viewModel.viewCells.filter(cell => cell.cellKind === CellKind.Markdown).slice(0, 5).map(cell => ({ cellId: cell.id, cellHandle: cell.handle, content: cell.getText(), offset: -10000 })); + const initRequests = viewModel.viewCells.filter(cell => cell.cellKind === CellKind.Markup).slice(0, 5).map(cell => ({ cellId: cell.id, cellHandle: cell.handle, content: cell.getText(), offset: -10000, visible: false })); await this._webview!.initializeMarkdown(initRequests); // no cached view state so we are rendering the first viewport // after above async call, we already get init height for markdown cells, we can update their offset let offset = 0; - const offsetUpdateRequests: { id: string, top: number }[] = []; + const offsetUpdateRequests: { id: string, top: number; }[] = []; const scrollBottom = Math.max(this._dimension?.height ?? 0, 1080); for (const cell of viewModel.viewCells) { - if (cell.cellKind === CellKind.Markdown) { + if (cell.cellKind === CellKind.Markup) { offsetUpdateRequests.push({ id: cell.id, top: offset }); } @@ -1330,7 +1463,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (this._list) { state.scrollPosition = { left: this._list.scrollLeft, top: this._list.scrollTop }; - const cellHeights: { [key: number]: number } = {}; + const cellHeights: { [key: number]: number; } = {}; for (let i = 0; i < this.viewModel!.length; i++) { const elm = this.viewModel!.cellAt(i) as CellViewModel; if (elm.cellKind === CellKind.Code) { @@ -1356,7 +1489,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } // Save contribution view states - const contributionsState: { [key: string]: unknown } = {}; + const contributionsState: { [key: string]: unknown; } = {}; for (const [id, contribution] of this._contributions) { if (typeof contribution.saveViewState === 'function') { contributionsState[id] = contribution.saveViewState(); @@ -1384,16 +1517,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + this._dimension = new DOM.Dimension(dimension.width, dimension.height); - DOM.size(this._body, dimension.width, dimension.height - (this._useGlobalToolbar ? /** Toolbar height */ 26 : 0)); - if (this._list.getRenderHeight() < dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP) { + DOM.size(this._body, dimension.width, dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0)); + if (this._list.getRenderHeight() < dimension.height - topInserToolbarHeight) { // the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down) - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); } else { // the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first. - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); } this._overlayContainer.style.visibility = 'visible'; @@ -1411,7 +1546,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webviewTransparentCover.style.width = `${dimension.width}px`; } - this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + this._notebookTopToolbar.layout(); + + this._viewContext?.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } //#endregion @@ -1454,11 +1591,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const focused = DOM.isAncestor(document.activeElement, this._overlayContainer); this._editorFocus.set(focused); this.viewModel?.setFocus(focused); - - if (!focused) { - this._editorToolbarDisposable.clear(); - this._toolbarActionDisposable.clear(); - } } hasFocus() { @@ -1686,21 +1818,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#region Kernel/Execution - private async _loadKernelPreloads() { - const kernel = this.activeKernel; - if (!kernel) { - return; - } - const preloadUris = kernel.preloadUris; - if (!preloadUris.length) { + private async _loadKernelPreloads(): Promise { + if (!this.hasModel()) { return; } - + const { selected } = this.notebookKernelService.getMatchingKernel(this.viewModel.notebookDocument); if (!this._webview?.isResolved()) { await this._resolveWebview(); } - - this._webview?.updateKernelPreloads([kernel.localResourceRoot], kernel.preloadUris); + this._webview?.updateKernelPreloads(selected); } get activeKernel() { @@ -1730,7 +1856,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#endregion //#region Cell operations/layout API - private _pendingLayouts = new WeakMap(); + private _pendingLayouts: WeakMap | null = new WeakMap(); async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { this._debug('layout cell', cell.handle, height); const viewIndex = this._list.getViewIndex(cell); @@ -1747,8 +1873,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list.updateElementHeight2(cell, height); }; - if (this._pendingLayouts.has(cell)) { - this._pendingLayouts.get(cell)!.dispose(); + if (this._pendingLayouts?.has(cell)) { + this._pendingLayouts?.get(cell)!.dispose(); } let r: () => void; @@ -1761,13 +1887,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - this._pendingLayouts.delete(cell); + this._pendingLayouts?.delete(cell); relayout(cell, height); r(); }); - this._pendingLayouts.set(cell, toDisposable(() => { + this._pendingLayouts?.set(cell, toDisposable(() => { layoutDisposable.dispose(); r(); })); @@ -1800,7 +1926,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const defaultLanguage = supportedLanguages[0] || 'plaintext'; if (cell?.cellKind === CellKind.Code) { language = cell.language; - } else if (cell?.cellKind === CellKind.Markdown) { + } else if (cell?.cellKind === CellKind.Markup) { const nearestCodeCellIndex = this._nearestCodeCellIndex(index); if (nearestCodeCellIndex > -1) { language = this.viewModel.cellAt(nearestCodeCellIndex)!.language; @@ -1855,7 +1981,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return false; } - if (this._pendingLayouts.has(cell)) { + if (this._pendingLayouts?.has(cell)) { this._pendingLayouts.get(cell)!.dispose(); } @@ -1978,13 +2104,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor let position = ''; switch (focusItem) { case 'editor': - position = `the inner ${cell.cellKind === CellKind.Markdown ? 'markdown' : 'code'} editor is focused, press escape to focus the cell container`; + position = `the inner ${cell.cellKind === CellKind.Markup ? 'markdown' : 'code'} editor is focused, press escape to focus the cell container`; break; case 'output': position = `the cell output is focused, press escape to focus the cell container`; break; case 'container': - position = `the ${cell.cellKind === CellKind.Markdown ? 'markdown preview' : 'cell container'} is focused, press enter to focus the inner ${cell.cellKind === CellKind.Markdown ? 'markdown' : 'code'} editor`; + position = `the ${cell.cellKind === CellKind.Markup ? 'markdown preview' : 'cell container'} is focused, press enter to focus the inner ${cell.cellKind === CellKind.Markup ? 'markdown' : 'code'} editor`; break; default: break; @@ -1993,17 +2119,36 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - toggleNotebookCellSelection(cell: ICellViewModel): void { + toggleNotebookCellSelection(selectedCell: ICellViewModel, selectFromPrevious: boolean): void { const currentSelections = this._list.getSelectedElements(); + const isSelected = currentSelections.includes(selectedCell); + + const previousSelection = selectFromPrevious ? currentSelections[currentSelections.length - 1] ?? selectedCell : selectedCell; + const selectedIndex = this._list.getViewIndex(selectedCell)!; + const previousIndex = this._list.getViewIndex(previousSelection)!; - const isSelected = currentSelections.includes(cell); + const cellsInSelectionRange = this.getCellsInRange(selectedIndex, previousIndex); if (isSelected) { // Deselect - this._list.selectElements(currentSelections.filter(current => current !== cell)); + this._list.selectElements(currentSelections.filter(current => !cellsInSelectionRange.includes(current))); } else { // Add to selection - this._list.selectElements([...currentSelections, cell]); + this.focusElement(selectedCell); + this._list.selectElements([...currentSelections.filter(current => !cellsInSelectionRange.includes(current)), ...cellsInSelectionRange]); + } + } + + private getCellsInRange(fromInclusive: number, toInclusive: number): ICellViewModel[] { + const selectedCellsInRange: ICellViewModel[] = []; + for (let index = 0; index < this._list.length; ++index) { + const cell = this._list.element(index); + if (cell) { + if ((index >= fromInclusive && index <= toInclusive) || (index >= toInclusive && index <= fromInclusive)) { + selectedCellsInRange.push(cell); + } + } } + return selectedCellsInRange; } focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { @@ -2115,7 +2260,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } const cellTop = this._list.getAbsoluteTopOfElement(cell); - await this._webview.showMarkdownPreview(cell.id, cell.handle, cell.getText(), cellTop, cell.contentHash); + await this._webview.showMarkdownPreview({ + cellHandle: cell.handle, + cellId: cell.id, + content: cell.getText(), + offset: cellTop, + visible: true, + }); } async unhideMarkdownPreviews(cells: readonly MarkdownCellViewModel[]) { @@ -2198,6 +2349,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (output.type === RenderOutputType.Extension) { + this.notebookRendererMessaging.prepare(output.renderer.id); + } + const cellTop = this._list.getAbsoluteTopOfElement(cell); if (!this._webview.insetMapping.has(output.source)) { await this._webview.createOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); @@ -2241,13 +2396,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; - postMessage(forRendererId: string | undefined, message: any) { + postMessage(message: any) { if (this._webview?.isResolved()) { - if (forRendererId === undefined) { - this._webview.webview.postMessage(message); - } else { - this._webview.postRendererMessage(forRendererId, message); - } + this._webview.postKernelMessage(message); } } @@ -2325,7 +2476,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview.removeInsets(removedItems); - const markdownUpdateItems: { id: string, top: number }[] = []; + const markdownUpdateItems: { id: string, top: number; }[] = []; for (const cellId of this._webview.markdownPreviewMapping.keys()) { const cell = this.viewModel?.viewCells.find(cell => cell.id === cellId); if (cell) { @@ -2352,10 +2503,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean) { const cell = this.getCellById(cellId); if (cell && cell instanceof MarkdownCellViewModel) { - if (height + BOTTOM_CELL_TOOLBAR_GAP !== cell.layoutInfo.totalHeight) { - this._debug('updateMarkdownCellHeight', cell.handle, height + BOTTOM_CELL_TOOLBAR_GAP, isInit); - cell.renderedMarkdownHeight = height; - } + const { bottomToolbarGap } = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); + this._debug('updateMarkdownCellHeight', cell.handle, height + bottomToolbarGap, isInit); + cell.renderedMarkdownHeight = height; } } @@ -2366,24 +2516,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - markdownCellDragStart(cellId: string, ctx: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number; }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.startExplicitDrag(cell, ctx); + this._dndController?.startExplicitDrag(cell, event.dragOffsetY); } } - markdownCellDrag(cellId: string, ctx: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number; }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrag(cell, ctx); + this._dndController?.explicitDrag(cell, event.dragOffsetY); } } - markdownCellDrop(cellId: string, ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }): void { + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean; }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrop(cell, ctx); + this._dndController?.explicitDrop(cell, event); } } @@ -2420,10 +2570,23 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._overlayContainer.remove(); this.viewModel?.dispose(); + + // unref + this._webview = null; + this._webviewResolvePromise = null; + this._webviewTransparentCover = null; + this._dndController = null; + this._listTopCellToolbar = null; + this._notebookViewModel = undefined; + this._cellContextKeyManager = null; + this._renderedEditors.clear(); + this._pendingLayouts = null; + this._listDelegate = null; + super.dispose(); } - toJSON(): { notebookUri: URI | undefined } { + toJSON(): { notebookUri: URI | undefined; } { return { notebookUri: this.viewModel?.uri, }; @@ -2552,26 +2715,37 @@ export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackgr hc: null }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); +export const cellEditorBackground = registerColor('notebook.cellEditorBackground', { + light: null, + dark: null, + hc: null +}, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); + registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, - .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { - padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; - box-sizing: border-box; - }`); + // add css variable rules + + const focusedCellBorderColor = theme.getColor(focusedCellBorder); + const inactiveFocusedBorderColor = theme.getColor(inactiveFocusedCellBorder); + const selectedCellBorderColor = theme.getColor(selectedCellBorder); + collector.addRule(` + :root { + --notebook-focused-cell-border-color: ${focusedCellBorderColor}; + --notebook-inactive-focused-cell-border-color: ${inactiveFocusedBorderColor}; + --notebook-selected-cell-border-color: ${selectedCellBorderColor}; + } + `); + const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.notebookOverlay .output a, - .notebookOverlay .cell.markdown a, + collector.addRule(`.notebookOverlay .cell.markdown a, .notebookOverlay .output-show-more-container a { color: ${link};} `); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.notebookOverlay .output a:hover, - .notebookOverlay .cell .output a:active, - .notebookOverlay .output-show-more-container a:active + collector.addRule(`.notebookOverlay .output-show-more-container a:active { color: ${activeLink}; }`); } const shortcut = theme.getColor(textPreformatForeground); @@ -2599,17 +2773,20 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .output-show-more-container { background-color: ${containerBackground}; }`); } - const editorBackgroundColor = theme.getColor(editorBackground); + const notebookBackground = theme.getColor(editorBackground); + if (notebookBackground) { + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${notebookBackground} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${notebookBackground}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${notebookBackground}; }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${notebookBackground} }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${notebookBackground} }`); + } + + const editorBackgroundColor = theme.getColor(cellEditorBackground) ?? theme.getColor(editorBackground); if (editorBackgroundColor) { collector.addRule(`.notebookOverlay .cell .monaco-editor-background, - .notebookOverlay .cell .margin-view-overlays, - .notebookOverlay .cell .cell-statusbar-container { background: ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${editorBackgroundColor} !important; }`); - - collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); - collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${editorBackgroundColor}; }`); } const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); @@ -2623,8 +2800,8 @@ registerThemingParticipant((theme, collector) => { const focusedCellBackgroundColor = theme.getColor(focusedCellBackground); if (focusedCellBackgroundColor) { - collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator, - .notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-collapsed-part { background-color: ${focusedCellBackgroundColor} !important; }`); } @@ -2656,30 +2833,6 @@ registerThemingParticipant((theme, collector) => { .notebookOverlay .code-cell-row:not(.focused).cell-output-hover .cell-collapsed-part { background-color: ${cellHoverBackgroundColor}; }`); } - const focusedCellBorderColor = theme.getColor(focusedCellBorder); - collector.addRule(` - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-bottom:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { - border-color: ${focusedCellBorderColor} !important; - }`); - - const inactiveFocusedBorderColor = theme.getColor(inactiveFocusedCellBorder); - collector.addRule(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before { - border-color: ${inactiveFocusedBorderColor} !important; - }`); - - const selectedCellBorderColor = theme.getColor(selectedCellBorder); - collector.addRule(` - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-bottom:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container.cell-editor-focus:before { - border-color: ${selectedCellBorderColor} !important; - }`); - const cellSymbolHighlightColor = theme.getColor(cellSymbolHighlight); if (cellSymbolHighlightColor) { collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-symbolHighlight .cell-focus-indicator, @@ -2768,43 +2921,5 @@ registerThemingParticipant((theme, collector) => { }`); } - // Cell Margin - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${CELL_RIGHT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${MARKDOWN_CELL_BOTTOM_MARGIN}px; padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container.webview-backed-markdown-cell { padding: 0; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell.markdown-cell-edit-mode .cell.code { padding-bottom: ${MARKDOWN_CELL_BOTTOM_MARGIN}px; padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_RIGHT_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + CELL_RIGHT_MARGIN}px); }`); - - collector.addRule(`.notebookOverlay .output-show-more-container { margin: 0px ${CELL_RIGHT_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .output-show-more-container { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + CELL_RIGHT_MARGIN}px); }`); - - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell .run-button-container { width: 20px; left: ${CODE_CELL_LEFT_MARGIN + Math.floor(CELL_RUN_GUTTER - 20) / 2}px }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row :not(.webview-backed-markdown-cell) .cell-focus-indicator-top { height: ${CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_GAP}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left, - .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-drag-handle { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${CODE_CELL_LEFT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${CELL_RIGHT_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${CELL_BOTTOM_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${CELL_BOTTOM_MARGIN}px; }`); - - collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; height: ${COLLAPSED_INDICATOR_HEIGHT}px; }`); - collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); - - collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px }`); - collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { height: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px }`); - - // left and right border margins - collector.addRule(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { - top: -${CELL_TOP_MARGIN}px; height: calc(100% + ${CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN}px) - }`); + }); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts index ba83b21978be..91a12f7ee467 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICellViewModel, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; @@ -13,12 +13,17 @@ import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/not export class NotebookEditorContextKeys { private readonly _notebookKernelCount: IContextKey; + private readonly _notebookKernelSelected: IContextKey; private readonly _interruptibleKernel: IContextKey; private readonly _someCellRunning: IContextKey; + private readonly _hasOutputs: IContextKey; + private readonly _useConsolidatedOutputButton: IContextKey; + private _viewType!: IContextKey; private readonly _disposables = new DisposableStore(); private readonly _viewModelDisposables = new DisposableStore(); private readonly _cellStateListeners: IDisposable[] = []; + private readonly _cellOutputsListeners: IDisposable[] = []; constructor( private readonly _editor: INotebookEditor, @@ -26,13 +31,22 @@ export class NotebookEditorContextKeys { @IContextKeyService contextKeyService: IContextKeyService, ) { this._notebookKernelCount = NOTEBOOK_KERNEL_COUNT.bindTo(contextKeyService); + this._notebookKernelSelected = NOTEBOOK_KERNEL_SELECTED.bindTo(contextKeyService); this._interruptibleKernel = NOTEBOOK_INTERRUPTIBLE_KERNEL.bindTo(contextKeyService); this._someCellRunning = NOTEBOOK_HAS_RUNNING_CELL.bindTo(contextKeyService); + this._useConsolidatedOutputButton = NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON.bindTo(contextKeyService); + this._hasOutputs = NOTEBOOK_HAS_OUTPUTS.bindTo(contextKeyService); + this._viewType = NOTEBOOK_VIEW_TYPE.bindTo(contextKeyService); + + this._handleDidChangeModel(); + this._updateForNotebookOptions(); this._disposables.add(_editor.onDidChangeModel(this._handleDidChangeModel, this)); this._disposables.add(_notebookKernelService.onDidAddKernel(this._updateKernelContext, this)); this._disposables.add(_notebookKernelService.onDidChangeNotebookKernelBinding(this._updateKernelContext, this)); - this._handleDidChangeModel(); + this._disposables.add(_editor.notebookOptions.onDidChangeOptions(() => { + this._updateForNotebookOptions(); + })); } dispose(): void { @@ -41,6 +55,11 @@ export class NotebookEditorContextKeys { this._notebookKernelCount.reset(); this._interruptibleKernel.reset(); this._someCellRunning.reset(); + this._viewType.reset(); + dispose(this._cellStateListeners); + this._cellStateListeners.length = 0; + dispose(this._cellOutputsListeners); + this._cellOutputsListeners.length = 0; } private _handleDidChangeModel(): void { @@ -50,6 +69,8 @@ export class NotebookEditorContextKeys { this._viewModelDisposables.clear(); dispose(this._cellStateListeners); this._cellStateListeners.length = 0; + dispose(this._cellOutputsListeners); + this._cellOutputsListeners.length = 0; if (!this._editor.hasModel()) { return; @@ -62,26 +83,52 @@ export class NotebookEditorContextKeys { if (!e.runStateChanged) { return; } - if (c.metadata?.runState === NotebookCellExecutionState.Pending) { + if (c.internalMetadata.runState === NotebookCellExecutionState.Pending) { executionCount++; - } else if (c.metadata?.runState === NotebookCellExecutionState.Idle) { + } else if (!c.internalMetadata.runState) { executionCount--; } this._someCellRunning.set(executionCount > 0); }); }; + const recomputeOutputsExistence = () => { + let hasOutputs = false; + if (this._editor.hasModel()) { + for (let i = 0; i < this._editor.viewModel.viewCells.length; i++) { + if (this._editor.viewModel.viewCells[i].outputsViewModels.length > 0) { + hasOutputs = true; + break; + } + } + } + + this._hasOutputs.set(hasOutputs); + }; + + const addCellOutputsListener = (c: ICellViewModel) => { + return c.model.onDidChangeOutputs(() => { + recomputeOutputsExistence(); + }); + }; + for (const cell of this._editor.viewModel.viewCells) { this._cellStateListeners.push(addCellStateListener(cell)); + this._cellOutputsListeners.push(addCellOutputsListener(cell)); } + recomputeOutputsExistence(); + this._viewModelDisposables.add(this._editor.viewModel.onDidChangeViewCells(e => { e.splices.reverse().forEach(splice => { const [start, deleted, newCells] = splice; - const deletedCells = this._cellStateListeners.splice(start, deleted, ...newCells.map(addCellStateListener)); - dispose(deletedCells); + const deletedCellStates = this._cellStateListeners.splice(start, deleted, ...newCells.map(addCellStateListener)); + const deletedCellOutputStates = this._cellOutputsListeners.splice(start, deleted, ...newCells.map(addCellOutputsListener)); + dispose(deletedCellStates); + dispose(deletedCellOutputStates); }); })); + this._viewType.set(this._editor.viewModel.viewType); } private _updateKernelContext(): void { @@ -94,5 +141,10 @@ export class NotebookEditorContextKeys { const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.viewModel.notebookDocument); this._notebookKernelCount.set(all.length); this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); + this._notebookKernelSelected.set(Boolean(selected)); + } + + private _updateForNotebookOptions(): void { + this._useConsolidatedOutputButton.set(this._editor.notebookOptions.getLayoutConfiguration().consolidatedOutputButton); } } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index edeff4ff744f..3fe6bdbd6c5d 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -11,6 +11,8 @@ export const configureKernelIcon = registerIcon('notebook-kernel-configure', Cod export const selectKernelIcon = registerIcon('notebook-kernel-select', Codicon.serverEnvironment, localize('selectKernelIcon', 'Configure icon to select a kernel in notebook editors.')); export const executeIcon = registerIcon('notebook-execute', Codicon.play, localize('executeIcon', 'Icon to execute in notebook editors.')); +export const executeAboveIcon = registerIcon('notebook-execute-above', Codicon.runAbove, localize('executeAboveIcon', 'Icon to execute above cells in notebook editors.')); +export const executeBelowIcon = registerIcon('notebook-execute-below', Codicon.runBelow, localize('executeBelowIcon', 'Icon to execute below cells in notebook editors.')); export const stopIcon = registerIcon('notebook-stop', Codicon.primitiveSquare, localize('stopIcon', 'Icon to stop an execution in notebook editors.')); export const deleteCellIcon = registerIcon('notebook-delete-cell', Codicon.trash, localize('deleteCellIcon', 'Icon to delete a cell in notebook editors.')); export const executeAllIcon = registerIcon('notebook-execute-all', Codicon.runAll, localize('executeAllIcon', 'Icon to execute all cells in notebook editors.')); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts new file mode 100644 index 000000000000..9a70372f3504 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/notebookKernelActionViewItem'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Action, IAction } from 'vs/base/common/actions'; +import { localize } from 'vs/nls'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { INotebookKernelMatchResult, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +registerThemingParticipant((theme, collector) => { + const value = theme.getColor(toolbarHoverBackground); + collector.addRule(`:root { + --code-toolbarHoverBackground: ${value}; + }`); +}); + +export class NotebooKernelActionViewItem extends ActionViewItem { + + private _kernelLabel?: HTMLAnchorElement; + + constructor( + actualAction: IAction, + private readonly _editor: NotebookEditor | INotebookEditor, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + ) { + super( + undefined, + new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)), + { label: false, icon: true } + ); + this._register(_editor.onDidChangeModel(this._update, this)); + this._register(_notebookKernelService.onDidChangeNotebookAffinity(this._update, this)); + this._register(_notebookKernelService.onDidChangeNotebookKernelBinding(this._update, this)); + } + + override render(container: HTMLElement): void { + this._update(); + super.render(container); + container.classList.add('kernel-action-view-item'); + this._kernelLabel = document.createElement('a'); + container.appendChild(this._kernelLabel); + this.updateLabel(); + } + + override updateLabel() { + if (this._kernelLabel) { + this._kernelLabel.classList.add('kernel-label'); + this._kernelLabel.innerText = this._action.label; + this._kernelLabel.title = this._action.tooltip; + } + } + + protected _update(): void { + const notebook = this._editor.viewModel?.notebookDocument; + + if (!notebook) { + this._resetAction(); + return; + } + + const info = this._notebookKernelService.getMatchingKernel(notebook); + this._updateActionFromKernelInfo(info); + } + + private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { + + if (info.all.length === 0) { + // should not happen - means "bad" context keys + this._resetAction(); + return; + } + + this._action.enabled = true; + const selectedOrSuggested = info.selected ?? info.suggested; + if (selectedOrSuggested) { + // selected or suggested kernel + this._action.label = selectedOrSuggested.label; + this._action.tooltip = selectedOrSuggested.description ?? selectedOrSuggested.detail ?? ''; + if (!info.selected) { + // special UI for selected kernel? + } + + } else { + // many kernels + this._action.label = localize('select', "Select Kernel"); + this._action.tooltip = ''; + } + } + + private _resetAction(): void { + this._action.enabled = false; + this._action.label = ''; + this._action.class = ''; + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index 52cdfeadee7d..7ef1ec244cf6 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -74,7 +74,7 @@ export class NotebookKernelService implements INotebookKernelService { // auto associate kernels to new notebook documents, also emit event when // a notebook has been closed (but don't update the memento) this._disposables.add(_notebookService.onDidAddNotebookDocument(this._tryAutoBindNotebook, this)); - this._disposables.add(_notebookService.onDidRemoveNotebookDocument(notebook => { + this._disposables.add(_notebookService.onWillRemoveNotebookDocument(notebook => { const kernelId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); if (kernelId) { this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel: kernelId, newKernel: undefined }); @@ -191,7 +191,7 @@ export class NotebookKernelService implements INotebookKernelService { const selectedId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); const selected = selectedId ? this._kernels.get(selectedId)?.kernel : undefined; - return { all, selected }; + return { all, selected, suggested: all.length === 1 ? all[0] : undefined }; } // default kernel for notebookType diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts new file mode 100644 index 000000000000..0d816f42fb55 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { INotebookRendererMessagingService, IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +type MessageToSend = { editorId: string; rendererId: string; message: unknown }; + +export class NotebookRendererMessagingService implements INotebookRendererMessagingService { + declare _serviceBrand: undefined; + /** + * Activation promises. Maps renderer IDs to a queue of messages that should + * be sent once activation finishes, or undefined if activation is complete. + */ + private readonly activations = new Map(); + private readonly receiveMessageEmitter = new Emitter<{ editorId: string; rendererId: string, message: unknown }>(); + public readonly onDidReceiveMessage = this.receiveMessageEmitter.event; + private readonly postMessageEmitter = new Emitter(); + public readonly onShouldPostMessage = this.postMessageEmitter.event; + + constructor(@IExtensionService private readonly extensionService: IExtensionService) { } + + /** @inheritdoc */ + public fireDidReceiveMessage(editorId: string, rendererId: string, message: unknown): void { + this.receiveMessageEmitter.fire({ editorId, rendererId, message }); + } + + /** @inheritdoc */ + public prepare(rendererId: string) { + if (this.activations.has(rendererId)) { + return; + } + + const queue: MessageToSend[] = []; + this.activations.set(rendererId, queue); + + this.extensionService.activateByEvent(`onRenderer:${rendererId}`).then(() => { + for (const message of queue) { + this.postMessageEmitter.fire(message); + } + + this.activations.set(rendererId, undefined); + }); + } + + /** @inheritdoc */ + public getScoped(editorId: string): IScopedRendererMessaging { + return { + onDidReceiveMessage: Event.filter(this.onDidReceiveMessage, e => e.editorId === editorId), + postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), + }; + } + + private postMessage(editorId: string, rendererId: string, message: unknown): void { + if (!this.activations.has(rendererId)) { + this.prepare(rendererId); + } + + const activation = this.activations.get(rendererId); + const toSend = { rendererId, editorId, message }; + if (activation === undefined) { + this.postMessageEmitter.fire(toSend); + } else { + activation.push(toSend); + } + } +} diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index e3d01d128b5f..a85a27938d04 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,44 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as glob from 'vs/base/common/glob'; -import { localize } from 'vs/nls'; import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import { Emitter, Event } from 'vs/base/common/event'; +import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; +import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { Memento } from 'vs/workbench/common/memento'; -import { INotebookEditorContribution, notebookMarkupRendererExtensionPoint, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; -import { NotebookEditorOptions, updateEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditorContribution, notebooksExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellUri, DisplayOrderKey, INotebookExclusiveDocumentFilter, INotebookMarkupRendererInfo, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, NotebookEditorPriority, NotebookRendererMatch, NotebookTextDiffEditorPreview, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookMarkupRendererInfo as NotebookMarkupRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookMarkdownRenderer'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellUri, DisplayOrderKey, INotebookExclusiveDocumentFilter, INotebookContributionData, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, NotebookEditorPriority, NotebookRendererMatch, NotebookTextDiffEditorPreview, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { updateEditorTopPadding } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { ComplexNotebookProviderInfo, INotebookContentProvider, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ContributedEditorPriority, DiffEditorInputFactoryFunction, EditorInputFactoryFunction, IEditorOverrideService, IEditorType } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Schemas } from 'vs/base/common/network'; -import { Lazy } from 'vs/base/common/lazy'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; -import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; -import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { ContributedEditorPriority, DiffEditorInputFactoryFunction, EditorInputFactoryFunction, IEditorAssociationsRegistry, IEditorOverrideService, IEditorType, IEditorTypesHandler } from 'vs/workbench/services/editor/common/editorOverrideService'; -import { EditorExtensions, IEditorInput } from 'vs/workbench/common/editor'; -import { IFileService } from 'vs/platform/files/common/files'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; export class NotebookProviderInfoStore extends Disposable { @@ -80,7 +80,7 @@ export class NotebookProviderInfoStore extends Disposable { } })); - notebookProviderExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); + notebooksExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); } override dispose(): void { @@ -94,16 +94,24 @@ export class NotebookProviderInfoStore extends Disposable { for (const extension of extensions) { for (const notebookContribution of extension.value) { + + if (!notebookContribution.type) { + extension.collector.error(`Notebook does not specify type-property`); + continue; + } + + if (this.get(notebookContribution.type)) { + extension.collector.error(`Notebook type '${notebookContribution.type}' already used`); + continue; + } + this.add(new NotebookProviderInfo({ - id: notebookContribution.viewType, + extension: extension.description.identifier, + id: notebookContribution.type, displayName: notebookContribution.displayName, selectors: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), - providerExtensionId: extension.description.identifier.value, - providerDescription: extension.description.description, providerDisplayName: extension.description.isBuiltin ? localize('builtinProviderDisplayName', "Built-in") : extension.description.displayName || extension.description.identifier.value, - providerExtensionLocation: extension.description.extensionLocation, - dynamicContribution: false, exclusive: false })); } @@ -127,7 +135,10 @@ export class NotebookProviderInfoStore extends Disposable { } - private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): void { + private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): IDisposable { + + const disposables = new DisposableStore(); + for (const selector of notebookProviderInfo.selectors) { const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as glob.IRelativePattern | string; const notebookEditorInfo = { @@ -148,10 +159,10 @@ export class NotebookProviderInfoStore extends Disposable { if (data) { notebookUri = data.notebook; - cellOptions = { resource: resource }; + cellOptions = { resource, options }; } - const notebookOptions = new NotebookEditorOptions({ ...options, cellOptions }); + const notebookOptions: INotebookEditorOptions = { ...options, cellOptions }; return { editor: NotebookEditorInput.create(this._instantiationService, notebookUri, notebookProviderInfo.id), options: notebookOptions }; }; const notebookEditorDiffFactory: DiffEditorInputFactoryFunction = (diffEditorInput: DiffEditorInput, options, group) => { @@ -162,7 +173,7 @@ export class NotebookProviderInfoStore extends Disposable { return { editor: NotebookDiffEditorInput.create(this._instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), diffEditorInput.getName(), notebookProviderInfo.id) }; }; // Register the notebook editor - this._contributedEditorDisposables.add(this._editorOverrideService.registerContributionPoint( + disposables.add(this._editorOverrideService.registerContributionPoint( globPattern, notebookEditorInfo, notebookEditorOptions, @@ -170,7 +181,7 @@ export class NotebookProviderInfoStore extends Disposable { notebookEditorDiffFactory )); // Then register the schema handler as exclusive for that notebook - this._contributedEditorDisposables.add(this._editorOverrideService.registerContributionPoint( + disposables.add(this._editorOverrideService.registerContributionPoint( `${Schemas.vscodeNotebookCell}:/**/${globPattern}`, { ...notebookEditorInfo, priority: ContributedEditorPriority.exclusive }, notebookEditorOptions, @@ -178,6 +189,8 @@ export class NotebookProviderInfoStore extends Disposable { notebookEditorDiffFactory )); } + + return disposables; } @@ -190,16 +203,25 @@ export class NotebookProviderInfoStore extends Disposable { return this._contributedEditors.get(viewType); } - add(info: NotebookProviderInfo): void { + add(info: NotebookProviderInfo): IDisposable { if (this._contributedEditors.has(info.id)) { - return; + throw new Error(`notebook type '${info.id}' ALREADY EXISTS`); } this._contributedEditors.set(info.id, info); - this._registerContributionPoint(info); + const editorRegistration = this._registerContributionPoint(info); + this._contributedEditorDisposables.add(editorRegistration); const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); + + return toDisposable(() => { + const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); + this._memento.saveMemento(); + editorRegistration.dispose(); + this._contributedEditors.delete(info.id); + }); } getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] { @@ -238,6 +260,10 @@ export class NotebookOutputRendererInfoStore { return this.contributedRenderers.get(rendererId); } + getAll(): NotebookOutputRendererInfo[] { + return Array.from(this.contributedRenderers.values()); + } + add(info: NotebookOutputRendererInfo): void { if (this.contributedRenderers.has(info.id)) { return; @@ -283,23 +309,27 @@ class ModelData implements IDisposable { } } -export class NotebookService extends Disposable implements INotebookService, IEditorTypesHandler { +export class NotebookService extends Disposable implements INotebookService { declare readonly _serviceBrand: undefined; private readonly _notebookProviders = new Map(); private readonly _notebookProviderInfoStore: NotebookProviderInfoStore; private readonly _notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore); - private readonly _markdownRenderersInfos = new Set(); private readonly _models = new ResourceMap(); - private readonly _onDidCreateNotebookDocument = this._register(new Emitter()); + private readonly _onWillAddNotebookDocument = this._register(new Emitter()); private readonly _onDidAddNotebookDocument = this._register(new Emitter()); + private readonly _onWillRemoveNotebookDocument = this._register(new Emitter()); private readonly _onDidRemoveNotebookDocument = this._register(new Emitter()); - readonly onDidCreateNotebookDocument = this._onDidCreateNotebookDocument.event; + readonly onWillAddNotebookDocument = this._onWillAddNotebookDocument.event; readonly onDidAddNotebookDocument = this._onDidAddNotebookDocument.event; readonly onDidRemoveNotebookDocument = this._onDidRemoveNotebookDocument.event; + readonly onWillRemoveNotebookDocument = this._onWillRemoveNotebookDocument.event; + + private readonly _onWillRemoveViewType = this._register(new Emitter()); + readonly onWillRemoveViewType = this._onWillRemoveViewType.event; private readonly _onDidChangeEditorTypes = this._register(new Emitter()); onDidChangeEditorTypes: Event = this._onDidChangeEditorTypes.event; @@ -316,6 +346,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, ) { super(); @@ -329,13 +360,13 @@ export class NotebookService extends Disposable implements INotebookService, IEd for (const extension of renderers) { for (const notebookContribution of extension.value) { if (!notebookContribution.entrypoint) { // avoid crashing - console.error(`Cannot register renderer for ${extension.description.identifier.value} since it did not have an entrypoint. This is now required: https://github.com/microsoft/vscode/issues/102644`); + extension.collector.error(`Notebook renderer does not specify entry point`); continue; } - const id = notebookContribution.id ?? notebookContribution.viewType; + const id = notebookContribution.id; if (!id) { - console.error(`Notebook renderer from ${extension.description.identifier.value} is missing an 'id'`); + extension.collector.error(`Notebook renderer does not specify id-property`); continue; } @@ -347,44 +378,11 @@ export class NotebookService extends Disposable implements INotebookService, IEd mimeTypes: notebookContribution.mimeTypes || [], dependencies: notebookContribution.dependencies, optionalDependencies: notebookContribution.optionalDependencies, + requiresMessaging: notebookContribution.requiresMessaging, })); } } }); - notebookMarkupRendererExtensionPoint.setHandler((renderers) => { - this._markdownRenderersInfos.clear(); - - for (const extension of renderers) { - if (!extension.description.enableProposedApi && !extension.description.isBuiltin) { - // Only allow proposed extensions to use this extension point - return; - } - - for (const notebookContribution of extension.value) { - if (!notebookContribution.entrypoint) { // avoid crashing - console.error(`Cannot register renderer for ${extension.description.identifier.value} since it did not have an entrypoint. This is now required: https://github.com/microsoft/vscode/issues/102644`); - continue; - } - - const id = notebookContribution.id; - if (!id) { - console.error(`Notebook renderer from ${extension.description.identifier.value} is missing an 'id'`); - continue; - } - - this._markdownRenderersInfos.add(new NotebookMarkupRendererInfo({ - id, - extension: extension.description, - entrypoint: notebookContribution.entrypoint, - displayName: notebookContribution.displayName, - mimeTypes: notebookContribution.mimeTypes, - dependsOn: notebookContribution.dependsOn, - })); - } - } - }); - - this._register(Registry.as(EditorExtensions.Associations).registerEditorTypesHandler('Notebook', this)); const updateOrder = () => { const userOrder = this._configurationService.getValue(DisplayOrderKey); @@ -469,51 +467,50 @@ export class NotebookService extends Disposable implements INotebookService, IEd return this._notebookProviders.has(viewType); } - private _registerProviderData(viewType: string, data: SimpleNotebookProviderInfo | ComplexNotebookProviderInfo): void { - if (this._notebookProviders.has(viewType)) { - throw new Error(`notebook controller for viewtype '${viewType}' already exists`); - } - this._notebookProviders.set(viewType, data); - } + registerContributedNotebookType(viewType: string, data: INotebookContributionData): IDisposable { - registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: INotebookContentProvider): IDisposable { - this._registerProviderData(viewType, new ComplexNotebookProviderInfo(viewType, controller, extensionData)); - if (controller.viewOptions && !this._notebookProviderInfoStore.get(viewType)) { - // register this content provider to the static contribution, if it does not exist - const info = new NotebookProviderInfo({ - displayName: controller.viewOptions.displayName, - id: viewType, - priority: ContributedEditorPriority.default, - selectors: [], - providerExtensionId: extensionData.id.value, - providerDescription: extensionData.description, - providerDisplayName: extensionData.id.value, - providerExtensionLocation: URI.revive(extensionData.location), - dynamicContribution: true, - exclusive: controller.viewOptions.exclusive - }); - - info.update({ selectors: controller.viewOptions.filenamePattern }); - info.update({ options: controller.options }); - this._notebookProviderInfoStore.add(info); - } + const info = new NotebookProviderInfo({ + extension: data.extension, + id: viewType, + displayName: data.displayName, + providerDisplayName: data.providerDisplayName, + exclusive: data.exclusive, + priority: ContributedEditorPriority.default, + selectors: [], + }); - this._notebookProviderInfoStore.get(viewType)?.update({ options: controller.options }); + info.update({ selectors: data.filenamePattern }); + const reg = this._notebookProviderInfoStore.add(info); this._onDidChangeEditorTypes.fire(); + return toDisposable(() => { - this._notebookProviders.delete(viewType); + reg.dispose(); this._onDidChangeEditorTypes.fire(); }); } - registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable { - this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); + private _registerProviderData(viewType: string, data: SimpleNotebookProviderInfo | ComplexNotebookProviderInfo): IDisposable { + if (this._notebookProviders.has(viewType)) { + throw new Error(`notebook controller for viewtype '${viewType}' already exists`); + } + this._notebookProviders.set(viewType, data); return toDisposable(() => { + this._onWillRemoveViewType.fire(viewType); this._notebookProviders.delete(viewType); }); } + registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: INotebookContentProvider): IDisposable { + this._notebookProviderInfoStore.get(viewType)?.update({ options: controller.options }); + return this._registerProviderData(viewType, new ComplexNotebookProviderInfo(viewType, controller, extensionData)); + } + + registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable { + this._notebookProviderInfoStore.get(viewType)?.update({ options: serializer.options }); + return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); + } + async withNotebookDataProvider(resource: URI, viewType?: string): Promise { const providers = this._notebookProviderInfoStore.getContributedNotebook(resource); // If we have a viewtype specified we want that data provider, as the resource won't always map correctly @@ -537,8 +534,8 @@ export class NotebookService extends Disposable implements INotebookService, IEd this._notebookRenderersInfoStore.setPreferred(mimeType, rendererId); } - getMarkupRendererInfo(): INotebookMarkupRendererInfo[] { - return Array.from(this._markdownRenderersInfos); + getRenderers(): INotebookRendererInfo[] { + return this._notebookRenderersInfoStore.getAll(); } // --- notebook documents: create, destory, retrieve, enumerate @@ -549,7 +546,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd } const notebookModel = this._instantiationService.createInstance(NotebookTextModel, viewType, uri, data.cells, data.metadata, transientOptions); this._models.set(uri, new ModelData(notebookModel, this._onWillDisposeDocument.bind(this))); - this._onDidCreateNotebookDocument.fire(notebookModel); + this._onWillAddNotebookDocument.fire(notebookModel); this._onDidAddNotebookDocument.fire(notebookModel); return notebookModel; } @@ -569,6 +566,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd private _onWillDisposeDocument(model: INotebookTextModel): void { const modelData = this._models.get(model.uri); if (modelData) { + this._onWillRemoveNotebookDocument.fire(modelData.model); this._models.delete(model.uri); modelData.dispose(); this._onDidRemoveNotebookDocument.fire(modelData.model); @@ -599,14 +597,14 @@ export class NotebookService extends Disposable implements INotebookService, IEd orderMimeTypes.push({ mimeType: mimeType, rendererId: handler.id, - isTrusted: textModel.metadata.trusted + isTrusted: true }); for (let i = 1; i < handlers.length; i++) { orderMimeTypes.push({ mimeType: mimeType, rendererId: handlers[i].id, - isTrusted: textModel.metadata.trusted + isTrusted: true }); } @@ -614,7 +612,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd orderMimeTypes.push({ mimeType: mimeType, rendererId: BUILTIN_RENDERER_ID, - isTrusted: mimeTypeIsAlwaysSecure(mimeType) || textModel.metadata.trusted + isTrusted: mimeTypeIsAlwaysSecure(mimeType) || this.workspaceTrustManagementService.isWorkpaceTrusted() }); } } else { @@ -622,13 +620,13 @@ export class NotebookService extends Disposable implements INotebookService, IEd orderMimeTypes.push({ mimeType: mimeType, rendererId: BUILTIN_RENDERER_ID, - isTrusted: mimeTypeIsAlwaysSecure(mimeType) || textModel.metadata.trusted + isTrusted: mimeTypeIsAlwaysSecure(mimeType) || this.workspaceTrustManagementService.isWorkpaceTrusted() }); } else { orderMimeTypes.push({ mimeType: mimeType, rendererId: RENDERER_NOT_AVAILABLE, - isTrusted: textModel.metadata.trusted + isTrusted: true }); } } @@ -641,7 +639,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd return this._notebookRenderersInfoStore.getContributedRenderer(mimeType, kernelProvides); } - getContributedNotebookProviders(resource?: URI): readonly NotebookProviderInfo[] { + getContributedNotebookTypes(resource?: URI): readonly NotebookProviderInfo[] { if (resource) { return this._notebookProviderInfoStore.getContributedNotebook(resource); } @@ -649,7 +647,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd return [...this._notebookProviderInfoStore]; } - getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined { + getContributedNotebookType(viewType: string): NotebookProviderInfo | undefined { return this._notebookProviderInfoStore.get(viewType); } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index f10680f32981..5584ff939771 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -24,8 +24,8 @@ import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/ import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { clamp } from 'vs/base/common/numbers'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ISplice } from 'vs/base/common/sequence'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; export interface IFocusNextPreviousDelegate { onFocusNext(applyFocusNext: () => void): void; @@ -91,10 +91,13 @@ export class NotebookCellList extends WorkbenchList implements ID private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; + private readonly _viewContext: ViewContext; + constructor( private listUser: string, parentContainer: HTMLElement, container: HTMLElement, + viewContext: ViewContext, delegate: IListVirtualDelegate, renderers: IListRenderer[], contextKeyService: IContextKeyService, @@ -106,6 +109,7 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); + this._viewContext = viewContext; this._focusNextPreviousDelegate = options.focusNextPreviousDelegate; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { @@ -177,7 +181,7 @@ export class NotebookCellList extends WorkbenchList implements ID this._localDisposableStore.add(this.view.onMouseDblClick(() => { const focus = this.getFocusedElements()[0]; - if (focus && focus.cellKind === CellKind.Markdown && !focus.metadata?.inputCollapsed) { + if (focus && focus.cellKind === CellKind.Markup && !focus.metadata.inputCollapsed) { focus.updateEditState(CellEditState.Editing, 'dbclick'); focus.focusMode = CellFocusMode.Editor; } @@ -900,7 +904,8 @@ export class NotebookCellList extends WorkbenchList implements ID } getViewScrollBottom() { - return this.getViewScrollTop() + this.view.renderHeight - SCROLLABLE_ELEMENT_PADDING_TOP; + const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight; } private _revealRange(viewIndex: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { @@ -1279,6 +1284,13 @@ export class NotebookCellList extends WorkbenchList implements ID this._viewModelStore.dispose(); this._localDisposableStore.dispose(); super.dispose(); + + // un-ref + this._previousFocusedElements = []; + this._viewModel = null; + this._hiddenRangeIds = []; + this.hiddenRangesPrefixSum = null; + this._visibleRanges = []; } } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index c44bf609b456..739111572324 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,82 +4,70 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { URI } from 'vs/base/common/uri'; +import { dispose } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; export class OutputRenderer { - protected readonly _contributions: { [key: string]: IOutputTransformContribution; }; - protected readonly _renderers: IOutputTransformContribution[]; - private _richMimeTypeRenderers = new Map(); + + private readonly _richMimeTypeRenderers = new Map(); constructor( notebookEditor: ICommonNotebookEditor, - private readonly instantiationService: IInstantiationService + instantiationService: IInstantiationService ) { - this._contributions = {}; - this._renderers = []; - - const contributions = NotebookRegistry.getOutputTransformContributions(); - - for (const desc of contributions) { + for (const desc of OutputRendererRegistry.getOutputTransformContributions()) { try { - const contribution = this.instantiationService.createInstance(desc.ctor, notebookEditor); - this._contributions[desc.id] = contribution; - contribution.getMimetypes().forEach(mimetype => { - this._richMimeTypeRenderers.set(mimetype, contribution); - }); + const contribution = instantiationService.createInstance(desc.ctor, notebookEditor); + contribution.getMimetypes().forEach(mimetype => { this._richMimeTypeRenderers.set(mimetype, contribution); }); } catch (err) { onUnexpectedError(err); } } } - getContribution(preferredMimeType: string | undefined): IOutputTransformContribution | undefined { - if (preferredMimeType) { - return this._richMimeTypeRenderers.get(preferredMimeType); - } + dispose(): void { + dispose(this._richMimeTypeRenderers.values()); + this._richMimeTypeRenderers.clear(); + } - return undefined; + getContribution(preferredMimeType: string): IOutputTransformContribution | undefined { + return this._richMimeTypeRenderers.get(preferredMimeType); } - renderNoop(viewModel: ICellOutputViewModel, container: HTMLElement): IRenderOutput { + private _renderMessage(container: HTMLElement, message: string): IRenderOutput { const contentNode = document.createElement('p'); - - contentNode.innerText = `No renderer could be found for output.`; + contentNode.innerText = message; container.appendChild(contentNode); return { type: RenderOutputType.Mainframe }; } - render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { if (!viewModel.model.outputs.length) { - return this.renderNoop(viewModel, container); + return this._renderMessage(container, localize('empty', "Cell has no output")); } - - if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { - const contentNode = document.createElement('p'); + if (!preferredMimeType) { const mimeTypes = viewModel.model.outputs.map(op => op.mime); - const mimeTypesMessage = mimeTypes.join(', '); - + return this._renderMessage(container, localize('noRenderer.2', "No renderer could be found for output. It has the following MIME types: {0}", mimeTypesMessage)); + } + if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { if (preferredMimeType) { - contentNode.innerText = `No renderer could be found for MIME type: ${preferredMimeType}`; - } else { - contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; + return this._renderMessage(container, localize('noRenderer.1', "No renderer could be found for MIME type: {0}", preferredMimeType)); } - - container.appendChild(contentNode); - return { type: RenderOutputType.Mainframe }; } - const renderer = this._richMimeTypeRenderers.get(preferredMimeType); - const items = viewModel.model.outputs.filter(op => op.mime === preferredMimeType); - - if (items.length && renderer) { - return renderer.render(viewModel, items, container, notebookUri); - } else { - return this.renderNoop(viewModel, container); + if (!renderer) { + return this._renderMessage(container, localize('noRenderer.1', "No renderer could be found for MIME type: {0}", preferredMimeType)); } + const first = viewModel.model.outputs.find(op => op.mime === preferredMimeType); + if (!first) { + return this._renderMessage(container, localize('empty', "Cell has no output")); + } + + return renderer.render(viewModel, [first], container, notebookUri); } } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts similarity index 66% rename from lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts rename to lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts index c382e015505d..574aa94d0155 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts @@ -9,20 +9,18 @@ import { ICommonNotebookEditor, IOutputTransformContribution } from 'vs/workbenc export type IOutputTransformCtor = IConstructorSignature1; export interface IOutputTransformDescription { - id: string; ctor: IOutputTransformCtor; } +export const OutputRendererRegistry = new class NotebookRegistryImpl { -export const NotebookRegistry = new class NotebookRegistryImpl { + readonly #outputTransforms: IOutputTransformDescription[] = []; - readonly outputTransforms: IOutputTransformDescription[] = []; - - registerOutputTransform(id: string, ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { - this.outputTransforms.push({ id: id, ctor: ctor as IOutputTransformCtor }); + registerOutputTransform(ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { + this.#outputTransforms.push({ ctor: ctor as IOutputTransformCtor }); } getOutputTransformContributions(): IOutputTransformDescription[] { - return this.outputTransforms.slice(0); + return this.#outputTransforms.slice(0); } }; diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 4285b38c02a0..f8daffff9c19 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -4,81 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { dirname } from 'vs/base/common/resources'; -import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution as IOutputRendererContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; import { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -function getStringValue(data: unknown): string { - return isArray(data) ? data.join('') : String(data); -} - -class JSONRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } - - getMimetypes() { - return ['application/json']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IModelService private readonly modelService: IModelService, - @IModeService private readonly modeService: IModeService, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const str = items.map(item => JSON.stringify(item.value, null, '\t')).join(''); - - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getOutputSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - }, - automaticLayout: true, - }, { - isSimpleWidget: true - }); - - const mode = this.modeService.create('json'); - const resource = URI.parse(`notebook-output-${Date.now()}.json`); - const textModel = this.modelService.createModel(str, mode, resource, false); - editor.setModel(textModel); - - const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; - const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; - const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); - - editor.layout({ - height, - width - }); - - container.style.height = `${height + 8}px`; - - return { type: RenderOutputType.Mainframe, initHeight: height }; - } -} class JavaScriptRendererContrib extends Disposable implements IOutputRendererContribution { getType() { @@ -95,11 +39,10 @@ class JavaScriptRendererContrib extends Disposable implements IOutputRendererCon super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { let scriptVal = ''; items.forEach(item => { - const data = item.value; - const str = isArray(data) ? data.join('') : data; + const str = getStringValue(item); scriptVal += ``; }); @@ -129,35 +72,43 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const str = items.map(item => getStringValue(item.value)).join(''); - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getOutputSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - } - }, { - isSimpleWidget: true - }); + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement): IRenderOutput { + const value = items.map(getStringValue).join(''); + return this._render(output, container, value, 'javascript'); + } + + protected _render(output: ICellOutputViewModel, container: HTMLElement, value: string, modeId: string): IRenderOutput { + const disposable = new DisposableStore(); + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, getOutputSimpleEditorOptions(), { isSimpleWidget: true }); - const mode = this.modeService.create('javascript'); - const resource = URI.parse(`notebook-output-${Date.now()}.js`); - const textModel = this.modelService.createModel(str, mode, resource, false); + const mode = this.modeService.create(modeId); + const textModel = this.modelService.createModel(value, mode, undefined, false); editor.setModel(textModel); const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); - editor.layout({ - height, - width - }); + editor.layout({ height, width }); + + disposable.add(editor); + disposable.add(textModel); container.style.height = `${height + 8}px`; - return { type: RenderOutputType.Mainframe }; + return { type: RenderOutputType.Mainframe, initHeight: height, disposable }; + } +} + +class JSONRendererContrib extends CodeRendererContrib { + + override getMimetypes() { + return ['application/json']; + } + + override render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement): IRenderOutput { + const str = items.map(getStringValue).join(''); + return this._render(output, container, str, 'jsonc'); } } @@ -167,26 +118,25 @@ class StreamRendererContrib extends Disposable implements IOutputRendererContrib } getMimetypes() { - return ['application/x.notebook.stdout', 'application/x.notebook.stream']; + return ['application/vnd.code.notebook.stdout', 'application/x.notebook.stdout', 'application/x.notebook.stream']; } constructor( public notebookEditor: ICommonNotebookEditor, @IOpenerService private readonly openerService: IOpenerService, @IThemeService private readonly themeService: IThemeService, - @ITextFileService private readonly textFileService: ITextFileService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { const linkDetector = this.instantiationService.createInstance(LinkDetector); items.forEach(item => { - const text = getStringValue(item.value); + const text = getStringValue(item); const contentNode = DOM.$('span.output-stream'); - truncatedArrayOfString(contentNode, [text], linkDetector, this.openerService, this.textFileService, this.themeService); + truncatedArrayOfString(notebookUri!, output.cellViewModel, contentNode, [text], linkDetector, this.openerService, this.themeService); container.appendChild(contentNode); }); @@ -200,64 +150,68 @@ class StderrRendererContrib extends StreamRendererContrib { } override getMimetypes() { - return ['application/x.notebook.stderr']; + return ['application/vnd.code.notebook.stderr', 'application/x.notebook.stderr']; } - override render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { + override render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { const result = super.render(output, items, container, notebookUri); container.classList.add('error'); return result; } } -class ErrorRendererContrib extends Disposable implements IOutputRendererContribution { +class JSErrorRendererContrib implements IOutputRendererContribution { + + constructor( + public notebookEditor: ICommonNotebookEditor, + @IThemeService private readonly _themeService: IThemeService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { } + + dispose(): void { + // nothing + } + getType() { return RenderOutputType.Mainframe; } getMimetypes() { - return ['application/x.notebook.error-traceback']; + return ['application/vnd.code.notebook.error']; } - constructor( - public notebookEditor: ICommonNotebookEditor, - @IThemeService private readonly themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + render(_output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, _notebookUri: URI): IRenderOutput { + const linkDetector = this._instantiationService.createInstance(LinkDetector); - ) { - super(); - } + type ErrorLike = Partial; + + for (let item of items) { + let err: ErrorLike; + try { + err = JSON.parse(getStringValue(item)); + } catch (e) { + this._logService.warn('INVALID output item (failed to parse)', e); + continue; + } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const linkDetector = this.instantiationService.createInstance(LinkDetector); - items.forEach(item => { - const data: any = item.value; const header = document.createElement('div'); - const headerMessage = data.ename && data.evalue - ? `${data.ename}: ${data.evalue}` - : data.ename || data.evalue; + const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message; if (headerMessage) { header.innerText = headerMessage; container.appendChild(header); } - const traceback = document.createElement('pre'); - traceback.classList.add('traceback'); - if (data.traceback) { - for (let j = 0; j < data.traceback.length; j++) { - traceback.appendChild(handleANSIOutput(data.traceback[j], linkDetector, this.themeService, undefined)); - } + const stack = document.createElement('pre'); + stack.classList.add('traceback'); + if (err.stack) { + stack.appendChild(handleANSIOutput(err.stack, linkDetector, this._themeService, undefined)); } - container.appendChild(traceback); + container.appendChild(stack); container.classList.add('error'); - return { type: RenderOutputType.Mainframe }; - - }); + } return { type: RenderOutputType.Mainframe }; } - - _render() { - } } class PlainTextRendererContrib extends Disposable implements IOutputRendererContribution { @@ -273,18 +227,17 @@ class PlainTextRendererContrib extends Disposable implements IOutputRendererCont public notebookEditor: ICommonNotebookEditor, @IOpenerService private readonly openerService: IOpenerService, @IThemeService private readonly themeService: IThemeService, - @ITextFileService private readonly textFileService: ITextFileService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { const linkDetector = this.instantiationService.createInstance(LinkDetector); - const str = items.map(item => getStringValue(item.value)); + const str = items.map(getStringValue); const contentNode = DOM.$('.output-plaintext'); - truncatedArrayOfString(contentNode, str, linkDetector, this.openerService, this.textFileService, this.themeService); + truncatedArrayOfString(notebookUri!, output.cellViewModel, contentNode, str, linkDetector, this.openerService, this.themeService); container.appendChild(contentNode); return { type: RenderOutputType.Mainframe, supportAppend: true }; @@ -297,7 +250,7 @@ class HTMLRendererContrib extends Disposable implements IOutputRendererContribut } getMimetypes() { - return ['text/html']; + return ['text/html', 'image/svg+xml']; } constructor( @@ -306,35 +259,8 @@ class HTMLRendererContrib extends Disposable implements IOutputRendererContribut super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const data = items.map(item => getStringValue(item.value)).join(''); - - const str = (isArray(data) ? data.join('') : data) as string; - return { - type: RenderOutputType.Html, - source: output, - htmlContent: str - }; - } -} - -class SVGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Html; - } - - getMimetypes() { - return ['image/svg+xml']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - const str = items.map(item => getStringValue(item.value)).join(''); + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { + const str = items.map(getStringValue).join(''); return { type: RenderOutputType.Html, source: output, @@ -360,26 +286,26 @@ class MdRendererContrib extends Disposable implements IOutputRendererContributio } render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - items.forEach(item => { - const data = item.value; - const str = (isArray(data) ? data.join('') : data) as string; + const disposable = new DisposableStore(); + for (let item of items) { + const str = getStringValue(item); const mdOutput = document.createElement('div'); const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }, undefined, { gfm: true }).element); container.appendChild(mdOutput); - }); - - return { type: RenderOutputType.Mainframe }; + disposable.add(mdRenderer); + } + return { type: RenderOutputType.Mainframe, disposable }; } } -class PNGRendererContrib extends Disposable implements IOutputRendererContribution { +class ImgRendererContrib extends Disposable implements IOutputRendererContribution { getType() { return RenderOutputType.Mainframe; } getMimetypes() { - return ['image/png']; + return ['image/png', 'image/jpeg', 'image/gif']; } constructor( @@ -388,65 +314,47 @@ class PNGRendererContrib extends Disposable implements IOutputRendererContributi super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - items.forEach(item => { - const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/png;base64,${imagedata}`; - const display = document.createElement('div'); - display.classList.add('display'); - display.appendChild(image); - container.appendChild(display); - }); - return { type: RenderOutputType.Mainframe }; - } -} + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { + const disposable = new DisposableStore(); -class JPEGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } + for (let item of items) { - getMimetypes() { - return ['image/jpeg']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - ) { - super(); - } + const bytes = new Uint8Array(item.valueBytes); + const blob = new Blob([bytes], { type: item.mime }); + const src = URL.createObjectURL(blob); + disposable.add(toDisposable(() => URL.revokeObjectURL(src))); - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI | undefined): IRenderOutput { - items.forEach(item => { const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/jpeg;base64,${imagedata}`; + image.src = src; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); container.appendChild(display); - }); - - return { type: RenderOutputType.Mainframe }; + } + return { type: RenderOutputType.Mainframe, disposable }; } } -NotebookRegistry.registerOutputTransform('json', JSONRendererContrib); -NotebookRegistry.registerOutputTransform('javascript', JavaScriptRendererContrib); -NotebookRegistry.registerOutputTransform('html', HTMLRendererContrib); -NotebookRegistry.registerOutputTransform('svg', SVGRendererContrib); -NotebookRegistry.registerOutputTransform('markdown', MdRendererContrib); -NotebookRegistry.registerOutputTransform('png', PNGRendererContrib); -NotebookRegistry.registerOutputTransform('jpeg', JPEGRendererContrib); -NotebookRegistry.registerOutputTransform('plain', PlainTextRendererContrib); -NotebookRegistry.registerOutputTransform('code', CodeRendererContrib); -NotebookRegistry.registerOutputTransform('error-trace', ErrorRendererContrib); -NotebookRegistry.registerOutputTransform('stream-text', StreamRendererContrib); -NotebookRegistry.registerOutputTransform('stderr', StderrRendererContrib); - -export function getOutputSimpleEditorOptions(): IEditorOptions { +OutputRendererRegistry.registerOutputTransform(JSONRendererContrib); +OutputRendererRegistry.registerOutputTransform(JavaScriptRendererContrib); +OutputRendererRegistry.registerOutputTransform(HTMLRendererContrib); +OutputRendererRegistry.registerOutputTransform(MdRendererContrib); +OutputRendererRegistry.registerOutputTransform(ImgRendererContrib); +OutputRendererRegistry.registerOutputTransform(PlainTextRendererContrib); +OutputRendererRegistry.registerOutputTransform(CodeRendererContrib); +OutputRendererRegistry.registerOutputTransform(JSErrorRendererContrib); +OutputRendererRegistry.registerOutputTransform(StreamRendererContrib); +OutputRendererRegistry.registerOutputTransform(StderrRendererContrib); + +// --- utils --- +function getStringValue(item: IOutputItemDto): string { + // todo@jrieken NOT proper, should be VSBuffer + return new TextDecoder().decode(new Uint8Array(item.valueBytes)); +} + +function getOutputSimpleEditorOptions(): IEditorConstructionOptions { return { + dimension: { height: 0, width: 0 }, readOnly: true, wordWrap: 'on', overviewRulerLanes: 0, @@ -464,6 +372,7 @@ export function getOutputSimpleEditorOptions(): IEditorOptions { lineNumbers: 'off', scrollbar: { alwaysConsumeMouseWheel: false - } + }, + automaticLayout: true, }; } diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts index 397636c09f45..81c0c88a7d02 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts @@ -12,14 +12,17 @@ import { Range } from 'vs/editor/common/core/range'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IGenericCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; const SIZE_LIMIT = 65535; const LINES_LIMIT = 500; -function generateViewMoreElement(outputs: string[], openerService: IOpenerService, textFileService: ITextFileService) { +function generateViewMoreElement(notebookUri: URI, cellViewModel: IGenericCellViewModel, outputs: string[], openerService: IOpenerService) { const md: IMarkdownString = { value: '[show more (open the raw output data in a text editor) ...](command:workbench.action.openLargeOutput)', isTrusted: true, @@ -30,14 +33,7 @@ function generateViewMoreElement(outputs: string[], openerService: IOpenerServic actionHandler: { callback: (content) => { if (content === 'command:workbench.action.openLargeOutput') { - return textFileService.untitled.resolve({ - associatedResource: undefined, - mode: 'plaintext', - initialValue: outputs.join('') - }).then(model => { - const resource = model.resource; - openerService.open(resource); - }); + openerService.open(CellUri.generateCellUri(notebookUri, cellViewModel.handle, Schemas.vscodeNotebookCellOutput)); } return; @@ -50,7 +46,7 @@ function generateViewMoreElement(outputs: string[], openerService: IOpenerServic return element; } -export function truncatedArrayOfString(container: HTMLElement, outputs: string[], linkDetector: LinkDetector, openerService: IOpenerService, textFileService: ITextFileService, themeService: IThemeService) { +export function truncatedArrayOfString(notebookUri: URI, cellViewModel: IGenericCellViewModel, container: HTMLElement, outputs: string[], linkDetector: LinkDetector, openerService: IOpenerService, themeService: IThemeService) { const fullLen = outputs.reduce((p, c) => { return p + c.length; }, 0); @@ -68,7 +64,7 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const truncatedText = buffer.getValueInRange(new Range(1, 1, sizeBufferLimitPosition.lineNumber, sizeBufferLimitPosition.column), EndOfLinePreference.TextDefined); container.appendChild(handleANSIOutput(truncatedText, linkDetector, themeService, undefined)); // view more ... - container.appendChild(generateViewMoreElement(outputs, openerService, textFileService)); + container.appendChild(generateViewMoreElement(notebookUri, cellViewModel, outputs, openerService)); return; } } @@ -92,7 +88,7 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), linkDetector, themeService, undefined)); // view more ... - container.appendChild(generateViewMoreElement(outputs, openerService, textFileService)); + container.appendChild(generateViewMoreElement(notebookUri, cellViewModel, outputs, openerService)); const lineCount = buffer.getLineCount(); const pre2 = DOM.$('div'); diff --git a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 3cc75f4b805e..a04712afd062 100644 --- a/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/lib/vscode/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -24,14 +24,15 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { PreloadOptions, preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; -import { INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; -import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; interface BaseToWebviewMessage { @@ -131,17 +132,13 @@ export interface IToggleMarkdownPreviewMessage extends BaseToWebviewMessage { export interface ICellDragStartMessage extends BaseToWebviewMessage { type: 'cell-drag-start'; readonly cellId: string; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDragMessage extends BaseToWebviewMessage { type: 'cell-drag'; readonly cellId: string; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDropMessage extends BaseToWebviewMessage { @@ -149,9 +146,7 @@ export interface ICellDropMessage extends BaseToWebviewMessage { readonly cellId: string; readonly ctrlKey: boolean readonly altKey: boolean; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDragEndMessage extends BaseToWebviewMessage { @@ -197,15 +192,15 @@ export interface ICreationRequestMessage { type: 'html'; content: | { type: RenderOutputType.Html; htmlContent: string } - | { type: RenderOutputType.Extension; outputId: string; value: unknown; metadata: unknown; mimeType: string }; + | { type: RenderOutputType.Extension; outputId: string; valueBytes: Uint8Array, metadata: unknown; metadata2: unknown, mimeType: string }; cellId: string; outputId: string; cellTop: number; outputOffset: number; left: number; - requiredPreloads: ReadonlyArray; + requiredPreloads: ReadonlyArray; readonly initiallyHidden?: boolean; - apiNamespace?: string | undefined; + rendererId?: string | undefined; } export interface IContentWidgetTopRequest { @@ -234,7 +229,7 @@ export interface IClearOutputRequestMessage { cellId: string; outputId: string; cellUri: string; - apiNamespace: string | undefined; + rendererId: string | undefined; } export interface IHideOutputMessage { @@ -263,15 +258,15 @@ export interface IAckOutputHeightMessage { height: number; } -export interface IPreloadResource { + +export interface IControllerPreload { originalUri: string; uri: string; } -export interface IUpdatePreloadResourceMessage { +export interface IUpdateControllerPreloadsMessage { type: 'preload'; - resources: IPreloadResource[]; - source: 'renderer' | 'kernel'; + resources: IControllerPreload[]; } export interface IUpdateDecorationsMessage { @@ -281,6 +276,11 @@ export interface IUpdateDecorationsMessage { removedClassNames: string[]; } +export interface ICustomKernelMessage extends BaseToWebviewMessage { + type: 'customKernelMessage'; + message: unknown; +} + export interface ICustomRendererMessage extends BaseToWebviewMessage { type: 'customRendererMessage'; rendererId: string; @@ -289,10 +289,7 @@ export interface ICustomRendererMessage extends BaseToWebviewMessage { export interface ICreateMarkdownMessage { type: 'createMarkdownPreview', - id: string; - handle: number; - content: string; - top: number; + cell: IMarkdownCellInitialization; } export interface IDeleteMarkdownMessage { type: 'deleteMarkdownPreview', @@ -322,9 +319,29 @@ export interface IUpdateSelectedMarkdownPreviews { readonly selectedCellIds: readonly string[] } +export interface IMarkdownCellInitialization { + cellId: string; + cellHandle: number; + content: string; + offset: number; + visible: boolean; +} + export interface IInitializeMarkdownMessage { type: 'initializeMarkdownPreview'; - cells: Array<{ cellId: string, cellHandle: number, content: string, offset: number }>; + cells: ReadonlyArray; +} + +export interface INotebookStylesMessage { + type: 'notebookStyles'; + styles: { + [key: string]: string; + }; +} + +export interface INotebookOptionsMessage { + type: 'notebookOptions'; + options: PreloadOptions; } export type FromWebviewMessage = @@ -337,6 +354,7 @@ export type FromWebviewMessage = | IWheelMessage | IScrollAckMessage | IBlurOutputMessage + | ICustomKernelMessage | ICustomRendererMessage | IClickedDataUrlMessage | IClickMarkdownPreviewMessage @@ -363,8 +381,9 @@ export type ToWebviewMessage = | IClearOutputRequestMessage | IHideOutputMessage | IShowOutputMessage - | IUpdatePreloadResourceMessage + | IUpdateControllerPreloadsMessage | IUpdateDecorationsMessage + | ICustomKernelMessage | ICustomRendererMessage | ICreateMarkdownMessage | IDeleteMarkdownMessage @@ -372,7 +391,9 @@ export type ToWebviewMessage = | IHideMarkdownMessage | IUnhideMarkdownMessage | IUpdateSelectedMarkdownPreviews - | IInitializeMarkdownMessage; + | IInitializeMarkdownMessage + | INotebookStylesMessage + | INotebookOptionsMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; @@ -393,7 +414,6 @@ function html(strings: TemplateStringsArray, ...values: any[]): string { export interface INotebookWebviewMessage { message: unknown; - forRenderer?: string; } export interface IResolvedBackLayerWebview { @@ -404,31 +424,34 @@ export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement | undefined = undefined; insetMapping: Map> = new Map(); - readonly markdownPreviewMapping = new Map(); + readonly markdownPreviewMapping = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; - kernelRootsCache: URI[] = []; private readonly _onMessage = this._register(new Emitter()); private readonly _preloadsCache = new Set(); public readonly onMessage: Event = this._onMessage.event; - private _loaded!: Promise; private _initalized?: Promise; private _disposed = false; + private _currentKernel?: INotebookKernel; constructor( - public notebookEditor: ICommonNotebookEditor, - public id: string, - public documentUri: URI, - public options: { + public readonly notebookEditor: ICommonNotebookEditor, + public readonly id: string, + public readonly documentUri: URI, + private options: { outputNodePadding: number, outputNodeLeftPadding: number, previewNodePadding: number, + markdownLeftMargin: number, leftMargin: number, rightMargin: number, runGutter: number, + dragAndDropEnabled: boolean, + fontSize: number }, + private readonly rendererMessaging: IScopedRendererMessaging | undefined, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @@ -447,11 +470,67 @@ export class BackLayerWebView extends Disposable { this.element.style.height = '1400px'; this.element.style.position = 'absolute'; + + if (rendererMessaging) { + this._register(rendererMessaging.onDidReceiveMessage(evt => { + this._sendMessageToWebview({ + __vscode_notebook_message: true, + type: 'customRendererMessage', + rendererId: evt.rendererId, + message: evt.message + }); + })); + } + } + + updateOptions(options: { + outputNodePadding: number, + outputNodeLeftPadding: number, + previewNodePadding: number, + markdownLeftMargin: number, + leftMargin: number, + rightMargin: number, + runGutter: number, + dragAndDropEnabled: boolean, + fontSize: number + }) { + this.options = options; + this._updateStyles(); + this._updateOptions(); + } + + private _updateStyles() { + this._sendMessageToWebview({ + type: 'notebookStyles', + styles: this._generateStyles() + }); } + + private _updateOptions() { + this._sendMessageToWebview({ + type: 'notebookOptions', + options: { + dragAndDropEnabled: this.options.dragAndDropEnabled + } + }); + } + + private _generateStyles() { + return { + 'notebook-output-left-margin': `${this.options.leftMargin + this.options.runGutter}px`, + 'notebook-output-width': `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`, + 'notebook-output-node-padding': `${this.options.outputNodePadding}px`, + 'notebook-run-gutter': `${this.options.runGutter}px`, + 'notebook-preivew-node-padding': `${this.options.previewNodePadding}px`, + 'notebook-markdown-left-margin': `${this.options.markdownLeftMargin}px`, + 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, + 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, + 'notebook-cell-output-font-size': `${this.options.fontSize}px` + }; + } + private generateContent(coreDependencies: string, baseUrl: string) { - const markupRenderer = this.getMarkdownRenderer(); - const outputWidth = `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`; - const outputMarginLeft = `${this.options.leftMargin + this.options.runGutter}px`; + const renderersData = this.getRendererData(); return html` @@ -533,7 +612,7 @@ export class BackLayerWebView extends Disposable { /* makes all markdown cells consistent */ div { - min-height: ${this.options.previewNodePadding * 2}px; + min-height: var(--notebook-markdown-min-height); } table { @@ -580,22 +659,8 @@ export class BackLayerWebView extends Disposable { white-space: pre-wrap; } - .latex-block { - display: block; - } - - .latex { - vertical-align: middle; - display: inline-block; - } - - .latex img, - .latex-block img { - filter: brightness(0) invert(0) - } - dragging { - background-color: var(--vscode-editor-background); + background-color: var(--theme-background); } @@ -609,31 +674,37 @@ export class BackLayerWebView extends Disposable { } #container > div > div > div.output { - width: ${outputWidth}; - margin-left: ${outputMarginLeft}; - padding: ${this.options.outputNodePadding}px ${this.options.outputNodePadding}px ${this.options.outputNodePadding}px ${this.options.outputNodeLeftPadding}px; + font-size: var(--notebook-cell-output-font-size); + width: var(--notebook-output-width); + margin-left: var(--notebook-output-left-margin); + padding-top: var(--notebook-output-node-padding); + padding-right: var(--notebook-output-node-padding); + padding-bottom: var(--notebook-output-node-padding); + padding-left: var(--notebook-output-node-left-padding); box-sizing: border-box; - background-color: var(--vscode-notebook-outputContainerBackgroundColor); + background-color: var(--theme-notebook-output-background); } /* markdown */ #container > div.preview { width: 100%; - padding-right: ${this.options.previewNodePadding}px; - padding-left: ${this.options.leftMargin}px; - padding-top: ${this.options.previewNodePadding}px; - padding-bottom: ${this.options.previewNodePadding}px; + padding-right: var(--notebook-preivew-node-padding); + padding-left: var(--notebook-markdown-left-margin); + padding-top: var(--notebook-preivew-node-padding); + padding-bottom: var(--notebook-preivew-node-padding); box-sizing: border-box; white-space: nowrap; overflow: hidden; + white-space: initial; + color: var(--theme-ui-foreground); + } + + #container > div.preview.draggable { user-select: none; -webkit-user-select: none; -ms-user-select: none; - white-space: initial; cursor: grab; - - color: var(--vscode-foreground); } #container > div.preview.emptyMarkdownCell::before { @@ -643,11 +714,11 @@ export class BackLayerWebView extends Disposable { } #container > div.preview.selected { - background: var(--vscode-notebook-selectedCellBackground); + background: var(--theme-notebook-cell-selected-background); } #container > div.preview.dragging { - background-color: var(--vscode-editor-background); + background-color: var(--theme-background); } .monaco-workbench.vs-dark .notebookOverlay .cell.markdown .latex img, @@ -655,16 +726,16 @@ export class BackLayerWebView extends Disposable { filter: brightness(0) invert(1) } - #container > div.nb-symbolHighlight > div { - background-color: var(--vscode-notebook-symbolHighlightBackground); + #container > div.nb-symbolHighlight { + background-color: var(--theme-notebook-symbol-highlight-background); } - #container > div.nb-cellDeleted > div { - background-color: var(--vscode-diffEditor-removedTextBackground); + #container > div.nb-cellDeleted { + background-color: var(--theme-notebook-diff-removed-background); } - #container > div.nb-cellAdded > div { - background-color: var(--vscode-diffEditor-insertedTextBackground); + #container > div.nb-cellAdded { + background-color: var(--theme-notebook-diff-inserted-background); } #container > div > div:not(.preview) > div { @@ -717,50 +788,33 @@ export class BackLayerWebView extends Disposable { ${coreDependencies}
    - + `; } - private getMarkdownRenderer(): Array<{ entrypoint: string, dependencies: Array<{ entrypoint: string }> }> { - const allRenderers = this.notebookService.getMarkupRendererInfo(); - - const topLevelMarkdownRenderers = allRenderers - .filter(renderer => !renderer.dependsOn) - .filter(renderer => renderer.mimeTypes?.includes('text/markdown')); - - const subRenderers = new Map>(); - for (const renderer of allRenderers) { - if (renderer.dependsOn) { - if (!subRenderers.has(renderer.dependsOn)) { - subRenderers.set(renderer.dependsOn, []); - } - const entryPoint = asWebviewUri(this.environmentService, this.id, renderer.entrypoint); - subRenderers.get(renderer.dependsOn)!.push({ entrypoint: entryPoint.toString(true) }); - } - } - - return topLevelMarkdownRenderers.map((renderer) => { - const src = asWebviewUri(this.environmentService, this.id, renderer.entrypoint); + private getRendererData(): RendererMetadata[] { + return this.notebookService.getRenderers().map((renderer): RendererMetadata => { + const entrypoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation).toString(); return { - entrypoint: src.toString(), - dependencies: subRenderers.get(renderer.id) || [], + id: renderer.id, + entrypoint, + mimeTypes: renderer.mimeTypes, + extends: renderer.extends, + messaging: renderer.messaging !== RendererMessagingSpec.Never, }; }); } - postRendererMessage(rendererId: string, message: any) { + private asWebviewUri(uri: URI, fromExtension: URI | undefined) { + return asWebviewUri(uri, fromExtension?.scheme === Schemas.vscodeRemote ? { isRemote: true, authority: fromExtension.authority } : undefined); + } + + postKernelMessage(message: any) { this._sendMessageToWebview({ __vscode_notebook_message: true, - type: 'customRendererMessage', + type: 'customKernelMessage', message, - rendererId }); } @@ -779,6 +833,16 @@ export class BackLayerWebView extends Disposable { } async createWebview(): Promise { + const baseUrl = this.asWebviewUri(dirname(this.documentUri), undefined); + + // Python hasn't moved to use a preload to load require support yet. + // For all other notebooks, we no longer want to include our loader. + if (!this.documentUri.path.toLowerCase().endsWith('.ipynb')) { + const htmlContent = this.generateContent('', baseUrl.toString()); + this._initialize(htmlContent); + return; + } + let coreDependencies = ''; let resolveFunc: () => void; @@ -786,11 +850,10 @@ export class BackLayerWebView extends Disposable { resolveFunc = resolve; }); - const baseUrl = asWebviewUri(this.environmentService, this.id, dirname(this.documentUri)); if (!isWeb) { const loaderUri = FileAccess.asFileUri('vs/loader.js', require); - const loader = asWebviewUri(this.environmentService, this.id, loaderUri); + const loader = this.asWebviewUri(loaderUri, undefined); coreDependencies = ` `; } @@ -570,28 +791,19 @@ export class GettingStartedPage extends EditorPane { const someStepsComplete = this.gettingStartedCategories.some(categry => categry.content.type === 'steps' && categry.content.stepsComplete); if (!someStepsComplete && !this.hasScrolledToFirstCategory) { - const fistContentBehaviour = - !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL) // isNewUser ? - ? 'openToFirstCategory' - : await Promise.race([ - this.tasExperimentService?.getTreatment<'index' | 'openToFirstCategory'>('GettingStartedFirstContent'), - new Promise<'index'>(resolve => setTimeout(() => resolve('index'), 1000)), - ]); - - if (this.gettingStartedCategories.some(category => category.content.type === 'steps' && category.content.stepsComplete)) { - this.setSlide('categories'); - return; - } else { - if (fistContentBehaviour === 'openToFirstCategory') { - const first = this.gettingStartedCategories.find(category => category.content.type === 'steps'); - this.hasScrolledToFirstCategory = true; - if (first) { - this.currentCategory = first; - this.editorInput.selectedCategory = this.currentCategory?.id; - this.buildCategorySlide(this.editorInput.selectedCategory); - this.setSlide('details'); - return; - } + const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL) || new Date().toUTCString(); + const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; + const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; + + if (fistContentBehaviour === 'openToFirstCategory') { + const first = this.gettingStartedCategories.find(category => category.content.type === 'steps'); + this.hasScrolledToFirstCategory = true; + if (first) { + this.currentCategory = first; + this.editorInput.selectedCategory = this.currentCategory?.id; + this.buildCategorySlide(this.editorInput.selectedCategory); + this.setSlide('details'); + return; } } } @@ -674,7 +886,7 @@ export class GettingStartedPage extends EditorPane { $('button.button-link', { 'x-dispatch': 'selectCategory:' + entry.id, - title: entry.description + this.getKeybindingLabel(entry.content.command), + title: entry.description + ' ' + this.getKeybindingLabel(entry.content.command), }, this.iconWidgetFor(entry), $('span', {}, entry.title))); @@ -749,6 +961,8 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.layout(size); this.recentlyOpenedList?.layout(size); + this.layoutMarkdown?.(); + this.container.classList[size.height <= 600 ? 'add' : 'remove']('height-constrained'); this.container.classList[size.width <= 400 ? 'add' : 'remove']('width-constrained'); this.container.classList[size.width <= 800 ? 'add' : 'remove']('width-semi-constrained'); @@ -772,22 +986,26 @@ export class GettingStartedPage extends EditorPane { bar.setAttribute('aria-valuemin', '0'); bar.setAttribute('aria-valuenow', '' + numDone); bar.setAttribute('aria-valuemax', '' + numTotal); - const progress = Math.max((numDone / numTotal) * 100, 3); + const progress = (numDone / numTotal) * 100; bar.style.width = `${progress}%`; + + (element.parentElement as HTMLElement).classList[numDone === 0 ? 'add' : 'remove']('no-progress'); + if (numTotal === numDone) { - bar.title = `All steps complete!`; + bar.title = localize('gettingStarted.allStepsComplete', "All {0} steps complete!", numTotal); } else { - bar.title = `${numDone} of ${numTotal} steps complete`; + bar.title = localize('gettingStarted.someStepsComplete', "{0} of {1} steps complete", numDone, numTotal); } }); } - private async scrollToCategory(categoryID: string) { + private async scrollToCategory(categoryID: string, stepId?: string) { this.inProgressScroll = this.inProgressScroll.then(async () => { reset(this.stepsContent); this.editorInput.selectedCategory = categoryID; + this.editorInput.selectedStep = stepId; this.currentCategory = this.gettingStartedCategories.find(category => category.id === categoryID); this.buildCategorySlide(categoryID); this.setSlide('details'); @@ -845,6 +1063,10 @@ export class GettingStartedPage extends EditorPane { } this.openerService.open(command, { allowCommands: true }); + if (!isCommand && (node.href.startsWith('https://') || node.href.startsWith('http://'))) { + this.gettingStartedService.progressByEvent('onLink:' + node.href); + } + }, null, this.detailsPageDisposables); if (isCommand) { @@ -862,11 +1084,10 @@ export class GettingStartedPage extends EditorPane { if (typeof node === 'string') { append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true })); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(p, link.el); this.detailsPageDisposables.add(link); - this.detailsPageDisposables.add(attachLinkStyler(link, this.themeService)); } } } @@ -931,14 +1152,26 @@ export class GettingStartedPage extends EditorPane { stepDescription); }); - const stepsContainer = $('.getting-started-detail-container', { 'role': 'list' }, ...categoryElements); + const showNextCategory = + this.gettingStartedCategories.find(_category => _category.id === category.next && _category.content.type === 'steps' && !_category.content.done); + + const stepsContainer = $( + '.getting-started-detail-container', { 'role': 'list' }, + ...categoryElements, + $('.done-next-container', {}, + $('button.button-link.all-done', { 'x-dispatch': 'allDone' }, $('span.codicon.codicon-check-all'), localize('allDone', "Mark Done")), + ...(showNextCategory + ? [$('button.button-link.next', { 'x-dispatch': 'nextSection' }, localize('nextOne', "Next Section"), $('span.codicon.codicon-arrow-small-right'))] + : []), + ) + ); this.detailsScrollbar = this._register(new DomScrollableElement(stepsContainer, { className: 'steps-container' })); const stepListComponent = this.detailsScrollbar.getDomNode(); reset(this.stepsContent, categoryDescriptorComponent, stepListComponent, this.stepMediaComponent); const toExpand = category.content.steps.find(step => !step.done) ?? category.content.steps[0]; - this.selectStep(selectedStep ?? toExpand.id, false); + this.selectStep(selectedStep ?? toExpand.id, !selectedStep, true); this.detailsScrollbar.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); @@ -962,6 +1195,7 @@ export class GettingStartedPage extends EditorPane { this.editorInput.selectedStep = undefined; this.selectStep(undefined); this.setSlide('categories'); + this.container.focus(); }); } @@ -983,7 +1217,7 @@ export class GettingStartedPage extends EditorPane { if (allSteps) { const toFind = this.editorInput.selectedStep ?? this.previousSelection; const selectedIndex = allSteps.findIndex(step => step.id === toFind); - if (allSteps[selectedIndex + 1]?.id) { this.selectStep(allSteps[selectedIndex + 1]?.id, true, false); } + if (allSteps[selectedIndex + 1]?.id) { this.selectStep(allSteps[selectedIndex + 1]?.id, false); } } } else { (document.activeElement?.nextElementSibling as HTMLElement)?.focus?.(); @@ -996,7 +1230,7 @@ export class GettingStartedPage extends EditorPane { if (allSteps) { const toFind = this.editorInput.selectedStep ?? this.previousSelection; const selectedIndex = allSteps.findIndex(step => step.id === toFind); - if (allSteps[selectedIndex - 1]?.id) { this.selectStep(allSteps[selectedIndex - 1]?.id, true, false); } + if (allSteps[selectedIndex - 1]?.id) { this.selectStep(allSteps[selectedIndex - 1]?.id, false); } } } else { (document.activeElement?.previousElementSibling as HTMLElement)?.focus?.(); @@ -1011,7 +1245,6 @@ export class GettingStartedPage extends EditorPane { this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = true); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('button').forEach(button => button.disabled = false); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('input').forEach(button => button.disabled = false); - this.container.focus(); } else { slideManager.classList.add('showDetails'); slideManager.classList.remove('showCategories'); @@ -1020,6 +1253,10 @@ export class GettingStartedPage extends EditorPane { this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('input').forEach(button => button.disabled = true); } } + + override focus() { + this.container.focus(); + } } export class GettingStartedInputSerializer implements IEditorInputSerializer { @@ -1052,6 +1289,8 @@ class GettingStartedIndexList extends Disposable { public itemCount: number; + private isDisposed = false; + constructor( title: string, klass: string, @@ -1083,7 +1322,12 @@ class GettingStartedIndexList extends Disposable { this._register(this.onDidChangeEntries(listener)); } - register(d: IDisposable) { this._register(d); } + register(d: IDisposable) { if (this.isDisposed) { d.dispose(); } else { this._register(d); } } + + override dispose() { + this.isDisposed = true; + super.dispose(); + } setLimit(limit: number) { this.limit = limit; @@ -1191,7 +1435,7 @@ registerThemingParticipant((theme, collector) => { if (link) { collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.codicon-close) { color: ${link}; }`); collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link { color: ${link}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .scroll-button { color: ${link}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .codicon { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts index ae51f02cf0a9..c02fe9097599 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint.ts @@ -7,11 +7,13 @@ import { localize } from 'vs/nls'; import { IStartEntry, IWalkthrough } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +const titleTranslated = localize('title', "Title"); + export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'walkthroughs', jsonSchema: { doNotSuggest: true, - description: localize('walkthroughs', "Contribute collections of steps to help users with your extension. Experimental, available in VS Code Insiders only."), + description: localize('walkthroughs', "Contribute walkthroughs to help users getting started with your extension."), type: 'array', items: { type: 'object', @@ -31,8 +33,7 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo description: localize('walkthroughs.description', "Description of walkthrough.") }, primary: { - type: 'boolean', - description: localize('walkthroughs.primary', "if this is a `primary` walkthrough, hinting if it should be opened on install of the extension. The first `primary` walkthough with a `when` condition matching the current context may be opened by core on install of the extension.") + deprecationMessage: localize('walkthroughs.primary.deprecated', "Deprecated. The first walkthrough with a satisfied when condition will be opened on install.") }, when: { type: 'string', @@ -46,11 +47,11 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo description: localize('walkthroughs.steps', "Steps to complete as part of this walkthrough."), items: { type: 'object', - required: ['id', 'title', 'description', 'media'], + required: ['id', 'title', 'media'], defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3', - 'doneOn': { 'command': '$5' }, + 'completionEvents': ['$5'], 'media': { 'path': '$6', 'type': '$7' } } }], @@ -65,10 +66,10 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo }, description: { type: 'string', - description: localize('walkthroughs.steps.description', "Description of step. Supports ``preformatted``, __italic__, and **bold** text. Use markdown-style links for commands or external links: [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms). Links on their own line will be rendered as buttons.") + description: localize('walkthroughs.steps.description.interpolated', "Description of step. Supports ``preformatted``, __italic__, and **bold** text. Use markdown-style links for commands or external links: {0}, {1}, or {2}. Links on their own line will be rendered as buttons.", `[${titleTranslated}](command:myext.command)`, `[${titleTranslated}](command:toSide:myext.command)`, `[${titleTranslated}](https://aka.ms)`) }, button: { - deprecationMessage: localize('walkthroughs.steps.button.deprecated', "Deprecated. Use markdown links in the description instead, i.e. [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms), "), + deprecationMessage: localize('walkthroughs.steps.button.deprecated.interpolated', "Deprecated. Use markdown links in the description instead, i.e. {0}, {1}, or {2}", `[${titleTranslated}](command:myext.command)`, `[${titleTranslated}](command:toSide:myext.command)`, `[${titleTranslated}](https://aka.ms)`), }, media: { type: 'object', @@ -76,10 +77,13 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo defaultSnippets: [{ 'body': { 'type': '$1', 'path': '$2' } }], oneOf: [ { - required: ['path', 'altText'], + required: ['image', 'altText'], additionalProperties: false, properties: { path: { + deprecationMessage: localize('pathDeprecated', "Deprecated. Please use `image` or `markdown` instead") + }, + image: { description: localize('walkthroughs.steps.media.image.path.string', "Path to an image - or object consisting of paths to light, dark, and hc images - relative to extension directory. Depending on context, the image will be displayed from 400px to 800px wide, with similar bounds on height. To support HIDPI displays, the image will be rendered at 1.5x scaling, for example a 900 physical pixels wide image will be displayed as 600 logical pixels wide."), oneOf: [ { @@ -111,10 +115,13 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo } } }, { - required: ['path'], + required: ['markdown'], additionalProperties: false, properties: { path: { + deprecationMessage: localize('pathDeprecated', "Deprecated. Please use `image` or `markdown` instead") + }, + markdown: { description: localize('walkthroughs.steps.media.markdown.path', "Path to the markdown document, relative to extension directory."), type: 'string', } @@ -122,8 +129,53 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo } ] }, + completionEvents: { + description: localize('walkthroughs.steps.completionEvents', "Events that should trigger this step to become checked off. If empty or not defined, the step will check off when any of the step's buttons or links are clicked; if the step has no buttons or links it will check on when it is selected."), + type: 'array', + items: { + type: 'string', + defaultSnippets: [ + { + label: 'onCommand', + description: localize('walkthroughs.steps.completionEvents.onCommand', 'Check off step when a given command is executed anywhere in VS Code.'), + body: 'onCommand:${1:commandId}' + }, + { + label: 'onLink', + description: localize('walkthroughs.steps.completionEvents.onLink', 'Check off step when a given link is opened via a Getting Started step.'), + body: 'onLink:${2:linkId}' + }, + { + label: 'onView', + description: localize('walkthroughs.steps.completionEvents.onView', 'Check off step when a given view is opened'), + body: 'onView:${2:viewId}' + }, + { + label: 'onSettingChanged', + description: localize('walkthroughs.steps.completionEvents.onSettingChanged', 'Check off step when a given setting is changed'), + body: 'onSettingChanged:${2:settingName}' + }, + { + label: 'onContext', + description: localize('walkthroughs.steps.completionEvents.onContext', 'Check off step when a context key expression is true.'), + body: 'onContext:${2:key}' + }, + { + label: 'extensionInstalled', + description: localize('walkthroughs.steps.completionEvents.extensionInstalled', 'Check off step when an extension with the given id is installed. If the extension is already installed, the step will start off checked.'), + body: 'extensionInstalled:${3:extensionId}' + }, + { + label: 'stepSelected', + description: localize('walkthroughs.steps.completionEvents.stepSelected', 'Check off step as soon as it is selected.'), + body: 'stepSelected' + }, + ] + } + }, doneOn: { description: localize('walkthroughs.steps.doneOn', "Signal to mark step as complete."), + deprecationMessage: localize('walkthroughs.steps.doneOn.deprecation', "doneOn is deprecated. By default steps will be checked off when their buttons are clicked, to configure further use completionEvents"), type: 'object', required: ['command'], defaultSnippets: [{ 'body': { command: '$1' } }], diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts index a6639f10062c..fab174551d91 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts @@ -5,7 +5,7 @@ import 'vs/css!./gettingStarted'; import { localize } from 'vs/nls'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -14,13 +14,14 @@ export const gettingStartedInputTypeId = 'workbench.editors.gettingStartedInput' export class GettingStartedInput extends EditorInput { static readonly ID = gettingStartedInputTypeId; + static readonly RESOURCE = URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_getting_started_page' }); override get typeId(): string { return GettingStartedInput.ID; } get resource(): URI | undefined { - return URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_getting_started_page' }); + return GettingStartedInput.RESOURCE; } override matches(other: unknown) { diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts index 3ed42ffabc31..8153b4273bdb 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator, IInstantiationService, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Disposable } from 'vs/base/common/lifecycle'; import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IExtensionDescription, IStartEntry } from 'vs/platform/extensions/common/extensions'; @@ -23,15 +23,18 @@ import { BuiltinGettingStartedCategory, BuiltinGettingStartedStep, BuiltinGettin import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { assertIsDefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { GettingStartedInput } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { GettingStartedPage } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { LinkedText, parseLinkedText } from 'vs/base/common/linkedText'; +import { ILink, LinkedText, parseLinkedText } from 'vs/base/common/linkedText'; import { walkthroughsExtensionPoint } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { dirname } from 'vs/base/common/path'; +import { coalesce, flatten } from 'vs/base/common/arrays'; +import { IViewsService } from 'vs/workbench/common/views'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; + +export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote contexts may be different from the platform of the UI")); export const IGettingStartedService = createDecorator('gettingStartedService'); @@ -52,10 +55,12 @@ export interface IGettingStartedStep { category: GettingStartedCategory | string when: ContextKeyExpression order: number - doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never } + /** @deprecated */ + doneOn?: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never } + completionEvents: string[] media: | { type: 'image', path: { hc: URI, light: URI, dark: URI }, altText: string } - | { type: 'markdown', path: URI, base: URI, } + | { type: 'markdown', path: URI, base: URI, root: URI } } export interface IGettingStartedWalkthroughDescriptor { @@ -63,6 +68,7 @@ export interface IGettingStartedWalkthroughDescriptor { title: string description: string order: number + next?: string icon: | { type: 'icon', icon: ThemeIcon } | { type: 'image', path: string } @@ -89,6 +95,7 @@ export interface IGettingStartedCategory { title: string description: string order: number + next?: string icon: | { type: 'icon', icon: ThemeIcon } | { type: 'image', path: string } @@ -116,8 +123,9 @@ export interface IGettingStartedCategoryWithProgress extends Omit - readonly onDidRemoveCategory: Event + readonly onDidAddCategory: Event + readonly onDidRemoveCategory: Event + readonly onDidChangeStep: Event readonly onDidChangeCategory: Event @@ -125,19 +133,23 @@ export interface IGettingStartedService { getCategories(): IGettingStartedCategoryWithProgress[] + registerWalkthrough(categoryDescriptor: IGettingStartedWalkthroughDescriptor, steps: IGettingStartedStep[]): void; + progressByEvent(eventName: string): void; progressStep(id: string): void; deprogressStep(id: string): void; + + installedExtensionsRegistered: Promise; } export class GettingStartedService extends Disposable implements IGettingStartedService { declare readonly _serviceBrand: undefined; - private readonly _onDidAddCategory = new Emitter(); - onDidAddCategory: Event = this._onDidAddCategory.event; + private readonly _onDidAddCategory = new Emitter(); + onDidAddCategory: Event = this._onDidAddCategory.event; - private readonly _onDidRemoveCategory = new Emitter(); - onDidRemoveCategory: Event = this._onDidRemoveCategory.event; + private readonly _onDidRemoveCategory = new Emitter(); + onDidRemoveCategory: Event = this._onDidRemoveCategory.event; private readonly _onDidChangeCategory = new Emitter(); onDidChangeCategory: Event = this._onDidChangeCategory.event; @@ -151,8 +163,8 @@ export class GettingStartedService extends Disposable implements IGettingStarted private memento: Memento; private stepProgress: Record; - private commandListeners = new Map(); - private eventListeners = new Map(); + private sessionEvents = new Set(); + private completionListeners = new Map>(); private gettingStartedContributions = new Map(); private steps = new Map(); @@ -160,18 +172,24 @@ export class GettingStartedService extends Disposable implements IGettingStarted private tasExperimentService?: ITASExperimentService; private sessionInstalledExtensions = new Set(); + private categoryVisibilityContextKeys = new Set(); + private stepCompletionContextKeyExpressions = new Set(); + private stepCompletionContextKeys = new Set(); + + private triggerInstalledExtensionsRegistered!: () => void; + installedExtensionsRegistered: Promise; + constructor( @IStorageService private readonly storageService: IStorageService, @ICommandService private readonly commandService: ICommandService, @IContextKeyService private readonly contextService: IContextKeyService, @IUserDataAutoSyncEnablementService readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IProductService private readonly productService: IProductService, - @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IHostService private readonly hostService: IHostService, + @IViewsService private readonly viewsService: IViewsService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { super(); @@ -186,19 +204,67 @@ export class GettingStartedService extends Disposable implements IGettingStarted removed.forEach(e => this.unregisterExtensionContributions(e.description)); }); - this._register(this.commandService.onDidExecuteCommand(command => this.progressByCommand(command.commandId))); + this._register(this.commandService.onDidExecuteCommand(command => this.progressByEvent(`onCommand:${command.commandId}`))); + + this.extensionManagementService.getInstalled().then(installed => { + installed.forEach(ext => this.progressByEvent(`extensionInstalled:${ext.identifier.id.toLowerCase()}`)); + }); this._register(this.extensionManagementService.onDidInstallExtension(async e => { if (await this.hostService.hadLastFocus()) { - this.sessionInstalledExtensions.add(e.identifier.id); + this.sessionInstalledExtensions.add(e.identifier.id.toLowerCase()); + } + this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`); + })); + + this._register(this.contextService.onDidChangeContext(event => { + if (event.affectsSome(this.categoryVisibilityContextKeys)) { this._onDidAddCategory.fire(); } + if (event.affectsSome(this.stepCompletionContextKeys)) { + this.stepCompletionContextKeyExpressions.forEach(expression => { + if (event.affectsSome(new Set(expression.keys())) && this.contextService.contextMatchesRules(expression)) { + this.progressByEvent(`onContext:` + expression.serialize()); + } + }); } })); - if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); } + this._register(this.viewsService.onDidChangeViewVisibility(e => { + if (e.visible) { this.progressByEvent('onView:' + e.id); } + })); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + e.affectedKeys.forEach(key => { this.progressByEvent('onSettingChanged:' + key); }); + })); + + this.remoteAgentService.getEnvironment().then(env => { + const remoteOS = env?.os; + + const remotePlatform = + remoteOS === OS.Macintosh ? 'mac' + : remoteOS === OS.Windows ? 'windows' + : remoteOS === OS.Linux ? 'linux' + : undefined; + + if (remotePlatform) { + WorkspacePlatform.bindTo(this.contextService).set(remotePlatform); + } else if (isMacintosh) { + WorkspacePlatform.bindTo(this.contextService).set('mac'); + } else if (isLinux) { + WorkspacePlatform.bindTo(this.contextService).set('linux'); + } else if (isWindows) { + WorkspacePlatform.bindTo(this.contextService).set('windows'); + } else { + WorkspacePlatform.bindTo(this.contextService).set(undefined); + } + }); + + if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('onEvent:sync-enabled'); } this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => { - if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); } + if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('onEvent:sync-enabled'); } })); + this.installedExtensionsRegistered = new Promise(r => this.triggerInstalledExtensionsRegistered = r); + startEntries.forEach(async (entry, index) => { this.getCategoryOverrides(entry); this.registerStartEntry({ @@ -221,13 +287,23 @@ export class GettingStartedService extends Disposable implements IGettingStarted this.getStepOverrides(step, category.id); return ({ ...step, + completionEvents: step.completionEvents ?? [], description: parseDescription(step.description), category: category.id, order: index, when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), media: step.media.type === 'image' - ? { type: 'image', altText: step.media.altText, path: convertInternalMediaPathsToBrowserURIs(step.media.path) } - : { type: 'markdown', path: convertInternalMediaPathToFileURI(step.media.path), base: FileAccess.asFileUri('vs/workbench/contrib/welcome/gettingStarted/common/media/', require) }, + ? { + type: 'image', + altText: step.media.altText, + path: convertInternalMediaPathsToBrowserURIs(step.media.path) + } + : { + type: 'markdown', + path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcome/gettingStarted/common/media/' + step.media.path }) }), + base: FileAccess.asFileUri('vs/workbench/contrib/welcome/gettingStarted/common/media/', require), + root: FileAccess.asFileUri('vs/workbench/contrib/welcome/gettingStarted/common/media/', require), + }, }); })); }); @@ -270,7 +346,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted this._onDidChangeStep.fire(this.getStepProgress(existingStep)); } - private registerExtensionContributions(extension: IExtensionDescription) { + private async registerExtensionContributions(extension: IExtensionDescription) { const convertExtensionPathToFileURI = (path: string) => path.startsWith('https://') ? URI.parse(path, true) : FileAccess.asFileUri(joinPath(extension.extensionLocation, path)); @@ -292,55 +368,56 @@ export class GettingStartedService extends Disposable implements IGettingStarted } }; - let sectionToOpen: string | undefined; - if (!(extension.contributes?.walkthroughs?.length)) { return; } - if (this.productService.quality === 'stable') { - console.warn('Extension', extension.identifier.value, 'contributes welcome page content but this is a Stable build and extension contributions are only available in Insiders. The contributed content will be disregarded.'); - return; + if (this.configurationService.getValue('workbench.welcomePage.experimental.startEntryContributions') && this.productService.quality !== 'stable') { + extension.contributes.startEntries?.forEach(entry => { + const entryID = extension.identifier.value + '#startEntry#' + idForStartEntry(entry); + this.registerStartEntry({ + content: { + type: 'startEntry', + command: entry.command, + }, + description: entry.description, + title: entry.title, + id: entryID, + order: 0, + when: ContextKeyExpr.deserialize(entry.when) ?? ContextKeyExpr.true(), + icon: { + type: 'image', + path: extension.icon + ? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true) + : DefaultIconPath + } + }); + }); } - if (!this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions')) { - console.warn('Extension', extension.identifier.value, 'contributes welcome page content but the welcome page extension contribution feature flag has not been set. Set `workbench.welcomePage.experimental.extensionContributions` to begin using this experimental feature.'); - return; - } - extension.contributes.startEntries?.forEach(entry => { - const entryID = extension.identifier.value + '#startEntry#' + idForStartEntry(entry); - this.registerStartEntry({ - content: { - type: 'startEntry', - command: entry.command, - }, - description: entry.description, - title: entry.title, - id: entryID, - order: 0, - when: ContextKeyExpr.deserialize(entry.when) ?? ContextKeyExpr.true(), - icon: { - type: 'image', - path: extension.icon - ? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true) - : DefaultIconPath - } - }); - }); + let sectionToOpen: string | undefined; + let sectionToOpenIndex = Math.min(); // '+Infinity'; + await Promise.all(extension.contributes?.walkthroughs?.map(async (walkthrough, index) => { + const categoryID = extension.identifier.value + '#' + walkthrough.id; + const override = await Promise.race([ + this.tasExperimentService?.getTreatment(`gettingStarted.overrideCategory.${categoryID}.when`), + new Promise(resolve => setTimeout(() => resolve(walkthrough.when), 5000)) + ]); - extension.contributes?.walkthroughs?.forEach(walkthrough => { - const categoryID = extension.identifier.value + '#walkthrough#' + walkthrough.id; if ( - this.sessionInstalledExtensions.has(extension.identifier.value) - && walkthrough.primary - && this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(walkthrough.when) ?? ContextKeyExpr.true()) + this.sessionInstalledExtensions.has(extension.identifier.value.toLowerCase()) + && this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(override ?? walkthrough.when) ?? ContextKeyExpr.true()) ) { - this.sessionInstalledExtensions.delete(extension.identifier.value); - sectionToOpen = categoryID; + this.sessionInstalledExtensions.delete(extension.identifier.value.toLowerCase()); + if (index < sectionToOpenIndex) { + sectionToOpen = categoryID; + sectionToOpenIndex = index; + } } - this.registerWalkthrough({ + + const walkthoughDescriptior = { content: { type: 'steps' }, description: walkthrough.description, title: walkthrough.title, @@ -352,60 +429,73 @@ export class GettingStartedService extends Disposable implements IGettingStarted ? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true) : DefaultIconPath }, - when: ContextKeyExpr.deserialize(walkthrough.when) ?? ContextKeyExpr.true(), - }, - (walkthrough.steps ?? (walkthrough as any).tasks).map((step, index) => { - const description = parseDescription(step.description); - const buttonDescription = (step as any as { button: LegacyButtonConfig }).button; - if (buttonDescription) { - description.push({ nodes: [{ href: buttonDescription.link ?? `command:${buttonDescription.command}`, label: buttonDescription.title }] }); + when: ContextKeyExpr.deserialize(override ?? walkthrough.when) ?? ContextKeyExpr.true(), + } as const; + + const steps = (walkthrough.steps ?? (walkthrough as any).tasks).map((step, index) => { + const description = parseDescription(step.description || ''); + const buttonDescription = (step as any as { button: LegacyButtonConfig }).button; + if (buttonDescription) { + description.push({ nodes: [{ href: buttonDescription.link ?? `command:${buttonDescription.command}`, label: buttonDescription.title }] }); + } + const fullyQualifiedID = extension.identifier.value + '#' + walkthrough.id + '#' + step.id; + + let media: IGettingStartedStep['media']; + + if (step.media.image) { + const altText = (step.media as any).altText; + if (altText === undefined) { + console.error('Getting Started: item', fullyQualifiedID, 'is missing altText for its media element.'); } - const fullyQualifiedID = extension.identifier.value + '#' + walkthrough.id + '#' + step.id; + media = { type: 'image', altText, path: convertExtensionRelativePathsToBrowserURIs(step.media.image) }; + } + else if (step.media.markdown) { + media = { + type: 'markdown', + path: convertExtensionPathToFileURI(step.media.markdown), + base: convertExtensionPathToFileURI(dirname(step.media.markdown)), + root: FileAccess.asFileUri(extension.extensionLocation), + }; + } - let media: IGettingStartedStep['media']; - if (typeof step.media.path === 'string' && step.media.path.endsWith('.md')) { + // Legacy media config + else { + const legacyMedia = step.media as unknown as { path: string, altText: string }; + if (typeof legacyMedia.path === 'string' && legacyMedia.path.endsWith('.md')) { media = { type: 'markdown', - path: convertExtensionPathToFileURI(step.media.path), - base: convertExtensionPathToFileURI(dirname(step.media.path)) + path: convertExtensionPathToFileURI(legacyMedia.path), + base: convertExtensionPathToFileURI(dirname(legacyMedia.path)), + root: FileAccess.asFileUri(extension.extensionLocation), }; - } else { - const altText = (step.media as any).altText; - if (!altText) { + } + else { + const altText = legacyMedia.altText; + if (altText === undefined) { console.error('Getting Started: item', fullyQualifiedID, 'is missing altText for its media element.'); } - media = { type: 'image', altText, path: convertExtensionRelativePathsToBrowserURIs(step.media.path) }; + media = { type: 'image', altText, path: convertExtensionRelativePathsToBrowserURIs(legacyMedia.path) }; } + } - return ({ - description, media, - doneOn: step.doneOn?.command - ? { commandExecuted: step.doneOn.command } - : { eventFired: 'markDone:' + fullyQualifiedID }, - id: fullyQualifiedID, - title: step.title, - when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), - category: categoryID, - order: index, - }); - })); - }); + return ({ + description, media, + completionEvents: step.completionEvents?.filter(x => typeof x === 'string') ?? [], + id: fullyQualifiedID, + title: step.title, + when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(), + category: categoryID, + order: index, + }); + }); - if (sectionToOpen) { - for (const group of this.editorGroupsService.groups) { - if (group.activeEditor instanceof GettingStartedInput) { - (group.activeEditorPane as GettingStartedPage).makeCategoryVisibleWhenAvailable(sectionToOpen); - return; - } - } + this.registerWalkthrough(walkthoughDescriptior, steps); + })); - if (this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions') === 'openToSide') { - this.editorService.openEditor(this.instantiationService.createInstance(GettingStartedInput, { selectedCategory: sectionToOpen }), {}, SIDE_GROUP); - } else if (this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions') === 'open') { - this.editorService.openEditor(this.instantiationService.createInstance(GettingStartedInput, { selectedCategory: sectionToOpen }), {}); - } else if (this.configurationService.getValue('workbench.welcomePage.experimental.extensionContributions') === 'openInBackground') { - this.editorService.openEditor(this.instantiationService.createInstance(GettingStartedInput, { selectedCategory: sectionToOpen }), { inactive: true }); - } + this.triggerInstalledExtensionsRegistered(); + + if (sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall')) { + this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen); } } @@ -417,7 +507,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted extension.contributes?.startEntries?.forEach(section => { const categoryID = extension.identifier.value + '#startEntry#' + idForStartEntry(section); this.gettingStartedContributions.delete(categoryID); - this._onDidRemoveCategory.fire(categoryID); + this._onDidRemoveCategory.fire(); }); extension.contributes?.walkthroughs?.forEach(section => { @@ -427,25 +517,86 @@ export class GettingStartedService extends Disposable implements IGettingStarted this.steps.delete(fullyQualifiedID); }); this.gettingStartedContributions.delete(categoryID); - this._onDidRemoveCategory.fire(categoryID); + this._onDidRemoveCategory.fire(); }); } private registerDoneListeners(step: IGettingStartedStep) { - if (step.doneOn.commandExecuted) { - const existing = this.commandListeners.get(step.doneOn.commandExecuted); - if (existing) { existing.push(step.id); } - else { - this.commandListeners.set(step.doneOn.commandExecuted, [step.id]); - } + if (step.doneOn) { + if (step.doneOn.commandExecuted) { step.completionEvents.push(`onCommand:${step.doneOn.commandExecuted}`); } + if (step.doneOn.eventFired) { step.completionEvents.push(`onEvent:${step.doneOn.eventFired}`); } } - if (step.doneOn.eventFired) { - const existing = this.eventListeners.get(step.doneOn.eventFired); - if (existing) { existing.push(step.id); } - else { - this.eventListeners.set(step.doneOn.eventFired, [step.id]); + + if (!step.completionEvents.length) { + step.completionEvents = coalesce(flatten( + step.description + .filter(linkedText => linkedText.nodes.length === 1) // only buttons + .map(linkedText => + linkedText.nodes + .filter(((node): node is ILink => typeof node !== 'string')) + .map(({ href }) => { + if (href.startsWith('command:')) { + return 'onCommand:' + href.slice('command:'.length, href.includes('?') ? href.indexOf('?') : undefined); + } + if (href.startsWith('https://') || href.startsWith('http://')) { + return 'onLink:' + href; + } + return undefined; + })))); + } + + if (!step.completionEvents.length) { + step.completionEvents.push('stepSelected'); + } + + for (let event of step.completionEvents) { + const [_, eventType, argument] = /^([^:]*):?(.*)$/.exec(event) ?? []; + + if (!eventType) { + console.error(`Unknown completionEvent ${event} when registering step ${step.id}`); + continue; } + + switch (eventType) { + case 'onLink': case 'onEvent': case 'onView': case 'onSettingChanged': + break; + case 'onContext': { + const expression = ContextKeyExpr.deserialize(argument); + if (expression) { + this.stepCompletionContextKeyExpressions.add(expression); + expression.keys().forEach(key => this.stepCompletionContextKeys.add(key)); + event = eventType + ':' + expression.serialize(); + } else { + console.error('Unable to parse context key expression:', expression, 'in getting started step', step.id); + } + break; + } + case 'stepSelected': + event = eventType + ':' + step.id; + break; + case 'onCommand': + event = eventType + ':' + argument.replace(/^toSide:/, ''); + break; + case 'extensionInstalled': + event = eventType + ':' + argument.toLowerCase(); + break; + default: + console.error(`Unknown completionEvent ${event} when registering step ${step.id}`); + continue; + } + + this.registerCompletionListener(event, step); + if (this.sessionEvents.has(event)) { + this.progressStep(step.id); + } + } + } + + private registerCompletionListener(event: string, step: IGettingStartedStep) { + if (!this.completionListeners.has(event)) { + this.completionListeners.set(event, new Set()); } + this.completionListeners.get(event)?.add(step.id); } getCategories(): IGettingStartedCategoryWithProgress[] { @@ -515,14 +666,11 @@ export class GettingStartedService extends Disposable implements IGettingStarted this._onDidProgressStep.fire(this.getStepProgress(step)); } - private progressByCommand(command: string) { - const listening = this.commandListeners.get(command) ?? []; - listening.forEach(id => this.progressStep(id)); - } - progressByEvent(event: string): void { - const listening = this.eventListeners.get(event) ?? []; - listening.forEach(id => this.progressStep(id)); + if (this.sessionEvents.has(event)) { return; } + + this.sessionEvents.add(event); + this.completionListeners.get(event)?.forEach(id => this.progressStep(id)); } private registerStartEntry(categoryDescriptor: IGettingStartedStartEntryDescriptor): void { @@ -535,10 +683,10 @@ export class GettingStartedService extends Disposable implements IGettingStarted const category: IGettingStartedCategory = { ...categoryDescriptor }; this.gettingStartedContributions.set(categoryDescriptor.id, category); - this._onDidAddCategory.fire(this.getCategoryProgress(category)); + this._onDidAddCategory.fire(); } - private registerWalkthrough(categoryDescriptor: IGettingStartedWalkthroughDescriptor, steps: IGettingStartedStep[]): void { + registerWalkthrough(categoryDescriptor: IGettingStartedWalkthroughDescriptor, steps: IGettingStartedStep[]): void { const oldCategory = this.gettingStartedContributions.get(categoryDescriptor.id); if (oldCategory) { console.error(`Skipping attempt to overwrite getting started category. (${categoryDescriptor.id})`); @@ -551,8 +699,28 @@ export class GettingStartedService extends Disposable implements IGettingStarted if (this.steps.has(step.id)) { throw Error('Attempting to register step with id ' + step.id + ' twice. Second is dropped.'); } this.steps.set(step.id, step); this.registerDoneListeners(step); + step.when.keys().forEach(key => this.categoryVisibilityContextKeys.add(key)); + }); + + if (this.contextService.contextMatchesRules(category.when)) { + this._onDidAddCategory.fire(); + } + + this.tasExperimentService?.getTreatment(`gettingStarted.overrideCategory.${categoryDescriptor.id.replace('#', '.')}.when`).then(override => { + if (override) { + const old = category.when; + const gnu = ContextKeyExpr.deserialize(override) ?? old; + this.categoryVisibilityContextKeys.add(override); + category.when = gnu; + + if (this.contextService.contextMatchesRules(old) && !this.contextService.contextMatchesRules(gnu)) { + this._onDidRemoveCategory.fire(); + } else if (!this.contextService.contextMatchesRules(old) && this.contextService.contextMatchesRules(gnu)) { + this._onDidAddCategory.fire(); + } + } }); - this._onDidAddCategory.fire(this.getCategoryProgress(category)); + category.when.keys().forEach(key => this.categoryVisibilityContextKeys.add(key)); } private getStep(id: string): IGettingStartedStep { diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts index eae3e4395227..aa5fdc543fc0 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/gettingStartedContent.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media'; +import 'vs/workbench/contrib/welcome/gettingStarted/common/media/notebookProfile'; import { localize } from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -12,14 +14,13 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; const setupIcon = registerIcon('getting-started-setup', Codicon.zap, localize('getting-started-setup-icon', "Icon used for the setup category of getting started")); const beginnerIcon = registerIcon('getting-started-beginner', Codicon.lightbulb, localize('getting-started-beginner-icon', "Icon used for the beginner category of getting started")); const intermediateIcon = registerIcon('getting-started-intermediate', Codicon.mortarBoard, localize('getting-started-intermediate-icon', "Icon used for the intermediate category of getting started")); -const codespacesIcon = registerIcon('getting-started-codespaces', Codicon.github, localize('getting-started-codespaces-icon', "Icon used for the codespaces category of getting started")); export type BuiltinGettingStartedStep = { id: string title: string, description: string, - doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never, } + completionEvents?: string[] when?: string, media: | { type: 'image', path: string | { hc: string, light: string, dark: string }, altText: string } @@ -30,6 +31,7 @@ export type BuiltinGettingStartedCategory = { id: string title: string, description: string, + next?: string, icon: ThemeIcon, when?: string, content: @@ -116,83 +118,29 @@ export const startEntries: GettingStartedStartEntryContent = [ }, ]; -export const walkthroughs: GettingStartedWalkthroughContent = [ - { - id: 'Codespaces', - title: localize('gettingStarted.codespaces.title', "Primer on Codespaces"), - icon: codespacesIcon, - when: 'remoteName == codespaces', - description: localize('gettingStarted.codespaces.description', "Get up and running with your instant code environment."), - content: { - type: 'steps', - steps: [ - { - id: 'runProjectStep', - title: localize('gettingStarted.runProject.title', "Build & run your app"), - description: localize('gettingStarted.runProject.description', "Build, run & debug your code in the cloud, right from the browser.\n[Start Debugging](command:workbench.action.debug.selectandstart)"), - doneOn: { commandExecuted: 'workbench.action.debug.selectandstart' }, - media: { type: 'image', altText: 'Node.js project running debug mode and paused.', path: 'runProject.png' }, - }, - { - id: 'forwardPortsStep', - title: localize('gettingStarted.forwardPorts.title', "Access your running application"), - description: localize('gettingStarted.forwardPorts.description', "Ports running within your codespace are automatically forwarded to the web, so you can open them in your browser.\n[Show Ports Panel](command:~remote.forwardedPorts.focus)"), - doneOn: { commandExecuted: '~remote.forwardedPorts.focus' }, - media: { type: 'image', altText: 'Ports panel.', path: 'forwardPorts.png' }, - }, - { - id: 'pullRequests', - title: localize('gettingStarted.pullRequests.title', "Pull requests at your fingertips"), - description: localize('gettingStarted.pullRequests.description', "Bring your GitHub workflow closer to your code, so you can review pull requests, add comments, merge branches, and more.\n[Open GitHub View](command:workbench.view.extension.github-pull-requests)"), - doneOn: { commandExecuted: 'workbench.view.extension.github-pull-requests' }, - media: { type: 'image', altText: 'Preview for reviewing a pull request.', path: 'pullRequests.png' }, - }, - { - id: 'remoteTerminal', - title: localize('gettingStarted.remoteTerminal.title', "Run tasks in the integrated terminal"), - description: localize('gettingStarted.remoteTerminal.description', "Perform quick command-line tasks using the built-in terminal.\n[Focus Terminal](command:terminal.focus)"), - doneOn: { commandExecuted: 'terminal.focus' }, - media: { type: 'image', altText: 'Remote terminal showing npm commands.', path: 'remoteTerminal.png' }, - }, - { - id: 'openVSC', - title: localize('gettingStarted.openVSC.title', "Develop remotely in VS Code"), - description: localize('gettingStarted.openVSC.description', "Access the power of your cloud development environment from your local VS Code. Set it up by installing the GitHub Codespaces extension and connecting your GitHub account.\n[Open in VS Code](command:github.codespaces.openInStable)"), - when: 'isWeb', - doneOn: { commandExecuted: 'github.codespaces.openInStable' }, - media: { - type: 'image', altText: 'Preview of the Open in VS Code command.', path: { - dark: 'dark/openVSC.png', - light: 'light/openVSC.png', - hc: 'light/openVSC.png', - } - }, - } - ] - } - }, +const Button = (title: string, href: string) => `[${title}](${href})`; +export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'Setup', - title: localize('gettingStarted.setup.title', "Customize your Setup"), - description: localize('gettingStarted.setup.description', "Extend and customize VS Code to make it yours."), + title: localize('gettingStarted.setup.title', "Get Started with VS Code"), + description: localize('gettingStarted.setup.description', "Discover the best customizations to make VS Code yours."), icon: setupIcon, - when: 'remoteName != codespaces', + next: 'Beginner', content: { type: 'steps', steps: [ { id: 'pickColorTheme', - title: localize('gettingStarted.pickColor.title', "Customize the look with themes"), - description: localize('gettingStarted.pickColor.description', "Pick a color theme to match your taste and mood while coding.\n[Pick a Theme](command:workbench.action.selectTheme)"), - doneOn: { commandExecuted: 'workbench.action.selectTheme' }, - media: { type: 'image', altText: 'Color theme preview for dark and light theme.', path: 'colorTheme.png', } + title: localize('gettingStarted.pickColor.title', "Choose the look you want"), + description: localize('gettingStarted.pickColor.description.interpolated', "The right color palette helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", Button(localize('titleID', "Browse Color Themes"), 'command:workbench.action.selectTheme')), + completionEvents: ['onSettingChanged:workbench.colorTheme'], + media: { type: 'markdown', path: 'example_markdown_media', } }, { id: 'findLanguageExtensions', - title: localize('gettingStarted.findLanguageExts.title', "Code in any language"), - description: localize('gettingStarted.findLanguageExts.description', "VS Code supports over 50+ programming languages. While many are built-in, others can be easily installed as extensions in one click.\n[Browse Language Extensions](command:workbench.extensions.action.showLanguageExtensions)"), - doneOn: { commandExecuted: 'workbench.extensions.action.showLanguageExtensions' }, + title: localize('gettingStarted.findLanguageExts.title', "Rich support for all your languages"), + description: localize('gettingStarted.findLanguageExts.description.interpolated', "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')), media: { type: 'image', altText: 'Language extensions', path: { dark: 'dark/languageExtensions.png', @@ -202,38 +150,35 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } }, { - id: 'keymaps', - title: localize('gettingStarted.keymaps.title', "Switch from other editors"), - description: localize('gettingStarted.keymaps.description', "Bring your favoriteย keyboard shortcuts from other editors into VS Code with keymaps.\n[Browse Keymap Extensions](command:workbench.extensions.action.showRecommendedKeymapExtensions)"), - doneOn: { commandExecuted: 'workbench.extensions.action.showRecommendedKeymapExtensions' }, + id: 'commandPaletteTask', + title: localize('gettingStarted.commandPalette.title', "One shortcut to access everything"), + description: localize('gettingStarted.commandPalette.description.interpolated', "Commands Palette is the keyboard way to accomplish any task in VS Code. **Practice** by looking up your frequently used commands to save time and keep in the flow.\n{0}\n__Try searching for 'view toggle'.__", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), media: { - type: 'image', altText: 'List of keymap extensions.', path: { - dark: 'dark/keymaps.png', - light: 'light/keymaps.png', - hc: 'hc/keymaps.png', - }, - } + type: 'image', altText: 'Command Palette overlay for searching and executing commands.', path: { + dark: 'dark/commandPalette.png', + light: 'light/commandPalette.png', + hc: 'hc/commandPalette.png', + } + }, }, { - id: 'settingsSync', - title: localize('gettingStarted.settingsSync.title', "Sync your favorite setup"), - description: localize('gettingStarted.settingsSync.description', "Never lose the perfect VS Code setup! Settings Sync will back up and share settings, keybindings & extensions across several VS Code instances.\n[Enable Settings Sync](command:workbench.userDataSync.actions.turnOn)"), - when: 'syncStatus != uninitialized', - doneOn: { eventFired: 'sync-enabled' }, + id: 'workspaceTrust', + title: localize('gettingStarted.workspaceTrust.title', "Safely browse and edit code"), + description: localize('gettingStarted.workspaceTrust.description.interpolated', "{0} lets you decide whether your project folders should **allow or restrict** automatic code execution __(required for extensions, debugging, etc)__.\nOpening a file/folder will prompt to grant trust. You can always {1} later.", Button(localize('workspaceTrust', "Workspace Trust"), 'https://github.com/microsoft/vscode-docs/blob/workspaceTrust/docs/editor/workspace-trust.md'), Button(localize('enableTrust', "enable trust"), 'command:toSide:workbench.action.manageTrustedDomain')), + when: '!isWorkspaceTrusted && workspaceFolderCount == 0', media: { - type: 'image', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: { - dark: 'dark/settingsSync.png', - light: 'light/settingsSync.png', - hc: 'hc/settingsSync.png', + type: 'image', altText: 'Workspace Trust editor in Restricted mode and a primary button for switching to Trusted mode.', path: { + dark: 'dark/workspaceTrust.svg', + light: 'light/workspaceTrust.svg', + hc: 'dark/workspaceTrust.svg', }, - } + }, }, { id: 'pickAFolderTask-Mac', - title: localize('gettingStarted.setup.OpenFolder.title', "Open your project folder"), - description: localize('gettingStarted.setup.OpenFolder.description', "Open a project folder to start coding!\n[Pick a Folder](command:workbench.action.files.openFileFolder)"), + title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), + description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), when: 'isMac && workspaceFolderCount == 0', - doneOn: { commandExecuted: 'workbench.action.files.openFileFolder' }, media: { type: 'image', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: { dark: 'dark/openFolder.png', @@ -244,10 +189,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'pickAFolderTask-Other', - title: localize('gettingStarted.setup.OpenFolder.title', "Open your project folder"), - description: localize('gettingStarted.setup.OpenFolder.description2', "Open a project folder to start coding!\n[Pick a Folder](command:workbench.action.files.openFolder)"), + title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), + description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), when: '!isMac && workspaceFolderCount == 0', - doneOn: { commandExecuted: 'workbench.action.files.openFolder' }, media: { type: 'image', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: { dark: 'dark/openFolder.png', @@ -258,10 +202,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'quickOpen', - title: localize('gettingStarted.quickOpen.title', "Quick open files"), - description: localize('gettingStarted.quickOpen.description', "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n[Quick Open a File](command:toSide:workbench.action.quickOpen)"), + title: localize('gettingStarted.quickOpen.title', "Quickly navigate between your files"), + description: localize('gettingStarted.quickOpen.description.interpolated', "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n{0}", Button(localize('quickOpen', "Quick Open a File"), 'command:toSide:workbench.action.quickOpen')), when: 'workspaceFolderCount != 0', - doneOn: { commandExecuted: 'workbench.action.quickOpen' }, media: { type: 'image', altText: 'Go to file in quick search.', path: { dark: 'dark/openFolder.png', @@ -278,29 +221,28 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'Beginner', title: localize('gettingStarted.beginner.title', "Learn the Fundamentals"), icon: beginnerIcon, + next: 'Intermediate', description: localize('gettingStarted.beginner.description', "Jump right into VS Code and get an overview of the must-have features."), content: { type: 'steps', steps: [ { - id: 'commandPaletteTask', - title: localize('gettingStarted.commandPalette.title', "Find & run commands"), - description: localize('gettingStarted.commandPalette.description', "The easiest way to find everything VS Code can do. If you're ever looking for a feature or a shortcut, check here first!\n[Open Command Palette](command:workbench.action.showCommands)"), - doneOn: { commandExecuted: 'workbench.action.showCommands' }, + id: 'playground', + title: localize('gettingStarted.playground.title', "Redefine your editing skills"), + description: localize('gettingStarted.playground.description.interpolated', "Want to code faster and smarter? Practice powerful code editing features in the interactive playground.\n{0}", Button(localize('openInteractivePlayground', "Open Interactive Playground"), 'command:toSide:workbench.action.showInteractivePlayground')), media: { - type: 'image', altText: 'Command Palette overlay for searching and executing commands.', path: { - dark: 'dark/commandPalette.png', - light: 'light/commandPalette.png', - hc: 'hc/commandPalette.png', - } + type: 'image', altText: 'Interactive Playground.', path: { + dark: 'dark/playground.png', + light: 'light/playground.png', + hc: 'light/playground.png' + }, }, }, { id: 'terminal', title: localize('gettingStarted.terminal.title', "Convenient built-in terminal"), - description: localize('gettingStarted.terminal.description', "Quickly run shell commands and monitor build output, right next to your code.\n[Show Terminal Panel](command:workbench.action.terminal.toggleTerminal)"), + description: localize('gettingStarted.terminal.description.interpolated', "Quickly run shell commands and monitor build output, right next to your code.\n{0}", Button(localize('showTerminal', "Show Terminal Panel"), 'command:workbench.action.terminal.toggleTerminal')), when: 'remoteName != codespaces && !terminalIsOpen', - doneOn: { commandExecuted: 'workbench.action.terminal.toggleTerminal' }, media: { type: 'image', altText: 'Integrated terminal running a few npm commands', path: { dark: 'dark/terminal.png', @@ -312,8 +254,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'extensions', title: localize('gettingStarted.extensions.title', "Limitless extensibility"), - description: localize('gettingStarted.extensions.description', "Extensions are VS Code's power-ups. They range from handy productivity hacks, expanding out-of-the-box features, to adding completely new capabilities.\n[Browse Recommended Extensions](command:workbench.extensions.action.showRecommendedExtensions)"), - doneOn: { commandExecuted: 'workbench.extensions.action.showRecommendedExtensions' }, + description: localize('gettingStarted.extensions.description.interpolated', "Extensions are VS Code's power-ups. They range from handy productivity hacks, expanding out-of-the-box features, to adding completely new capabilities.\n{0}", Button(localize('browseRecommended', "Browse Recommended Extensions"), 'command:workbench.extensions.action.showRecommendedExtensions')), media: { type: 'image', altText: 'VS Code extension marketplace with featured language extensions', path: { dark: 'dark/extensions.png', @@ -325,8 +266,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'settings', title: localize('gettingStarted.settings.title', "Tune your settings"), - description: localize('gettingStarted.settings.description', "Tweak every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n[Tweak my Settings](command:toSide:workbench.action.openSettings)"), - doneOn: { commandExecuted: 'workbench.action.openSettings' }, + description: localize('gettingStarted.settings.description.interpolated', "Tweak every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Tweak my Settings"), 'command:toSide:workbench.action.openSettings')), media: { type: 'image', altText: 'VS Code Settings', path: { dark: 'dark/settings.png', @@ -335,11 +275,24 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } }, }, + { + id: 'settingsSync', + title: localize('gettingStarted.settingsSync.title', "Sync your stuff across devices"), + description: localize('gettingStarted.settingsSync.description.interpolated', "Never lose the perfect VS Code setup! Settings Sync will back up and share settings, keybindings & extensions across several installations.\n{0}", Button(localize('enableSync', "Enable Settings Sync"), 'command:workbench.userDataSync.actions.turnOn')), + when: 'syncStatus != uninitialized', + completionEvents: ['onEvent:sync-enabled'], + media: { + type: 'image', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: { + dark: 'dark/settingsSync.png', + light: 'light/settingsSync.png', + hc: 'hc/settingsSync.png', + }, + } + }, { id: 'videoTutorial', title: localize('gettingStarted.videoTutorial.title', "Lean back and learn"), - description: localize('gettingStarted.videoTutorial.description', "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n[Watch Tutorial](https://aka.ms/vscode-getting-started-video)"), - doneOn: { eventFired: 'linkOpened:https://aka.ms/vscode-getting-started-video' }, + description: localize('gettingStarted.videoTutorial.description.interpolated', "Watch the first in a series of short & practical video tutorials for VS Code's key features.\n{0}", Button(localize('watch', "Watch Tutorial"), 'https://aka.ms/vscode-getting-started-video')), media: { type: 'image', altText: 'VS Code Settings', path: 'tutorialVideo.png' }, } ] @@ -354,24 +307,10 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ content: { type: 'steps', steps: [ - { - id: 'playground', - title: localize('gettingStarted.playground.title', "Redefine your editing skills"), - description: localize('gettingStarted.playground.description', "Want to code faster and smarter? Practice powerful code editing features in the interactive playground.\n[Open Interactive Playground](command:toSide:workbench.action.showInteractivePlayground)"), - doneOn: { commandExecuted: 'workbench.action.showInteractivePlayground' }, - media: { - type: 'image', altText: 'Interactive Playground.', path: { - dark: 'dark/playground.png', - light: 'light/playground.png', - hc: 'light/playground.png' - }, - }, - }, { id: 'splitview', title: localize('gettingStarted.splitview.title', "Side by side editing"), - description: localize('gettingStarted.splitview.description', "Make the most of your screen estate by opening files side by side, vertically and horizontally.\n[Split Editor](command:workbench.action.splitEditor)"), - doneOn: { commandExecuted: 'workbench.action.splitEditor' }, + description: localize('gettingStarted.splitview.description.interpolated', "Make the most of your screen estate by opening files side by side, vertically and horizontally.\n{0}", Button(localize('splitEditor', "Split Editor"), 'command:workbench.action.splitEditor')), media: { type: 'image', altText: 'Multiple editors in split view.', path: { dark: 'dark/splitview.png', @@ -383,9 +322,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'debugging', title: localize('gettingStarted.debug.title', "Watch your code in action"), - description: localize('gettingStarted.debug.description', "Accelerate your edit, build, test, and debug loop by setting up a launch configuration.\n[Run your Project](command:workbench.action.debug.selectandstart)"), + description: localize('gettingStarted.debug.description.interpolated', "Accelerate your edit, build, test, and debug loop by setting up a launch configuration.\n{0}", Button(localize('runProject', "Run your Project"), 'command:workbench.action.debug.selectandstart')), when: 'workspaceFolderCount != 0', - doneOn: { commandExecuted: 'workbench.action.debug.selectandstart' }, media: { type: 'image', altText: 'Run and debug view.', path: { dark: 'dark/debug.png', @@ -397,9 +335,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'scmClone', title: localize('gettingStarted.scm.title', "Track your code with Git"), - description: localize('gettingStarted.scmClone.description', "Set up the built-in version control for your project to track your changes and collaborate with others.\n[Clone Repository](command:git.clone)"), + description: localize('gettingStarted.scmClone.description.interpolated', "Set up the built-in version control for your project to track your changes and collaborate with others.\n{0}", Button(localize('cloneRepo', "Clone Repository"), 'command:git.clone')), when: 'config.git.enabled && !git.missing && workspaceFolderCount == 0', - doneOn: { commandExecuted: 'git.clone' }, media: { type: 'image', altText: 'Source Control view.', path: { dark: 'dark/scm.png', @@ -411,9 +348,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'scmSetup', title: localize('gettingStarted.scm.title', "Track your code with Git"), - description: localize('gettingStarted.scmSetup.description', "Set up the built-in version control for your project to track your changes and collaborate with others.\n[Initialize Git Repository](command:git.init)"), + description: localize('gettingStarted.scmSetup.description.interpolated', "Set up the built-in version control for your project to track your changes and collaborate with others.\n{0}", Button(localize('initRepo', "Initialize Git Repository"), 'command:git.init')), when: 'config.git.enabled && !git.missing && workspaceFolderCount != 0 && gitOpenRepositoryCount == 0', - doneOn: { commandExecuted: 'git.init' }, media: { type: 'image', altText: 'Source Control view.', path: { dark: 'dark/scm.png', @@ -425,9 +361,8 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'scm', title: localize('gettingStarted.scm.title', "Track your code with Git"), - description: localize('gettingStarted.scm.description', "No more looking up Git commands! Git and GitHub workflows are seamlessly integrated.[Open Source Control](command:workbench.view.scm)"), + description: localize('gettingStarted.scm.description.interpolated', "No more looking up Git commands! Git and GitHub workflows are seamlessly integrated.\n{0}", Button(localize('openSCM', "Open Source Control"), 'command:workbench.view.scm')), when: 'config.git.enabled && !git.missing && workspaceFolderCount != 0 && gitOpenRepositoryCount != 0 && activeViewlet != \'workbench.view.scm\'', - doneOn: { commandExecuted: 'workbench.view.scm.focus' }, media: { type: 'image', altText: 'Source Control view.', path: { dark: 'dark/scm.png', @@ -440,8 +375,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ id: 'tasks', title: localize('gettingStarted.tasks.title', "Automate your project tasks"), when: 'workspaceFolderCount != 0', - description: localize('gettingStarted.tasks.description', "Create tasks for your common workflows and enjoy the integrated experience of running scripts and automatically checking results.\n[Run Auto-detected Tasks](command:workbench.action.tasks.runTask)"), - doneOn: { commandExecuted: 'workbench.action.tasks.runTask' }, + description: localize('gettingStarted.tasks.description.interpolated', "Create tasks for your common workflows and enjoy the integrated experience of running scripts and automatically checking results.\n{0}", Button(localize('runTasks', "Run Auto-detected Tasks"), 'command:workbench.action.tasks.runTask')), media: { type: 'image', altText: 'Task runner.', path: { dark: 'dark/tasks.png', @@ -453,8 +387,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'shortcuts', title: localize('gettingStarted.shortcuts.title', "Customize your shortcuts"), - description: localize('gettingStarted.shortcuts.description', "Once you have discovered your favorite commands, create custom keyboard shortcuts for instant access.\n[Keyboard Shortcuts](command:toSide:workbench.action.openGlobalKeybindings)"), - doneOn: { commandExecuted: 'workbench.action.openGlobalKeybindings' }, + description: localize('gettingStarted.shortcuts.description.interpolated', "Once you have discovered your favorite commands, create custom keyboard shortcuts for instant access.\n{0}", Button(localize('keyboardShortcuts', "Keyboard Shortcuts"), 'command:toSide:workbench.action.openGlobalKeybindings')), media: { type: 'image', altText: 'Interactive shortcuts.', path: { dark: 'dark/shortcuts.png', @@ -465,5 +398,27 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } ] } + }, + { + id: 'notebooks', + title: localize('gettingStarted.notebook.title', "Customize Notebooks"), + description: '', + icon: setupIcon, + when: 'config.notebook.experimental.gettingStarted && userHasOpenedNotebook', + content: { + type: 'steps', + steps: [ + { + completionEvents: ['onCommand:notebook.setProfile'], + id: 'notebookProfile', + title: localize('gettingStarted.notebookProfile.title', "Select the layout for your notebooks"), + description: localize('gettingStarted.notebookProfile.description', "Get notebooks to feel just the way you prefer"), + when: 'userHasOpenedNotebook', + media: { + type: 'markdown', path: 'notebookProfile' + } + }, + ] + } } ]; diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark.png new file mode 100644 index 000000000000..bac2bc8d2d80 Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/keymaps.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/keymaps.png deleted file mode 100644 index b4027937a3ab..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/keymaps.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/openVSC.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/openVSC.png deleted file mode 100644 index 9a700e0d4535..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/openVSC.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg new file mode 100644 index 000000000000..f1cf7fe5053f --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/dark/workspaceTrust.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts new file mode 100644 index 000000000000..148374b5544c --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/example_markdown_media.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { escape } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; + +export default () => ` + + + + + ${escape(localize('light', "Light"))} + + + + ${escape(localize('dark', "Dark"))} + + + + ${escape(localize('HighContrast', "High Contrast"))} + + + + ${escape(localize('seeMore', "See More Themes..."))} + + +`; diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/forwardPorts.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/forwardPorts.png deleted file mode 100644 index ae10f31d6f6d..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/forwardPorts.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/hc/keymaps.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/hc/keymaps.png deleted file mode 100644 index 6105fd0f7147..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/hc/keymaps.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light.png new file mode 100644 index 000000000000..81aa74dc4de3 Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/keymaps.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/keymaps.png deleted file mode 100644 index 784f97e43145..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/keymaps.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/openVSC.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/openVSC.png deleted file mode 100644 index 95610a5ea4eb..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/openVSC.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/workspaceTrust.svg b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/workspaceTrust.svg new file mode 100644 index 000000000000..486ed7dd305e --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/light/workspaceTrust.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/monokai.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/monokai.png new file mode 100644 index 000000000000..ea0926178e0f Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/monokai.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/more.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/more.png new file mode 100644 index 000000000000..e1b741da46a1 Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/more.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookProfile.ts b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookProfile.ts new file mode 100644 index 000000000000..25da8e871656 --- /dev/null +++ b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookProfile.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { escape } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; + +const profileArg = (profile: string) => encodeURIComponent(JSON.stringify({ profile })); +const imageSize = 400; + +export default () => ` + + + + + ${escape(localize('default', "Default"))} + + + + ${escape(localize('jupyter', "Jupyter"))} + + + + ${escape(localize('colab', "Colab"))} + + + +`; diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/colab.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/colab.png new file mode 100644 index 000000000000..3e54f9875fc0 Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/colab.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/default.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/default.png new file mode 100644 index 000000000000..301bb1129f1b Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/default.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/jupyter.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/jupyter.png new file mode 100644 index 000000000000..a81eecb1b804 Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/notebookThemes/jupyter.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/pullRequests.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/pullRequests.png deleted file mode 100644 index 77911cafd758..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/pullRequests.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/quiet-light.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/quiet-light.png new file mode 100644 index 000000000000..f620e22d3bea Binary files /dev/null and b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/quiet-light.png differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/remoteTerminal.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/remoteTerminal.png deleted file mode 100644 index 366f16404a49..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/remoteTerminal.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/runProject.png b/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/runProject.png deleted file mode 100644 index f90629c6945c..000000000000 Binary files a/lib/vscode/src/vs/workbench/contrib/welcome/gettingStarted/common/media/runProject.png and /dev/null differ diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts index af4cb6871738..c32048e7df18 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { escape } from 'vs/base/common/strings'; -import product from 'vs/platform/product/common/product'; import { localize } from 'vs/nls'; +import product from 'vs/platform/product/common/product'; export default () => `
    diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts index 5234bdf243ef..ca012b9fd548 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts @@ -6,15 +6,36 @@ import { localize } from 'vs/nls'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { WelcomePageContribution, WelcomePageAction, WelcomeInputSerializer, DEFAULT_STARTUP_EDITOR_CONFIG } from 'vs/workbench/contrib/welcome/page/browser/welcomePage'; +import { WelcomePageContribution, WelcomePageAction, WelcomeInputSerializer } from 'vs/workbench/contrib/welcome/page/browser/welcomePage'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; Registry.as(ConfigurationExtensions.Configuration) - .registerConfiguration(DEFAULT_STARTUP_EDITOR_CONFIG); + .registerConfiguration({ + ...workbenchConfigurationNodeBase, + 'properties': { + 'workbench.startupEditor': { + 'scope': ConfigurationScope.RESOURCE, + 'type': 'string', + 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'gettingStarted', 'gettingStartedInEmptyWorkbench'], + 'enumDescriptions': [ + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the legacy Welcome page."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the legacy Welcome page when opening an empty workbench."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the new Welcome Page with content to aid in getting started with VS Code and extensions."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStartedInEmptyWorkbench' }, "When opening an empty workbench, open the new Welcome Page with content to aid in getting started with VS Code and extensions.") + ], + 'default': 'gettingStarted', + 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") + }, + } + }); Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(WelcomePageContribution, LifecyclePhase.Restored); diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css index 2608762fccc5..76546c30d4e4 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css +++ b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css @@ -94,7 +94,7 @@ } .monaco-workbench .part.editor > .content .welcomePage .splash .section { - margin-bottom: 3em; + margin-bottom: 5em; } .monaco-workbench .part.editor > .content .welcomePage .splash ul { diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index cb20a4cd8fc6..a8f5d76bda39 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -10,7 +10,7 @@ import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/c import * as arrays from 'vs/base/common/arrays'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; @@ -32,7 +32,7 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { IEditorInputSerializer, EditorInput } from 'vs/workbench/common/editor'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TimeoutTimer } from 'vs/base/common/async'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -48,54 +48,8 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { GettingStartedInput, gettingStartedInputTypeId } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput'; import { welcomeButtonBackground, welcomeButtonHoverBackground, welcomePageBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; -import { ILogService } from 'vs/platform/log/common/log'; - - -export const DEFAULT_STARTUP_EDITOR_CONFIG: IConfigurationNode = { - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.startupEditor': { - 'scope': ConfigurationScope.RESOURCE, - 'type': 'string', - 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'gettingStarted'], - 'enumDescriptions': [...[ - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the Getting Started page.")] - ], - 'default': 'welcomePage', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") - }, - } -}; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -export const EXPERIMENTAL_GETTING_STARTED_STARTUP_EDITOR_CONFIG: IConfigurationNode = { - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.startupEditor': { - 'scope': ConfigurationScope.RESOURCE, - 'type': 'string', - 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'gettingStarted'], - 'enumDescriptions': [...[ - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the Getting Started page.")] - ], - 'default': 'gettingStarted', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") - }, - } -}; const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; @@ -103,7 +57,6 @@ const telemetryFrom = 'welcomePage'; export class WelcomePageContribution implements IWorkbenchContribution { private experimentManagementComplete: Promise; - private tasExperimentService: ITASExperimentService | undefined; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -115,11 +68,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ICommandService private readonly commandService: ICommandService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILogService private readonly logService: ILogService, - @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { - this.tasExperimentService = tasExperimentService; // Run immediately to minimize time spent waiting for exp service. this.experimentManagementComplete = this.manageDefaultValuesForGettingStartedExperiment().catch(onUnexpectedError); @@ -132,43 +81,6 @@ export class WelcomePageContribution implements IWorkbenchContribution { if (this.lifecycleService.startupKind === StartupKind.ReloadedWindow || config.value !== config.defaultValue) { return; } - - if (this.configurationService.getValue('workbench.gettingStartedTreatmentOverride')) { - await new Promise(resolve => setTimeout(resolve, 1000)); - Registry.as(ConfigurationExtensions.Configuration).deregisterConfigurations([DEFAULT_STARTUP_EDITOR_CONFIG]); - Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(EXPERIMENTAL_GETTING_STARTED_STARTUP_EDITOR_CONFIG); - } - - let someValueReturned = false; - type GettingStartedTreatmentData = { value: string; }; - type GettingStartedTreatmentClassification = { value: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; }; - - const tasUseGettingStartedAsDefault = this.tasExperimentService?.getTreatment('StartupGettingStarted') - .then(result => { - this.logService.trace('StartupGettingStarted:', result); - this.telemetryService.publicLog2('gettingStartedTreatmentValue', { value: '' + !!result }); - someValueReturned = true; - return result; - }) - .catch(error => { - this.logService.error('Recieved error when consulting experiment service for getting started experiment', error); - this.telemetryService.publicLog2('gettingStartedTreatmentValue', { value: 'err' }); - someValueReturned = true; - return false; - }); - - const fallback = new Promise(c => setTimeout(() => c(false), 2000)).then( - () => { - if (!someValueReturned) { this.logService.trace('Unable to read getting started treatment data in time, falling back to welcome'); } - someValueReturned = true; - } - ); - - const useGettingStartedAsDefault = !!await Promise.race([tasUseGettingStartedAsDefault, fallback]); - if (useGettingStartedAsDefault) { - Registry.as(ConfigurationExtensions.Configuration).deregisterConfigurations([DEFAULT_STARTUP_EDITOR_CONFIG]); - Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(EXPERIMENTAL_GETTING_STARTED_STARTUP_EDITOR_CONFIG); - } } private async run() { @@ -224,7 +136,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { await this.experimentManagementComplete; const startupEditorSetting = this.configurationService.getValue(configurationKey); - const startupEditorTypeID = startupEditorSetting === 'gettingStarted' ? gettingStartedInputTypeId : welcomeInputTypeId; + const startupEditorTypeID = (startupEditorSetting === 'gettingStarted' || startupEditorSetting === 'gettingStartedInEmptyWorkbench') ? gettingStartedInputTypeId : welcomeInputTypeId; const editor = this.editorService.activeEditor; // Ensure that the welcome editor won't get opened more than once @@ -251,7 +163,10 @@ function isWelcomePageEnabled(configurationService: IConfigurationService, conte if (startupEditor.value === 'readme' && startupEditor.userValue !== 'readme') { console.error('Warning: `workbench.startupEditor: readme` setting ignored due to being set somewhere other than user settings'); } - return startupEditor.value === 'welcomePage' || startupEditor.value === 'gettingStarted' || startupEditor.userValue === 'readme' || startupEditor.value === 'welcomePageInEmptyWorkbench' && contextService.getWorkbenchState() === WorkbenchState.EMPTY; + return startupEditor.value === 'welcomePage' + || startupEditor.value === 'gettingStarted' + || startupEditor.userValue === 'readme' + || (contextService.getWorkbenchState() === WorkbenchState.EMPTY && (startupEditor.value === 'welcomePageInEmptyWorkbench' || startupEditor.value === 'gettingStartedInEmptyWorkbench')); } export class WelcomePageAction extends Action { @@ -748,10 +663,10 @@ export class WelcomeInputSerializer implements IEditorInputSerializer { } public serialize(editorInput: EditorInput): string { - return '{}'; + return ''; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput { + public deserialize(instantiationService: IInstantiationService): WalkThroughInput { return instantiationService.createInstance(WelcomePage) .editorInput; } diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts b/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts index 4c055d9f81ee..f174ef7a7fc0 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts @@ -10,8 +10,9 @@ import { Action } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WalkThroughInput, WalkThroughInputOptions } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { IEditorInputSerializer, EditorInput } from 'vs/workbench/common/editor'; +import { IEditorInputSerializer } from 'vs/workbench/common/editor'; import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; const typeId = 'workbench.editors.walkThroughInput'; const inputOptions: WalkThroughInputOptions = { @@ -55,10 +56,10 @@ export class EditorWalkThroughInputSerializer implements IEditorInputSerializer } public serialize(editorInput: EditorInput): string { - return '{}'; + return ''; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput { + public deserialize(instantiationService: IInstantiationService): WalkThroughInput { return instantiationService.createInstance(WalkThroughInput, inputOptions); } } diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index be87363368f7..30f595f01d2d 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { URI } from 'vs/base/common/uri'; import { DisposableStore, IReference } from 'vs/base/common/lifecycle'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -130,10 +131,7 @@ export class WalkThroughInput extends EditorInput { } if (otherInput instanceof WalkThroughInput) { - let otherResourceEditorInput = otherInput; - - // Compare by properties - return isEqual(otherResourceEditorInput.options.resource, this.options.resource); + return isEqual(otherInput.options.resource, this.options.resource); } return false; diff --git a/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 7b1af8906969..676462d52b6b 100644 --- a/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/lib/vscode/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -10,7 +10,7 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorOptions, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; @@ -26,7 +26,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Event } from 'vs/base/common/event'; import { isObject } from 'vs/base/common/types'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions as ICodeEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, textPreformatForeground, contrastBorder, textBlockQuoteBackground, textBlockQuoteBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; @@ -39,6 +39,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { CancellationToken } from 'vs/base/common/cancellation'; import { domEvent } from 'vs/base/browser/event'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export const WALK_THROUGH_FOCUS = new RawContextKey('interactivePlaygroundFocus', false); @@ -267,7 +268,7 @@ export class WalkThroughPart extends EditorPane { this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + scrollDimensions.height }); } - override setInput(input: WalkThroughInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override setInput(input: WalkThroughInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (this.input instanceof WalkThroughInput) { this.saveTextEditorViewState(this.input); } @@ -420,7 +421,7 @@ export class WalkThroughPart extends EditorPane { }); } - private getEditorOptions(language: string): IEditorOptions { + private getEditorOptions(language: string): ICodeEditorOptions { const config = deepClone(this.configurationService.getValue('editor', { overrideIdentifier: language })); return { ...isObject(config) ? config : Object.create(null), diff --git a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 4bdd5fa900ba..2ea100b6ac31 100644 --- a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -7,63 +7,183 @@ import 'vs/css!./workspaceTrustEditor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, WorkspaceTrustRequestOptions, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeColor } from 'vs/workbench/api/common/extHostTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; -import { WorkspaceTrustEditor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustEditor'; +import { shieldIcon, WorkspaceTrustEditor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustEditor'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; -import { isWorkspaceTrustEnabled, WorkspaceTrustContext, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; -import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { WorkspaceTrustContext, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_UNTRUSTED_FILES } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isWeb } from 'vs/base/common/platform'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { dirname, resolve } from 'vs/base/common/path'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import product from 'vs/platform/product/common/product'; -import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { Schemas } from 'vs/base/common/network'; import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; - +import { splitName } from 'vs/base/common/labels'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IBannerItem, IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; + +const BANNER_RESTRICTED_MODE = 'workbench.banner.restrictedMode'; const STARTUP_PROMPT_SHOWN_KEY = 'workspace.trust.startupPrompt.shown'; - +const BANNER_RESTRICTED_MODE_DISMISSED_KEY = 'workbench.banner.restrictedMode.dismissed'; /* - * Trust Request UX Handler + * Trust Request via Service UX handler */ -export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution { +export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution { constructor( @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService) { + super(); + + this.registerListeners(); + } + + private get useWorkspaceLanguage(): boolean { + return !isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); + } + + private get modalTitle(): string { + return this.useWorkspaceLanguage ? + localize('workspaceTrust', "Do you trust the authors of the files in this workspace?") : + localize('folderTrust', "Do you trust the authors of the files in this folder?"); + } + + private async registerListeners(): Promise { + await this.workspaceTrustManagementService.workspaceResolved; + this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(async requestOptions => { + // Message + const defaultMessage = localize('immediateTrustRequestMessage', "A feature you are trying to use may be a security risk if you do not trust the source of the files or folders you currently have open."); + const message = requestOptions?.message ?? defaultMessage; + + // Buttons + const buttons = requestOptions?.buttons ?? [ + { label: this.useWorkspaceLanguage ? localize('grantWorkspaceTrustButton', "Trust Workspace & Continue") : localize('grantFolderTrustButton', "Trust Folder & Continue"), type: 'ContinueWithTrust' }, + { label: localize('manageWorkspaceTrustButton', "Manage"), type: 'Manage' } + ]; + // Add Cancel button if not provided + if (!buttons.some(b => b.type === 'Cancel')) { + buttons.push({ label: localize('cancelWorkspaceTrustButton', "Cancel"), type: 'Cancel' }); + } + + // Dialog + const result = await this.dialogService.show( + Severity.Info, + this.modalTitle, + buttons.map(b => b.label), + { + cancelId: buttons.findIndex(b => b.type === 'Cancel'), + custom: { + icon: Codicon.shield, + markdownDetails: [ + { markdown: new MarkdownString(message) }, + { markdown: new MarkdownString(localize('immediateTrustRequestLearnMore', "If you don't trust the authors of these files, we do not recommend continuing as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.")) } + ] + } + } + ); + + // Dialog result + switch (buttons[result.choice].type) { + case 'ContinueWithTrust': + await this.workspaceTrustRequestService.completeRequest(true); + break; + case 'ContinueWithoutTrust': + await this.workspaceTrustRequestService.completeRequest(undefined); + break; + case 'Manage': + this.workspaceTrustRequestService.cancelRequest(); + await this.commandService.executeCommand(MANAGE_TRUST_COMMAND_ID); + break; + case 'Cancel': + this.workspaceTrustRequestService.cancelRequest(); + break; + } + })); + } +} + + +/* + * Trust UX and Startup Handler + */ +export class WorkspaceTrustUXHandler extends Disposable implements IWorkbenchContribution { + + private readonly entryId = `status.workspaceTrust.${this.workspaceContextService.getWorkspace().id}`; + + private readonly statusbarEntryAccessor: MutableDisposable; + + // try showing the banner only after some files have been opened + private showIndicatorsInEmptyWindow = false; + + constructor( + @IDialogService private readonly dialogService: IDialogService, + @IEditorService private readonly editorService: IEditorService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IStatusbarService private readonly statusbarService: IStatusbarService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IBannerService private readonly bannerService: IBannerService, + @IHostService private readonly hostService: IHostService, ) { super(); - if (isWorkspaceTrustEnabled(configurationService)) { - this.registerListeners(); - this.showModalOnStart(); - } + this.statusbarEntryAccessor = this._register(new MutableDisposable()); + + (async () => { + + await this.workspaceTrustManagementService.workspaceTrustInitialized; + + if (this.workspaceTrustManagementService.workspaceTrustEnabled) { + this.registerListeners(); + this.createStatusbarEntry(); + + // Set empty workspace trust state + await this.setEmptyWorkspaceTrustState(); + + // Show modal dialog + if (this.hostService.hasFocus) { + this.showModalOnStart(); + } else { + const focusDisposable = this.hostService.onDidChangeFocus(focused => { + if (focused) { + focusDisposable.dispose(); + this.showModalOnStart(); + } + }); + } + } + })(); } private get startupPromptSetting(): 'always' | 'once' | 'never' { @@ -108,12 +228,13 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben switch (result.choice) { case 0: if (result.checkboxChecked) { - this.workspaceTrustManagementService.setParentFolderTrust(true); + await this.workspaceTrustManagementService.setParentFolderTrust(true); } else { - this.workspaceTrustRequestService.completeRequest(true); + await this.workspaceTrustRequestService.completeRequest(true); } break; case 1: + this.updateWorkbenchIndicators(false); this.workspaceTrustRequestService.cancelRequest(); break; } @@ -121,16 +242,36 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben this.storageService.store(STARTUP_PROMPT_SHOWN_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - private showModalOnStart(): void { + private async showModalOnStart(): Promise { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + this.updateWorkbenchIndicators(true); + return; + } + + // Don't show modal prompt if workspace trust cannot be changed + if (!(this.workspaceTrustManagementService.canSetWorkspaceTrust())) { + return; + } + + // Don't show modal prompt for virtual workspaces by default + if (isVirtualWorkspace(this.workspaceContextService.getWorkspace())) { + this.updateWorkbenchIndicators(false); + return; + } + + // Don't show modal prompt for empty workspaces by default + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY) { + this.updateWorkbenchIndicators(false); return; } if (this.startupPromptSetting === 'never') { + this.updateWorkbenchIndicators(false); return; } if (this.startupPromptSetting === 'once' && this.storageService.getBoolean(STARTUP_PROMPT_SHOWN_KEY, StorageScope.WORKSPACE, false)) { + this.updateWorkbenchIndicators(false); return; } @@ -138,7 +279,9 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())!; const isSingleFolderWorkspace = isSingleFolderWorkspaceIdentifier(workspaceIdentifier); if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { - checkboxText = localize('checkboxString', "I trust the authors of all files in the parent folder"); + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + const { name } = splitName(parentPath); + checkboxText = localize('checkboxString', "Trust the authors of all files in the parent folder '{0}'", name); } // Show Workspace Trust Start Dialog @@ -148,72 +291,212 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben { label: localize('dontTrustOption', "No, I don't trust the authors"), sublabel: isSingleFolderWorkspace ? localize('dontTrustFolderOptionDescription', "Browse folder in restricted mode") : localize('dontTrustWorkspaceOptionDescription', "Browse workspace in restricted mode") }, [ !isSingleFolderWorkspace ? - localize('workspaceStartupTrustDetails', "{0} provides advanced editing features that may automatically execute files in this workspace.", product.nameShort) : - localize('folderStartupTrustDetails', "{0} provides advanced editing features that may automatically execute files in this folder.", product.nameShort), + localize('workspaceStartupTrustDetails', "{0} provides features that may automatically execute files in this workspace.", product.nameShort) : + localize('folderStartupTrustDetails', "{0} provides features that may automatically execute files in this folder.", product.nameShort), localize('startupTrustRequestLearnMore', "If you don't trust the authors of these files, we recommend to continue in restricted mode as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") ], checkboxText ); } - private registerListeners(): void { - this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(async requestOptions => { - if (requestOptions.modal) { - // Message - const defaultMessage = localize('immediateTrustRequestMessage', "A feature you are trying to use may be a security risk if you do not trust the source of the files or folders you currently have open."); - const message = requestOptions.message ?? defaultMessage; - - // Buttons - const buttons = requestOptions.buttons ?? [ - { label: this.useWorkspaceLanguage ? localize('grantWorkspaceTrustButton', "Trust Workspace & Continue") : localize('grantFolderTrustButton', "Trust Folder & Continue"), type: 'ContinueWithTrust' }, - { label: localize('manageWorkspaceTrustButton', "Manage"), type: 'Manage' } - ]; - // Add Cancel button if not provided - if (!buttons.some(b => b.type === 'Cancel')) { - buttons.push({ label: localize('cancelWorkspaceTrustButton', "Cancel"), type: 'Cancel' }); + private createStatusbarEntry(): void { + const entry = this.getStatusbarEntry(this.workspaceTrustManagementService.isWorkpaceTrusted()); + this.statusbarEntryAccessor.value = this.statusbarService.addEntry(entry, this.entryId, StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); + this.statusbarService.updateEntryVisibility(this.entryId, false); + } + + private getBannerItem(restrictedMode: boolean): IBannerItem | undefined { + + const dismissedRestricted = this.storageService.getBoolean(BANNER_RESTRICTED_MODE_DISMISSED_KEY, StorageScope.WORKSPACE, false); + + // info has been dismissed + if (dismissedRestricted) { + return undefined; + } + + const actions = + [ + { + label: localize('restrictedModeBannerManage', "Manage"), + href: 'command:' + MANAGE_TRUST_COMMAND_ID + }, + { + label: localize('restrictedModeBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-workspace-trust' } + ]; - // Dialog - const result = await this.dialogService.show( - Severity.Info, - this.modalTitle, - buttons.map(b => b.label), - { - cancelId: buttons.findIndex(b => b.type === 'Cancel'), - custom: { - icon: Codicon.shield, - markdownDetails: [ - { markdown: new MarkdownString(message) }, - { markdown: new MarkdownString(localize('immediateTrustRequestLearnMore', "If you don't trust the authors of these files, we do not recommend continuing as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.")) } - ] - } - } - ); - - // Dialog result - switch (buttons[result.choice].type) { - case 'ContinueWithTrust': - this.workspaceTrustRequestService.completeRequest(true); - break; - case 'ContinueWithoutTrust': - this.workspaceTrustRequestService.completeRequest(undefined); - break; - case 'Manage': - this.workspaceTrustRequestService.cancelRequest(); - await this.commandService.executeCommand('workbench.trust.manage'); - break; - case 'Cancel': - this.workspaceTrustRequestService.cancelRequest(); - break; + return { + id: BANNER_RESTRICTED_MODE, + icon: shieldIcon, + ariaLabel: this.getBannerItemAriaLabels(), + message: this.getBannerItemMessages(), + actions, + onClose: () => { + if (restrictedMode) { + this.storageService.store(BANNER_RESTRICTED_MODE_DISMISSED_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); } } - })); + }; + } + + private getBannerItemAriaLabels(): string { + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('restrictedModeBannerAriaLabelWindow', "Restricted Mode is intended for safe code browsing. Trust this window to enable all features. Use navigation keys to access banner actions."); + case WorkbenchState.FOLDER: + return localize('restrictedModeBannerAriaLabelFolder', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."); + case WorkbenchState.WORKSPACE: + return localize('restrictedModeBannerAriaLabelWorkspace', "Restricted Mode is intended for safe code browsing. Trust this workspace to enable all features. Use navigation keys to access banner actions."); + } + } + + private getBannerItemMessages(): string { + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('restrictedModeBannerMessageWindow', "Restricted Mode is intended for safe code browsing. Trust this window to enable all features."); + case WorkbenchState.FOLDER: + return localize('restrictedModeBannerMessageFolder', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."); + case WorkbenchState.WORKSPACE: + return localize('restrictedModeBannerMessageWorkspace', "Restricted Mode is intended for safe code browsing. Trust this workspace to enable all features."); + } + } + + private getStatusbarEntry(trusted: boolean): IStatusbarEntry { + const text = workspaceTrustToString(trusted); + const backgroundColor = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND); + const color = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND); + + let ariaLabel = ''; + let toolTip: IMarkdownString | string | undefined; + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: { + ariaLabel = trusted ? localize('status.ariaTrustedWindow', "This window is trusted.") : + localize('status.ariaUntrustedWindow', "Restricted Mode: Some features are disabled because this window is not trusted."); + toolTip = trusted ? ariaLabel : { + value: localize( + { key: 'status.tooltipUntrustedWindow2', comment: ['[abc]({n}) are links. Only translate `features are disabled` and `window is not trusted`. Do not change brackets and parentheses or {n}'] }, + "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [window is not trusted]({1}).", + `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`, + `command:${MANAGE_TRUST_COMMAND_ID}` + ), + isTrusted: true, + supportThemeIcons: true + }; + break; + } + case WorkbenchState.FOLDER: { + ariaLabel = trusted ? localize('status.ariaTrustedFolder', "This folder is trusted.") : + localize('status.ariaUntrustedFolder', "Restricted Mode: Some features are disabled because this folder is not trusted."); + toolTip = trusted ? ariaLabel : { + value: localize( + { key: 'status.tooltipUntrustedFolder2', comment: ['[abc]({n}) are links. Only translate `features are disabled` and `folder is not trusted`. Do not change brackets and parentheses or {n}'] }, + "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [folder is not trusted]({1}).", + `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`, + `command:${MANAGE_TRUST_COMMAND_ID}` + ), + isTrusted: true, + supportThemeIcons: true + }; + break; + } + case WorkbenchState.WORKSPACE: { + ariaLabel = trusted ? localize('status.ariaTrustedWorkspace', "This workspace is trusted.") : + localize('status.ariaUntrustedWorkspace', "Restricted Mode: Some features are disabled because this workspace is not trusted."); + toolTip = trusted ? ariaLabel : { + value: localize( + { key: 'status.tooltipUntrustedWorkspace2', comment: ['[abc]({n}) are links. Only translate `features are disabled` and `workspace is not trusted`. Do not change brackets and parentheses or {n}'] }, + "Running in Restricted Mode\n\nSome [features are disabled]({0}) because this [workspace is not trusted]({1}).", + `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`, + `command:${MANAGE_TRUST_COMMAND_ID}` + ), + isTrusted: true, + supportThemeIcons: true + }; + break; + } + } + + return { + name: localize('status.WorkspaceTrust', "Workspace Trust"), + text: trusted ? `$(shield)` : `$(shield) ${text}`, + ariaLabel: ariaLabel, + tooltip: toolTip, + command: MANAGE_TRUST_COMMAND_ID, + backgroundColor, + color + }; + } + + private async setEmptyWorkspaceTrustState(): Promise { + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + return; + } + + // Open files + const openFiles = this.editorService.editors.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { filterByScheme: Schemas.file })).filter(uri => !!uri); + + if (openFiles.length) { + this.showIndicatorsInEmptyWindow = true; + + // If all open files are trusted, transition to a trusted workspace + const openFilesTrustInfo = await Promise.all(openFiles.map(uri => this.workspaceTrustManagementService.getUriTrustInfo(uri!))); + + if (openFilesTrustInfo.map(info => info.trusted).every(trusted => trusted)) { + this.workspaceTrustManagementService.setWorkspaceTrust(true); + } + } else { + // No open files, use the setting to set workspace trust state + const disposable = this._register(this.editorService.onDidActiveEditorChange(() => { + const editor = this.editorService.activeEditor; + if (editor && !!EditorResourceAccessor.getCanonicalUri(editor, { filterByScheme: Schemas.file })) { + this.showIndicatorsInEmptyWindow = true; + this.updateWorkbenchIndicators(this.workspaceTrustManagementService.isWorkpaceTrusted()); + disposable.dispose(); + } + })); + // TODO: Consider moving the check into setWorkspaceTrust() + if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { + if (this.configurationService.getValue(WORKSPACE_TRUST_EMPTY_WINDOW)) { + this.workspaceTrustManagementService.setWorkspaceTrust(true); + } + } + } + } + + private updateStatusbarEntry(trusted: boolean): void { + this.statusbarEntryAccessor.value?.update(this.getStatusbarEntry(trusted)); + this.updateStatusbarEntryVisibility(trusted); + } + + private updateStatusbarEntryVisibility(trusted: boolean): void { + this.statusbarService.updateEntryVisibility(this.entryId, !trusted); + } + + private updateWorkbenchIndicators(trusted: boolean): void { + const isEmptyWorkspace = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY; + const bannerItem = this.getBannerItem(!trusted); + + if (!isEmptyWorkspace || this.showIndicatorsInEmptyWindow) { + this.updateStatusbarEntry(trusted); + + if (bannerItem) { + if (!trusted) { + this.bannerService.show(bannerItem); + } else { + this.bannerService.hide(BANNER_RESTRICTED_MODE); + } + } + } + } + + private registerListeners(): void { this._register(this.workspaceContextService.onWillChangeWorkspaceFolders(e => { if (e.fromCache) { return; } - if (!isWorkspaceTrustEnabled(this.configurationService)) { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } const trusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); @@ -221,8 +504,9 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return e.join(new Promise(async resolve => { // Workspace is trusted and there are added/changed folders if (trusted && (e.changes.added.length || e.changes.changed.length)) { - const addedFoldersTrustInfo = e.changes.added.map(folder => this.workspaceTrustManagementService.getFolderTrustInfo(folder.uri)); - if (!addedFoldersTrustInfo.map(i => i.trusted).every(trusted => trusted)) { + const addedFoldersTrustInfo = await Promise.all(e.changes.added.map(folder => this.workspaceTrustManagementService.getUriTrustInfo(folder.uri))); + + if (!addedFoldersTrustInfo.map(info => info.trusted).every(trusted => trusted)) { const result = await this.dialogService.show( Severity.Info, localize('addWorkspaceFolderMessage', "Do you trust the authors of the files in this folder?"), @@ -235,7 +519,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben ); // Mark added/changed folders as trusted - this.workspaceTrustManagementService.setFoldersTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); + await this.workspaceTrustManagementService.setUrisTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); resolve(); } @@ -244,75 +528,15 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben resolve(); })); })); - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTrustRequestHandler, LifecyclePhase.Ready); - -/* - * Status Bar Entry - */ -class WorkspaceTrustStatusbarItem extends Disposable implements IWorkbenchContribution { - private readonly entryId = `status.workspaceTrust.${this.workspaceService.getWorkspace().id}`; - private readonly statusBarEntryAccessor: MutableDisposable; - private pendingRequestContextKey = WorkspaceTrustContext.PendingRequest.key; - private contextKeys = new Set([this.pendingRequestContextKey]); - - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { - super(); - - this.statusBarEntryAccessor = this._register(new MutableDisposable()); - - if (isWorkspaceTrustEnabled(configurationService)) { - const entry = this.getStatusbarEntry(this.workspaceTrustManagementService.isWorkpaceTrusted()); - this.statusBarEntryAccessor.value = this.statusbarService.addEntry(entry, this.entryId, localize('status.WorkspaceTrust', "Workspace Trust"), StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); - this._register(this.workspaceTrustManagementService.onDidChangeTrust(trusted => this.updateStatusbarEntry(trusted))); - this._register(this.contextKeyService.onDidChangeContext((contextChange) => { - if (contextChange.affectsSome(this.contextKeys)) { - this.updateVisibility(this.workspaceTrustManagementService.isWorkpaceTrusted()); - } - })); - - this.updateVisibility(this.workspaceTrustManagementService.isWorkpaceTrusted()); - } - } - - private getStatusbarEntry(trusted: boolean): IStatusbarEntry { - const text = workspaceTrustToString(trusted); - const backgroundColor = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND); - const color = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND); - - return { - text: trusted ? `$(shield)` : `$(shield) ${text}`, - ariaLabel: trusted ? localize('status.ariaTrusted', "This workspace is trusted.") : localize('status.ariaUntrusted', "Restricted Mode: Some features are disabled because this workspace is not trusted."), - tooltip: trusted ? localize('status.tooltipTrusted', "This workspace is trusted.") : localize('status.tooltipUntrusted', "Some features are disabled because this workspace is not trusted."), - command: 'workbench.trust.manage', - backgroundColor, - color - }; - } - - private updateVisibility(trusted: boolean): void { - const pendingRequest = this.contextKeyService.getContextKeyValue(this.pendingRequestContextKey) === true; - this.statusbarService.updateEntryVisibility(this.entryId, !trusted || pendingRequest); - } - - private updateStatusbarEntry(trusted: boolean): void { - this.statusBarEntryAccessor.value?.update(this.getStatusbarEntry(trusted)); - this.updateVisibility(trusted); + this._register(this.workspaceTrustManagementService.onDidChangeTrust(trusted => { + this.updateWorkbenchIndicators(trusted); + })); } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( - WorkspaceTrustStatusbarItem, - LifecyclePhase.Starting -); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTrustRequestHandler, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTrustUXHandler, LifecyclePhase.Restored); /** * Trusted Workspace GUI Editor @@ -324,10 +548,10 @@ class WorkspaceTrustEditorInputSerializer implements IEditorInputSerializer { } serialize(input: WorkspaceTrustEditorInput): string { - return '{}'; + return ''; } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WorkspaceTrustEditorInput { + deserialize(instantiationService: IInstantiationService): WorkspaceTrustEditorInput { return instantiationService.createInstance(WorkspaceTrustEditorInput); } } @@ -350,11 +574,13 @@ Registry.as(EditorExtensions.Editors).registerEditor( * Actions */ +const MANAGE_TRUST_COMMAND_ID = 'workbench.trust.manage'; + // Manage Workspace Trust registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.trust.manage', + id: MANAGE_TRUST_COMMAND_ID, title: { original: 'Manage Workspace Trust', value: localize('manageWorkspaceTrust', "Manage Workspace Trust") @@ -364,7 +590,7 @@ registerAction2(class extends Action2 { id: MenuId.GlobalActivity, group: '6_workspace_trust', order: 40, - when: ContextKeyExpr.and(IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), WorkspaceTrustContext.PendingRequest.negate()) + when: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)) }, }); } @@ -380,16 +606,6 @@ registerAction2(class extends Action2 { } }); -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - command: { - id: 'workbench.trust.manage', - title: localize('manageWorkspaceTrustPending', "Manage Workspace Trust (1)"), - }, - group: '6_workspace_trust', - order: 40, - when: ContextKeyExpr.and(IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true), WorkspaceTrustContext.PendingRequest) -}); - /* * Configuration */ @@ -403,10 +619,10 @@ Registry.as(ConfigurationExtensions.Configuration) properties: { [WORKSPACE_TRUST_ENABLED]: { type: 'boolean', - default: false, + default: true, included: !isWeb, description: localize('workspace.trust.description', "Controls whether or not workspace trust is enabled within VS Code."), - scope: ConfigurationScope.APPLICATION + scope: ConfigurationScope.APPLICATION, }, [WORKSPACE_TRUST_STARTUP_PROMPT]: { type: 'string', @@ -420,6 +636,26 @@ Registry.as(ConfigurationExtensions.Configuration) localize('workspace.trust.startupPrompt.once', "Ask for trust the first time an untrusted workspace is opened."), localize('workspace.trust.startupPrompt.never', "Do not ask for trust when an untrusted workspace is opened."), ] + }, + [WORKSPACE_TRUST_UNTRUSTED_FILES]: { + type: 'string', + default: 'prompt', + included: !isWeb, + markdownDescription: localize('workspace.trust.untrustedFiles.description', "Controls how to handle opening untrusted files in a trusted workspace. This setting also applies to opening files in an empty window which is trusted via `#{0}#`.", WORKSPACE_TRUST_EMPTY_WINDOW), + scope: ConfigurationScope.APPLICATION, + enum: ['prompt', 'open', 'newWindow'], + enumDescriptions: [ + localize('workspace.trust.untrustedFiles.prompt', "Ask how to handle untrusted files for each workspace. Once untrusted files are introduced to a trusted workspace, you will not be prompted again."), + localize('workspace.trust.untrustedFiles.open', "Always allow untrusted files to be introduced to a trusted workspace without prompting."), + localize('workspace.trust.untrustedFiles.newWindow', "Always open untrusted files in a separate window in restricted mode without prompting."), + ] + }, + [WORKSPACE_TRUST_EMPTY_WINDOW]: { + type: 'boolean', + default: true, + included: !isWeb, + markdownDescription: localize('workspace.trust.emptyWindow.description', "Controls whether or not the empty window is trusted by default within VS Code. When used with `#{0}#`, you can enable the full functionality of VS Code without prompting in an empty window.", WORKSPACE_TRUST_UNTRUSTED_FILES), + scope: ConfigurationScope.APPLICATION } } }); @@ -429,7 +665,6 @@ Registry.as(ConfigurationExtensions.Configuration) */ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkbenchContribution { constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -439,13 +674,13 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben super(); this._register(this.workspaceTrustManagementService.onDidChangeTrust(isTrusted => this.logWorkspaceTrustChangeEvent(isTrusted))); - this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(options => this.logWorkspaceTrustRequest(options))); + this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequest(_ => this.logWorkspaceTrustRequest())); this.logInitialWorkspaceTrustInfo(); } private logInitialWorkspaceTrustInfo(): void { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } @@ -458,12 +693,12 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; this.telemetryService.publicLog2('workspaceTrustFolderCounts', { - trustedFoldersCount: this.workspaceTrustManagementService.getTrustedFolders().length, + trustedFoldersCount: this.workspaceTrustManagementService.getTrustedUris().length, }); } - private logWorkspaceTrustChangeEvent(isTrusted: boolean): void { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + private async logWorkspaceTrustChangeEvent(isTrusted: boolean): Promise { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } @@ -508,7 +743,7 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; for (const folder of this.workspaceContextService.getWorkspace().folders) { - const { trusted, uri } = this.workspaceTrustManagementService.getFolderTrustInfo(folder.uri); + const { trusted, uri } = await this.workspaceTrustManagementService.getUriTrustInfo(folder.uri); if (!trusted) { continue; } @@ -522,25 +757,22 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben } } - private async logWorkspaceTrustRequest(options: WorkspaceTrustRequestOptions): Promise { - if (!isWorkspaceTrustEnabled(this.configurationService)) { + private async logWorkspaceTrustRequest(): Promise { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled) { return; } type WorkspaceTrustRequestedEventClassification = { - modal: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; workspaceId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; extensions: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; type WorkspaceTrustRequestedEvent = { - modal: boolean, workspaceId: string, extensions: string[] }; this.telemetryService.publicLog2('workspaceTrustRequested', { - modal: options.modal, workspaceId: this.workspaceContextService.getWorkspace().id, extensions: (await this.extensionService.getExtensions()).filter(ext => !!ext.capabilities?.untrustedWorkspaces).map(ext => ext.identifier.value) }); diff --git a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts deleted file mode 100644 index 9260d6261b30..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustColors.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from 'vs/nls'; -import { editorErrorForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { welcomePageTileBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors'; - -export const trustedForegroundColor = registerColor('workspaceTrust.trustedForegound', { dark: debugIconStartForeground, light: debugIconStartForeground, hc: debugIconStartForeground }, localize('workspaceTrustTrustedColor', 'Color used when indicating a workspace is trusted.')); -export const untrustedForegroundColor = registerColor('workspaceTrust.untrustedForeground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, localize('workspaceTrustUntrustedColor', 'Color used when indicating a workspace is not trusted.')); - -export const trustEditorTileBackgroundColor = registerColor('workspaceTrust.tileBackground', { dark: welcomePageTileBackground, light: welcomePageTileBackground, hc: welcomePageTileBackground }, localize('workspaceTrust.tileBackground', 'Background color for the tiles on the Workspace Trust page.')); diff --git a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index de08b35453c9..b26d4cc9aa5b 100644 --- a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -6,13 +6,13 @@ .monaco-icon-label.file-icon.workspacetrusteditor-name-file-icon.ext-file-icon.tab-label::before { font-family: 'codicon'; content: '\eb53'; + background-image: none; + font-size: 150%; } .workspace-trust-editor { max-width: 1000px; padding-top: 11px; - padding-left: 15px; - padding-right: 15px; margin: auto; height: calc(100% - 11px); } @@ -42,7 +42,7 @@ } .workspace-trust-editor .workspace-trust-header .workspace-trust-title .workspace-trust-title-icon { - color: var(--workspace-trust-selected-state-color) !important; + color: var(--workspace-trust-selected-color) !important; } .workspace-trust-editor .workspace-trust-header .workspace-trust-description { @@ -74,21 +74,23 @@ user-select: text; display: flex; flex-direction: row; + flex-flow: wrap; justify-content: space-evenly; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { min-height: 315px; + border: 1px solid var(--workspace-trust-unselected-color); + margin: 4px 4px; + display: flex; + flex-direction: column; + padding: 10px 40px; } -.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted { - border-width: 2px; - border-color: var(--workspace-trust-selected-state-color) !important; -} - +.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted, .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted { border-width: 2px; - border-color: var(--workspace-trust-selected-state-color) !important; + border-color: var(--workspace-trust-selected-color) !important; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations ul { @@ -107,12 +109,12 @@ } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations.trusted .list-item-icon { - color: var(--workspace-trust-trusted-color) !important; + color: var(--workspace-trust-check-color) !important; font-size: 18px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations.untrusted .list-item-icon { - color: var(--workspace-trust-untrusted-color) !important; + color: var(--workspace-trust-x-color) !important; font-size: 20px; } @@ -141,14 +143,10 @@ display: none; } -.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon { - display: unset; - color: var(--workspace-trust-selected-state-color) !important; -} - +.workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon, .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon { display: unset; - color: var(--workspace-trust-selected-state-color) !important; + color: var(--workspace-trust-selected-color) !important; } .workspace-trust-editor .workspace-trust-features .workspace-trust-untrusted-description { @@ -179,79 +177,104 @@ padding: 5px 10px; overflow: hidden; text-overflow: ellipsis; - margin: 4px 5px; /* allows button focus outline to be visible */ outline-offset: 2px !important; } -.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown { - padding: 0 2px; -} - -.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown .monaco-button { - margin-left: 1px; - margin-right: 1px; +.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons > .monaco-button, +.workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons > .monaco-button-dropdown { + margin: 4px 5px; /* allows button focus outline to be visible */ } .workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown .monaco-dropdown-button { - padding: 5px 0px; + padding: 5px; } .workspace-trust-limitations { width: 50%; max-width: 350px; + min-width: 250px; + flex: 1; } -/** Settings */ -.workspace-trust-editor .workspace-trust-settings .workspace-trust-section-title { - padding: 14px; +.workspace-trust-intro-dialog { + min-width: min(50vw, 500px); + padding-right: 24px; } -.workspace-trust-editor .settings-editor .settings-body { - margin-top: 0; +.workspace-trust-intro-dialog .workspace-trust-dialog-image-row p { + display: flex; + align-items: center; } -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .shadow.top { - left: initial; - margin-left: initial; - max-width: initial; +.workspace-trust-intro-dialog .workspace-trust-dialog-image-row.badge-row img { + max-height: 40px; + padding-right: 10px; } -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-rows { - background: unset; +.workspace-trust-intro-dialog .workspace-trust-dialog-image-row.status-bar img { + max-height: 32px; + padding-right: 10px; } -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents { - padding-left: 0; - padding-right: 0; +.workspace-trust-editor .workspace-trust-settings { + padding: 20px 14px; } -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents .setting-list-edit-row > .setting-list-valueInput { - width: 100%; - max-width: 100%; +.workspace-trust-editor .workspace-trust-settings .workspace-trusted-folders-title { + font-weight: 600; } -.workspace-trust-editor .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key, -.workspace-trust-editor .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-input-key { - margin-left: 4px; - min-width: 20%; +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path:not(.input-mode) .monaco-inputbox, +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path.input-mode .path-label { + display: none; } -.workspace-trust-intro-dialog { - min-width: min(50vw, 500px); - padding-right: 24px; +.workspace-trust-editor .monaco-table-tr .monaco-table-td .current-workspace-parent .path-label, +.workspace-trust-editor .monaco-table-tr .monaco-table-td .current-workspace-parent .host-label { + font-weight: bold; + font-style: italic; } -.workspace-trust-intro-dialog .workspace-trust-dialog-image-row p { + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path .monaco-inputbox input { + padding-left: 5px; +} + +.workspace-trust-editor .monaco-table-th, +.workspace-trust-editor .monaco-table-td { + padding-left: 5px; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-action-bar .action-item > .codicon { display: flex; align-items: center; + justify-content: center; + color: inherit; } -.workspace-trust-intro-dialog .workspace-trust-dialog-image-row.badge-row img { - max-height: 40px; - padding-right: 10px; +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td { + align-items: center; + display: flex; + overflow: hidden; } -.workspace-trust-intro-dialog .workspace-trust-dialog-image-row.status-bar img { - max-height: 32px; - padding-right: 10px; +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .path { + width: 100%; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .monaco-button { + height: 18px; + padding-left: 8px; + padding-right: 8px; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: none; + flex: 1; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-list-row.selected .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-table .monaco-list-row.focused .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-list-row:hover .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: flex; } diff --git a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 553260531d9b..4f95167c1f1c 100644 --- a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -3,56 +3,530 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append, clearNode, Dimension, EventHelper } from 'vs/base/browser/dom'; +import { $, addDisposableListener, addStandardDisposableListener, append, clearNode, Dimension, EventHelper, EventType } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Action } from 'vs/base/common/actions'; +import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { Action, IAction } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon, registerCodicon } from 'vs/base/common/codicons'; -import { Color, RGBA } from 'vs/base/common/color'; import { debounce } from 'vs/base/common/decorators'; -import { Iterable } from 'vs/base/common/iterator'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { splitName } from 'vs/base/common/labels'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchTable } from 'vs/platform/list/browser/listService'; +import { IPromptChoiceWithMenu } from 'vs/platform/notification/common/notification'; import { Link } from 'vs/platform/opener/browser/link'; import product from 'vs/platform/product/common/product'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachLinkStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { buttonBackground, buttonSecondaryBackground, editorErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { attachButtonStyler, attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ChoiceAction } from 'vs/workbench/common/notifications'; -import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { IExtensionsWorkbenchService, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; -import { trustedForegroundColor, untrustedForegroundColor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustColors'; -import { IWorkspaceTrustSettingChangeEvent, WorkspaceTrustSettingArrayRenderer, WorkspaceTrustTree, WorkspaceTrustTreeModel } from 'vs/workbench/contrib/workspace/browser/workspaceTrustTree'; -import { filterSettingsRequireWorkspaceTrust, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; -const shieldIcon = registerCodicon('workspace-trust-icon', Codicon.shield); +export const shieldIcon = registerCodicon('workspace-trust-icon', Codicon.shield); const checkListIcon = registerCodicon('workspace-trusted-check-icon', Codicon.check); const xListIcon = registerCodicon('workspace-trusted-x-icon', Codicon.x); +const enum TrustedUriItemType { + Existing = 1, + Add = 2 +} + +interface ITrustedUriItem { + entryType: TrustedUriItemType; + parentOfWorkspaceItem: boolean; + uri: URI; +} + +class WorkspaceTrustedUrisTable extends Disposable { + private readonly _onDidAcceptEdit: Emitter = this._register(new Emitter()); + readonly onDidAcceptEdit: Event = this._onDidAcceptEdit.event; + + private readonly _onDidRejectEdit: Emitter = this._register(new Emitter()); + readonly onDidRejectEdit: Event = this._onDidRejectEdit.event; + + private _onEdit: Emitter = this._register(new Emitter()); + readonly onEdit: Event = this._onEdit.event; + + private _onDelete: Emitter = this._register(new Emitter()); + readonly onDelete: Event = this._onDelete.event; + + private readonly table: WorkbenchTable; + + constructor( + private readonly container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IUriIdentityService private readonly uriService: IUriIdentityService, + @IFileDialogService private readonly fileDialogService: IFileDialogService + ) { + super(); + + this.table = this.instantiationService.createInstance( + WorkbenchTable, + 'WorkspaceTrust', + this.container, + new TrustedUriTableVirtualDelegate(), + [ + { + label: localize('hostColumnLabel', "Host"), + tooltip: '', + weight: 1, + templateId: TrustedUriHostColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + { + label: localize('pathColumnLabel', "Path"), + tooltip: '', + weight: 9, + templateId: TrustedUriPathColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + { + label: '', + tooltip: '', + weight: 0, + minimumWidth: 55, + maximumWidth: 55, + templateId: TrustedUriActionsColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + ], + [ + this.instantiationService.createInstance(TrustedUriHostColumnRenderer, this), + this.instantiationService.createInstance(TrustedUriPathColumnRenderer, this), + this.instantiationService.createInstance(TrustedUriActionsColumnRenderer, this), + ], + { + horizontalScrolling: false, + alwaysConsumeMouseWheel: false, + openOnSingleClick: false, + } + ) as WorkbenchTable; + + this._register(this.table.onDidOpen(item => { + // default prevented when input box is double clicked #125052 + if (item && item.element && !item.browserEvent?.defaultPrevented) { + this.edit(item.element); + } + })); + + this._register(this.workspaceTrustManagementService.onDidChangeTrustedFolders(() => { + this.updateTable(); + })); + } + + private getIndexOfTrustedUriEntry(item: ITrustedUriItem): number { + const index = this.trustedUriEntries.indexOf(item); + if (index === -1) { + for (let i = 0; i < this.trustedUriEntries.length; i++) { + if (this.trustedUriEntries[i].entryType !== item.entryType) { + continue; + } + + if (item.entryType === TrustedUriItemType.Add || this.trustedUriEntries[i].uri === item.uri) { + return i; + } + } + } + + return index; + } + + private selectTrustedUriEntry(item: ITrustedUriItem, focus: boolean = true): void { + const index = this.getIndexOfTrustedUriEntry(item); + if (index !== -1) { + if (focus) { + this.table.domFocus(); + this.table.setFocus([index]); + } + this.table.setSelection([index]); + } + } + + private get currentWorkspaceUri(): URI { + return this.workspaceService.getWorkspace().folders[0]?.uri || URI.file('/'); + } + + private get trustedUriEntries(): ITrustedUriItem[] { + const currentWorkspace = this.workspaceService.getWorkspace(); + const currentWorkspaceUris = currentWorkspace.folders.map(folder => folder.uri); + if (currentWorkspace.configuration) { + currentWorkspaceUris.push(currentWorkspace.configuration); + } + + const entries = this.workspaceTrustManagementService.getTrustedUris().map(uri => { + + let relatedToCurrentWorkspace = false; + for (const workspaceUri of currentWorkspaceUris) { + relatedToCurrentWorkspace = relatedToCurrentWorkspace || this.uriService.extUri.isEqualOrParent(workspaceUri, uri); + } + + return { + uri, + entryType: TrustedUriItemType.Existing, + parentOfWorkspaceItem: relatedToCurrentWorkspace + }; + }); + entries.push({ uri: this.currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); + return entries; + } + + layout(): void { + this.table.layout((this.trustedUriEntries.length * TrustedUriTableVirtualDelegate.ROW_HEIGHT) + TrustedUriTableVirtualDelegate.HEADER_ROW_HEIGHT, undefined); + } + + updateTable(): void { + this.table.splice(0, Number.POSITIVE_INFINITY, this.trustedUriEntries); + this.layout(); + } + + acceptEdit(item: ITrustedUriItem, uri: URI) { + const trustedFolders = this.workspaceTrustManagementService.getTrustedUris(); + const index = this.getIndexOfTrustedUriEntry(item); + + if (index >= trustedFolders.length) { + trustedFolders.push(uri); + } else { + trustedFolders[index] = uri; + } + + this.workspaceTrustManagementService.setTrustedUris(trustedFolders); + this._onDidAcceptEdit.fire(item); + } + + rejectEdit(item: ITrustedUriItem) { + this._onDidRejectEdit.fire(item); + } + + async delete(item: ITrustedUriItem) { + await this.workspaceTrustManagementService.setUrisTrust([item.uri], false); + this._onDelete.fire(item); + } + + async edit(item: ITrustedUriItem) { + const canUseOpenDialog = item.uri.scheme === Schemas.file || + (item.uri.scheme === this.currentWorkspaceUri.scheme && this.uriService.extUri.isEqualAuthority(this.currentWorkspaceUri.authority, item.uri.authority)); + if (canUseOpenDialog) { + const uri = await this.fileDialogService.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: item.uri, + openLabel: localize('trustUri', "Trust Folder"), + title: localize('selectTrustedUri', "Select Folder To Trust") + }); + + if (uri) { + this.acceptEdit(item, uri[0]); + } else { + this.rejectEdit(item); + } + } else { + this.selectTrustedUriEntry(item); + this._onEdit.fire(item); + } + } +} + +class TrustedUriTableVirtualDelegate implements ITableVirtualDelegate { + static readonly HEADER_ROW_HEIGHT = 30; + static readonly ROW_HEIGHT = 24; + readonly headerRowHeight = TrustedUriTableVirtualDelegate.HEADER_ROW_HEIGHT; + getHeight(item: ITrustedUriItem) { + return TrustedUriTableVirtualDelegate.ROW_HEIGHT; + } +} + +interface IActionsColumnTemplateData { + readonly actionBar: ActionBar; +} + +class TrustedUriActionsColumnRenderer implements ITableRenderer { + + static readonly TEMPLATE_ID = 'actions'; + + readonly templateId: string = TrustedUriActionsColumnRenderer.TEMPLATE_ID; + + constructor(private readonly table: WorkspaceTrustedUrisTable) { } + + renderTemplate(container: HTMLElement): IActionsColumnTemplateData { + const element = container.appendChild($('.actions')); + const actionBar = new ActionBar(element, { animated: false }); + return { actionBar }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: IActionsColumnTemplateData, height: number | undefined): void { + templateData.actionBar.clear(); + + if (item.entryType !== TrustedUriItemType.Add) { + const actions: IAction[] = []; + actions.push(this.createEditAction(item)); + actions.push(this.createDeleteAction(item)); + templateData.actionBar.push(actions, { icon: true }); + } + } + + private createEditAction(item: ITrustedUriItem): IAction { + return { + class: ThemeIcon.asClassName(settingsEditIcon), + enabled: true, + id: 'editTrustedUri', + tooltip: localize('editTrustedUri', "Change Path"), + run: () => { + this.table.edit(item); + } + }; + } + + private createDeleteAction(item: ITrustedUriItem): IAction { + return { + class: ThemeIcon.asClassName(settingsRemoveIcon), + enabled: true, + id: 'deleteTrustedUri', + tooltip: localize('deleteTrustedUri', "Delete Path"), + run: async () => { + await this.table.delete(item); + } + }; + } + + disposeTemplate(templateData: IActionsColumnTemplateData): void { + templateData.actionBar.dispose(); + } + +} + +interface ITrustedUriPathColumnTemplateData { + element: HTMLElement; + pathLabel: HTMLElement; + pathInput: InputBox; + renderDisposables: DisposableStore; + disposables: DisposableStore; +} + +class TrustedUriPathColumnRenderer implements ITableRenderer { + static readonly TEMPLATE_ID = 'path'; + + readonly templateId: string = TrustedUriPathColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly table: WorkspaceTrustedUrisTable, + @IContextViewService private readonly contextViewService: IContextViewService, + @IThemeService private readonly themeService: IThemeService, + ) { + } + + renderTemplate(container: HTMLElement): ITrustedUriPathColumnTemplateData { + const element = container.appendChild($('.path')); + const pathLabel = element.appendChild($('div.path-label')); + + const pathInput = new InputBox(element, this.contextViewService); + + const disposables = new DisposableStore(); + disposables.add(attachInputBoxStyler(pathInput, this.themeService)); + + const renderDisposables = disposables.add(new DisposableStore()); + + return { + element, + pathLabel, + pathInput, + disposables, + renderDisposables + }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: ITrustedUriPathColumnTemplateData, height: number | undefined): void { + templateData.renderDisposables.clear(); + + templateData.renderDisposables.add(this.table.onEdit(async (e) => { + if (item === e) { + templateData.element.classList.add('input-mode'); + templateData.pathInput.focus(); + templateData.pathInput.select(); + templateData.element.parentElement!.style.paddingLeft = '0px'; + } + })); + + // stop double click action from re-rendering the element on the table #125052 + templateData.renderDisposables.add(addDisposableListener(templateData.pathInput.element, EventType.DBLCLICK, e => { + EventHelper.stop(e); + })); + + + const hideInputBox = () => { + templateData.element.classList.remove('input-mode'); + templateData.element.parentElement!.style.paddingLeft = '5px'; + }; + + const accept = () => { + hideInputBox(); + const uri = item.uri.with({ path: templateData.pathInput.value }); + templateData.pathLabel.innerText = templateData.pathInput.value; + + if (uri) { + this.table.acceptEdit(item, uri); + } + }; + + const reject = () => { + hideInputBox(); + templateData.pathInput.value = stringValue; + this.table.rejectEdit(item); + }; + + templateData.renderDisposables.add(addStandardDisposableListener(templateData.pathInput.inputElement, EventType.KEY_DOWN, e => { + let handled = false; + if (e.equals(KeyCode.Enter)) { + accept(); + handled = true; + } else if (e.equals(KeyCode.Escape)) { + reject(); + handled = true; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + })); + templateData.renderDisposables.add((addDisposableListener(templateData.pathInput.inputElement, EventType.BLUR, () => { + reject(); + }))); + + const stringValue = item.uri.scheme === Schemas.file ? URI.revive(item.uri).fsPath : item.uri.path; + templateData.pathInput.value = stringValue; + templateData.pathLabel.innerText = stringValue; + templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); + + templateData.pathLabel.style.display = item.entryType === TrustedUriItemType.Add ? 'none' : ''; + } + + disposeTemplate(templateData: ITrustedUriPathColumnTemplateData): void { + templateData.disposables.dispose(); + templateData.renderDisposables.dispose(); + } + +} + + +interface ITrustedUriHostColumnTemplateData { + element: HTMLElement; + hostContainer: HTMLElement; + buttonBarContainer: HTMLElement; + disposables: DisposableStore; + renderDisposables: DisposableStore; +} + +class TrustedUriHostColumnRenderer implements ITableRenderer { + static readonly TEMPLATE_ID = 'host'; + + readonly templateId: string = TrustedUriHostColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly table: WorkspaceTrustedUrisTable, + @ILabelService private readonly labelService: ILabelService, + @IThemeService private readonly themeService: IThemeService, + ) { } + + renderTemplate(container: HTMLElement): ITrustedUriHostColumnTemplateData { + const disposables = new DisposableStore(); + const renderDisposables = disposables.add(new DisposableStore()); + + const element = container.appendChild($('.host')); + const hostContainer = element.appendChild($('div.host-label')); + const buttonBarContainer = element.appendChild($('div.button-bar')); + + return { + element, + hostContainer, + buttonBarContainer, + disposables, + renderDisposables + }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: ITrustedUriHostColumnTemplateData, height: number | undefined): void { + templateData.renderDisposables.clear(); + templateData.renderDisposables.add({ dispose: () => { clearNode(templateData.buttonBarContainer); } }); + + templateData.hostContainer.innerText = item.uri.authority ? this.labelService.getHostLabel(item.uri.scheme, item.uri.authority) : localize('localAuthority', "Local"); + templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); + + if (item.entryType === TrustedUriItemType.Add) { + templateData.hostContainer.style.display = 'none'; + templateData.buttonBarContainer.style.display = ''; + + const buttonBar = templateData.renderDisposables.add(new ButtonBar(templateData.buttonBarContainer)); + const addButton = templateData.renderDisposables.add(buttonBar.addButton({ title: localize('addButton', "Add Folder") })); + addButton.label = localize('addButton', "Add Folder"); + + templateData.renderDisposables.add(attachButtonStyler(addButton, this.themeService)); + + templateData.renderDisposables.add(addButton.onDidClick(() => { + this.table.edit(item); + })); + + templateData.renderDisposables.add(this.table.onEdit(e => { + if (item === e) { + templateData.hostContainer.style.display = ''; + templateData.buttonBarContainer.style.display = 'none'; + } + })); + + templateData.renderDisposables.add(this.table.onDidRejectEdit(e => { + if (item === e) { + templateData.hostContainer.style.display = 'none'; + templateData.buttonBarContainer.style.display = ''; + } + })); + } else { + templateData.hostContainer.style.display = ''; + templateData.buttonBarContainer.style.display = 'none'; + } + } + + disposeTemplate(templateData: ITrustedUriHostColumnTemplateData): void { + templateData.disposables.dispose(); + } + +} + export class WorkspaceTrustEditor extends EditorPane { static readonly ID: string = 'workbench.editor.workspaceTrust'; private rootElement!: HTMLElement; @@ -71,9 +545,7 @@ export class WorkspaceTrustEditor extends EditorPane { // Settings Section private configurationContainer!: HTMLElement; - private trustSettingsTree!: WorkspaceTrustTree; - private workspaceTrustSettingsTreeModel!: WorkspaceTrustTreeModel; - + private workpaceTrustedUrisTable!: WorkspaceTrustedUrisTable; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -84,20 +556,20 @@ export class WorkspaceTrustEditor extends EditorPane { @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IDialogService private readonly dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { - this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '-1' })); + this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '0' })); + this.rootElement.style.visibility = 'hidden'; this.createHeaderElement(this.rootElement); const scrollableContent = $('.workspace-trust-editor-body'); this.bodyScrollBar = this._register(new DomScrollableElement(scrollableContent, { horizontal: ScrollbarVisibility.Hidden, - vertical: ScrollbarVisibility.Visible, + vertical: ScrollbarVisibility.Auto, })); append(this.rootElement, this.bodyScrollBar.getDomNode()); @@ -105,26 +577,24 @@ export class WorkspaceTrustEditor extends EditorPane { this.createAffectedFeaturesElement(scrollableContent); this.createConfigurationElement(scrollableContent); - this._register(attachStylerCallback(this.themeService, { ACTIVITY_BAR_BADGE_BACKGROUND, trustedForegroundColor, untrustedForegroundColor }, colors => { - this.rootElement.style.setProperty('--workspace-trust-trusted-color', colors.trustedForegroundColor?.toString() || ''); - this.rootElement.style.setProperty('--workspace-trust-untrusted-color', colors.untrustedForegroundColor?.toString() || ''); - this.rootElement.style.setProperty('--workspace-trust-selected-state-color', colors.ACTIVITY_BAR_BADGE_BACKGROUND?.toString() || ''); + this._register(attachStylerCallback(this.themeService, { debugIconStartForeground, editorErrorForeground, buttonBackground, buttonSecondaryBackground }, colors => { + this.rootElement.style.setProperty('--workspace-trust-selected-color', colors.buttonBackground?.toString() || ''); + this.rootElement.style.setProperty('--workspace-trust-unselected-color', colors.buttonSecondaryBackground?.toString() || ''); + this.rootElement.style.setProperty('--workspace-trust-check-color', colors.debugIconStartForeground?.toString() || ''); + this.rootElement.style.setProperty('--workspace-trust-x-color', colors.editorErrorForeground?.toString() || ''); })); + } - this._register(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.3)); - collector.addRule(`.workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { border: 1px solid ${fgWithOpacity}; margin: 0px 4px; display: flex; flex-direction: column; padding: 10px 40px;}`); - } - })); + override focus() { + this.rootElement.focus(); } - override async setInput(input: WorkspaceTrustEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: WorkspaceTrustEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); if (token.isCancellationRequested) { return; } + await this.workspaceTrustManagementService.workspaceTrustInitialized; this.registerListeners(); this.render(); } @@ -144,17 +614,23 @@ export class WorkspaceTrustEditor extends EditorPane { return 'workspace-trust-header workspace-trust-untrusted'; } - private useWorkspaceLanguage(): boolean { - return !isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())); - } - private getHeaderTitleText(trusted: boolean): string { - if (trusted) { - return this.useWorkspaceLanguage() ? localize('trustedHeaderWorkspace', "You trust this workspace") : localize('trustedHeaderFolder', "You trust this folder"); + if (this.workspaceTrustManagementService.isWorkspaceTrustForced()) { + return localize('trustedUnsettableWindow', "This window is trusted"); + } + + switch (this.workspaceService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('trustedHeaderWindow', "You trust this window"); + case WorkbenchState.FOLDER: + return localize('trustedHeaderFolder', "You trust this folder"); + case WorkbenchState.WORKSPACE: + return localize('trustedHeaderWorkspace', "You trust this workspace"); + } } - return this.useWorkspaceLanguage() ? localize('untrustedHeaderWorkspace', "You are in restricted mode") : localize('untrustedHeaderFolder', "You are in Restricted Mode"); + return localize('untrustedHeader', "You are in Restricted Mode"); } private getHeaderDescriptionText(trusted: boolean): string { @@ -169,6 +645,34 @@ export class WorkspaceTrustEditor extends EditorPane { return shieldIcon.classNamesArray; } + private getFeaturesHeaderText(trusted: boolean): [string, string] { + let title: string = ''; + let subTitle: string = ''; + + switch (this.workspaceService.getWorkbenchState()) { + case WorkbenchState.EMPTY: { + title = trusted ? localize('trustedWindow', "In a trusted window") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedWindowSubtitle', "You trust the authors of the files in the current window. All features are enabled:") : + localize('untrustedWindowSubtitle', "You do not trust the authors of the files in the current window. The following features are disabled:"); + break; + } + case WorkbenchState.FOLDER: { + title = trusted ? localize('trustedFolder', "In a trusted folder") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedFolderSubtitle', "You trust the authors of the files in the current folder. All features are enabled:") : + localize('untrustedFolderSubtitle', "You do not trust the authors of the files in the current folder. The following features are disabled:"); + break; + } + case WorkbenchState.WORKSPACE: { + title = trusted ? localize('trustedWorkspace', "In a trusted workspace") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedWorkspaceSubtitle', "You trust the authors of the files in the current workspace. All features are enabled:") : + localize('untrustedWorkspaceSubtitle', "You do not trust the authors of the files in the current workspace. The following features are disabled:"); + break; + } + } + + return [title, subTitle]; + } + private rendering = false; private rerenderDisposables: DisposableStore = this._register(new DisposableStore()); @debounce(100) @@ -196,17 +700,43 @@ export class WorkspaceTrustEditor extends EditorPane { if (typeof node === 'string') { append(p, document.createTextNode(node)); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(p, link.el); this.rerenderDisposables.add(link); - this.rerenderDisposables.add(attachLinkStyler(link, this.themeService)); } } this.headerContainer.className = this.getHeaderContainerClass(isWorkspaceTrusted); + this.rootElement.setAttribute('aria-label', `${localize('root element label', "Manage Workspace Trust")}: ${this.headerContainer.innerText}`); // Settings - const settingsRequiringTrustedWorkspaceCount = filterSettingsRequireWorkspaceTrust(this.configurationService.restrictedSettings.default).length; + const restrictedSettings = this.configurationService.restrictedSettings; + const configurationRegistry = Registry.as(Extensions.Configuration); + const settingsRequiringTrustedWorkspaceCount = restrictedSettings.default.filter(key => { + const property = configurationRegistry.getConfigurationProperties()[key]; + + // cannot be configured in workspace + if (property.scope === ConfigurationScope.APPLICATION || property.scope === ConfigurationScope.MACHINE) { + return false; + } + + // If deprecated include only those configured in the workspace + if (property.deprecationMessage || property.markdownDeprecationMessage) { + if (restrictedSettings.workspace?.includes(key)) { + return true; + } + if (restrictedSettings.workspaceFolder) { + for (const workspaceFolderSettings of restrictedSettings.workspaceFolder.values()) { + if (workspaceFolderSettings.includes(key)) { + return true; + } + } + } + return false; + } + + return true; + }).length; // Features List const installedExtensions = await this.instantiationService.invokeFunction(getInstalledExtensions); @@ -216,19 +746,22 @@ export class WorkspaceTrustEditor extends EditorPane { this.renderAffectedFeatures(settingsRequiringTrustedWorkspaceCount, onDemandExtensionCount + onStartExtensionCount); // Configuration Tree - this.workspaceTrustSettingsTreeModel.update(this.workspaceTrustManagementService.getTrustedFolders()); - this.trustSettingsTree.setChildren(null, Iterable.map(this.workspaceTrustSettingsTreeModel.settings, s => { return { element: s }; })); + this.workpaceTrustedUrisTable.updateTable(); this.bodyScrollBar.getDomNode().style.height = `calc(100% - ${this.headerContainer.clientHeight}px)`; this.bodyScrollBar.scanDomNode(); + this.rootElement.style.visibility = ''; this.rendering = false; } private getExtensionCountByUntrustedWorkspaceSupport(extensions: IExtensionStatus[], trustRequestType: ExtensionUntrustedWorkpaceSupportType): number { const filtered = extensions.filter(ext => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(ext.local.manifest) === trustRequestType); const set = new Set(); + const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace()); for (const ext of filtered) { - set.add(ext.identifier.id); + if (!inVirtualWorkspace || this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(ext.local.manifest) !== false) { + set.add(ext.identifier.id); + } } return set.size; @@ -243,85 +776,74 @@ export class WorkspaceTrustEditor extends EditorPane { } private createConfigurationElement(parent: HTMLElement): void { - this.configurationContainer = append(parent, $('.workspace-trust-settings.settings-editor')); + this.configurationContainer = append(parent, $('.workspace-trust-settings')); + const configurationTitle = append(this.configurationContainer, $('.workspace-trusted-folders-title')); + configurationTitle.innerText = localize('trustedFoldersAndWorkspaces', "Trusted Folders & Workspaces"); - const settingsBody = append(this.configurationContainer, $('.workspace-trust-settings-body.settings-body')); + const configurationDescription = append(this.configurationContainer, $('.workspace-trusted-folders-description')); + configurationDescription.innerText = localize('trustedFoldersDescription', "You trust the following folders, their children, and workspace files."); - const workspaceTrustTreeContainer = append(settingsBody, $('.workspace-trust-settings-tree-container.settings-tree-container')); - const renderer = this.instantiationService.createInstance(WorkspaceTrustSettingArrayRenderer,); + this.workpaceTrustedUrisTable = this._register(this.instantiationService.createInstance(WorkspaceTrustedUrisTable, this.configurationContainer)); - this.trustSettingsTree = this._register(this.instantiationService.createInstance(WorkspaceTrustTree, - workspaceTrustTreeContainer, - [renderer])); - this.workspaceTrustSettingsTreeModel = this.instantiationService.createInstance(WorkspaceTrustTreeModel); - - this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e))); } private createAffectedFeaturesElement(parent: HTMLElement): void { this.affectedFeaturesContainer = append(parent, $('.workspace-trust-features')); } - private renderAffectedFeatures(numSettings: number, numExtensions: number): void { + private async renderAffectedFeatures(numSettings: number, numExtensions: number): Promise { clearNode(this.affectedFeaturesContainer); - const trustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.trusted')); - this.renderLimitationsHeaderElement(trustedContainer, - this.useWorkspaceLanguage() ? localize('trustedWorkspace', "In a trusted workspace") : localize('trustedFolder', "In a Trusted Folder"), - this.useWorkspaceLanguage() ? localize('trustedWorkspaceSubtitle', "You trust the authors of the files in the current workspace. All features are enabled:") : localize('trustedFolderSubtitle', "You trust the authors of the files in the current folder. All features are enabled:")); - this.renderLimitationsListElement(trustedContainer, [ - localize('trustedTasks', "Tasks will be allowed to run"), - localize('trustedDebugging', "Debugging will be enabled"), - localize('trustedSettings', "All workspace settings will be applied"), - localize('trustedExtensions', "All extensions will be enabled") - ], checkListIcon.classNamesArray); + // Trusted features + const trustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.trusted')); + const [trustedTitle, trustedSubTitle] = this.getFeaturesHeaderText(true); + + this.renderLimitationsHeaderElement(trustedContainer, trustedTitle, trustedSubTitle); + const trustedContainerItems = this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? + [ + localize('trustedTasks', "Tasks are allowed to run"), + localize('trustedDebugging', "Debugging is enabled"), + localize('trustedExtensions', "All extensions are enabled") + ] : + [ + localize('trustedTasks', "Tasks are allowed to run"), + localize('trustedDebugging', "Debugging is enabled"), + localize('trustedSettings', "All workspace settings are applied"), + localize('trustedExtensions', "All extensions are enabled") + ]; + this.renderLimitationsListElement(trustedContainer, trustedContainerItems, checkListIcon.classNamesArray); + + // Restricted Mode features const untrustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.untrusted')); - this.renderLimitationsHeaderElement(untrustedContainer, - localize('untrustedWorkspace', "In Restricted Mode"), - this.useWorkspaceLanguage() ? localize('untrustedWorkspaceSubtitle', "You do not trust the authors of the files in the current workspace. The following features are disabled:") : localize('untrustedFolderSubtitle', "You do not trust the authors of the files in the current folder. The following features are disabled:")); - - this.renderLimitationsListElement(untrustedContainer, [ - localize('untrustedTasks', "Tasks will be disabled"), - localize('untrustedDebugging', "Debugging will be disabled"), - numSettings ? localize('untrustedSettings', "[{0} workspace settings](command:{1}) will not be applied", numSettings, 'settings.filterUntrusted') : localize('no untrustedSettings', "Workspace settings requiring trust will not be applied"), - localize('untrustedExtensions', "[{0} extensions](command:{1}) will be disabled or have limited functionality", numExtensions, 'workbench.extensions.action.listTrustRequiredExtensions') - ], xListIcon.classNamesArray); - - if (!this.workspaceTrustManagementService.isWorkpaceTrusted()) { - this.addTrustButtonToElement(trustedContainer); - } - - if (this.isTrustedExplicitlyOnly()) { - this.addDontTrustButtonToElement(untrustedContainer); + const [untrustedTitle, untrustedSubTitle] = this.getFeaturesHeaderText(false); + + this.renderLimitationsHeaderElement(untrustedContainer, untrustedTitle, untrustedSubTitle); + const untrustedContainerItems = this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? + [ + localize('untrustedTasks', "Tasks are disabled"), + localize('untrustedDebugging', "Debugging is disabled"), + localize('untrustedExtensions', "[{0} extensions]({1}) are disabled or have limited functionality", numExtensions, `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`) + ] : + [ + localize('untrustedTasks', "Tasks are disabled"), + localize('untrustedDebugging', "Debugging is disabled"), + numSettings ? localize('untrustedSettings', "[{0} workspace settings]({1}) are not applied", numSettings, 'command:settings.filterUntrusted') : localize('no untrustedSettings', "Workspace settings requiring trust are not applied"), + localize('untrustedExtensions', "[{0} extensions]({1}) are disabled or have limited functionality", numExtensions, `command:${LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID}`) + ]; + this.renderLimitationsListElement(untrustedContainer, untrustedContainerItems, xListIcon.classNamesArray); + + if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { + this.addDontTrustButtonToElement(untrustedContainer); + } else { + this.addTrustedTextToElement(untrustedContainer); + } } else { - this.addTrustedTextToElement(untrustedContainer); - } - } - - private isTrustedExplicitlyOnly(): boolean { - // Can only be trusted explicitly in the single folder scenario - const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); - if (!(isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file)) { - return false; - } - - // If the current folder isn't trusted directly, return false - const trustInfo = this.workspaceTrustManagementService.getFolderTrustInfo(workspaceIdentifier.uri); - if (!trustInfo.trusted || !isEqual(workspaceIdentifier.uri, trustInfo.uri)) { - return false; - } - - // Check if the parent is also trusted - if (this.workspaceTrustManagementService.canSetParentFolderTrust()) { - const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); - const parentIsTrusted = this.workspaceTrustManagementService.getFolderTrustInfo(URI.file(parentPath)).trusted; - if (parentIsTrusted) { - return false; + if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { + this.addTrustButtonToElement(trustedContainer); } } - - return true; } private createButton(parent: HTMLElement, action: Action, enabled?: boolean): void { @@ -353,58 +875,56 @@ export class WorkspaceTrustEditor extends EditorPane { } private addTrustButtonToElement(parent: HTMLElement): void { - if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { - - const trustUris = async (uris?: URI[]) => { - if (!uris) { - this.workspaceTrustManagementService.setWorkspaceTrust(true); - } else { - this.workspaceTrustManagementService.setFoldersTrust(uris, true); - } - }; - - const trustChoiceWithMenu: IPromptChoiceWithMenu = { - isSecondary: false, - label: localize('trustButton', "Trust"), - menu: [], - run: () => { - trustUris(); - } - }; + const trustUris = async (uris?: URI[]) => { + if (!uris) { + await this.workspaceTrustManagementService.setWorkspaceTrust(true); + } else { + await this.workspaceTrustManagementService.setUrisTrust(uris, true); + } + }; - const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); - if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { - const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); - if (parentPath) { - trustChoiceWithMenu.menu.push({ - label: localize('trustParentButton', "Trust All in Parent Folder"), - run: () => { - trustUris([URI.file(parentPath)]); - } - }); - } + const trustChoiceWithMenu: IPromptChoiceWithMenu = { + isSecondary: false, + label: localize('trustButton', "Trust"), + menu: [], + run: () => { + trustUris(); } + }; - const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); - this.createButton(parent, new ChoiceAction('workspace.trust.button.action', trustChoiceWithMenu), !isWorkspaceTrusted); + const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + if (parentPath) { + trustChoiceWithMenu.menu.push({ + label: localize('trustParentButton', "Trust All in Parent Folder"), + run: () => { + trustUris([URI.file(parentPath)]); + } + }); + } } + + const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); + this.createButton(parent, new ChoiceAction('workspace.trust.button.action', trustChoiceWithMenu), !isWorkspaceTrusted); } private addDontTrustButtonToElement(parent: HTMLElement): void { - if (this.workspaceTrustManagementService.canSetWorkspaceTrust() && this.isTrustedExplicitlyOnly()) { - this.createButton(parent, new Action('workspace.trust.button.action.deny', localize('dontTrustButton', "Don't Trust"), undefined, true, async () => { - await this.workspaceTrustManagementService.setWorkspaceTrust(false); - })); - } + this.createButton(parent, new Action('workspace.trust.button.action.deny', localize('dontTrustButton', "Don't Trust"), undefined, true, async () => { + await this.workspaceTrustManagementService.setWorkspaceTrust(false); + })); } private addTrustedTextToElement(parent: HTMLElement): void { - const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); - const canSetWorkspaceTrust = this.workspaceTrustManagementService.canSetWorkspaceTrust(); + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return; + } - if (canSetWorkspaceTrust && isWorkspaceTrusted) { - const textElement = append(parent, $('.workspace-trust-untrusted-description')); - textElement.innerText = this.useWorkspaceLanguage() ? localize('untrustedWorkspaceReason', "This workspace is trusted via one or more of the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via one or more of the trusted folders below."); + const textElement = append(parent, $('.workspace-trust-untrusted-description')); + if (!this.workspaceTrustManagementService.isWorkspaceTrustForced()) { + textElement.innerText = this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the the trusted folders below."); + } else { + textElement.innerText = localize('trustedForcedReason', "This window is trusted by nature of the workspace that is opened."); } } @@ -433,46 +953,21 @@ export class WorkspaceTrustEditor extends EditorPane { if (typeof node === 'string') { append(text, document.createTextNode(node)); } else { - const link = this.instantiationService.createInstance(Link, node); + const link = this.instantiationService.createInstance(Link, node, {}); append(text, link.el); this.rerenderDisposables.add(link); - this.rerenderDisposables.add(attachLinkStyler(link, this.themeService)); } } } } - private onDidChangeSetting(change: IWorkspaceTrustSettingChangeEvent) { - const applyChangesWithPrompt = async (showPrompt: boolean, applyChanges: () => void) => { - if (showPrompt) { - const message = localize('workspaceTrustSettingModificationMessage', "Update Workspace Trust Settings"); - const detail = localize('workspaceTrustTransitionDetail', "In order to safely complete this action, all affected windows will have to be reloaded. Are you sure you want to proceed with this action?"); - const primaryButton = localize('workspaceTrustTransitionPrimaryButton', "Yes"); - const secondaryButton = localize('workspaceTrustTransitionSecondaryButton', "No"); - - const result = await this.dialogService.show(Severity.Info, message, [primaryButton, secondaryButton], { cancelId: 1, detail, custom: { icon: Codicon.shield } }); - if (result.choice !== 0) { - return; - } - } - - applyChanges(); - }; - - if (isArray(change.value)) { - if (change.key === 'trustedFolders') { - applyChangesWithPrompt(false, () => this.workspaceTrustManagementService.setTrustedFolders(change.value!)); - } - } - } - private layoutParticipants: { layout: () => void; }[] = []; layout(dimension: Dimension): void { if (!this.isVisible()) { return; } - this.trustSettingsTree.layout(dimension.height, dimension.width); + this.workpaceTrustedUrisTable.layout(); this.layoutParticipants.forEach(participant => { participant.layout(); diff --git a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts b/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts deleted file mode 100644 index 94f24228f04e..000000000000 --- a/lib/vscode/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts +++ /dev/null @@ -1,624 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { addDisposableListener, append, EventType, $, createStyleSheet, trackFocus, addStandardDisposableListener } from 'vs/base/browser/dom'; -import { DefaultStyleController, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; -import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; -import { ITreeModel, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; -import { Color, RGBA } from 'vs/base/common/color'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { isArray } from 'vs/base/common/types'; -import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { NonCollapsibleObjectTreeModel } from 'vs/workbench/contrib/preferences/browser/settingsTree'; -import { AbstractListSettingWidget, focusedRowBackground, focusedRowBorder, ISettingListChangeEvent, rowHoverBackground, settingsHeaderForeground, settingsSelectBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; -import { attachButtonStyler, attachInputBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; -import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IAction } from 'vs/base/common/actions'; -import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { disposableTimeout } from 'vs/base/common/async'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { ILabelService } from 'vs/platform/label/common/label'; - - -export class WorkspaceTrustSettingsTreeEntry { - id: string; - displayLabel: string; - setting: { - key: string; - description: string; - }; - value: URI[]; - - constructor(key: string, displayLabel: string, description: string, value: URI[]) { - this.setting = { key, description }; - this.displayLabel = displayLabel; - this.value = value; - this.id = key; - } -} - -export interface IWorkspaceTrustSettingItemTemplate { - onChange?: (value: T, type: WorkspaceTrustSettingListItemChangeType) => void; - - toDispose: DisposableStore; - context?: WorkspaceTrustSettingsTreeEntry; - containerElement: HTMLElement; - labelElement: HTMLElement; - descriptionElement: HTMLElement; - controlElement: HTMLElement; - elementDisposables: DisposableStore; -} - -export interface IWorkspaceTrustUriDataItem extends UriComponents { } - -class WorkspaceTrustFolderSettingWidget extends AbstractListSettingWidget { - constructor( - container: HTMLElement, - @ILabelService protected readonly labelService: ILabelService, - @IThemeService themeService: IThemeService, - @IContextViewService contextViewService: IContextViewService - ) { - super(container, themeService, contextViewService); - } - - protected getEmptyItem(): IWorkspaceTrustUriDataItem { - return URI.file(''); - } - - protected getContainerClasses() { - return ['workspace-trust-uri-setting-widget', 'setting-list-object-widget']; - } - - protected getActionsForItem(item: IWorkspaceTrustUriDataItem, idx: number): IAction[] { - return [ - { - class: ThemeIcon.asClassName(settingsEditIcon), - enabled: true, - id: 'workbench.action.editListItem', - tooltip: this.getLocalizedStrings().editActionTooltip, - run: () => this.editSetting(idx) - }, - { - class: ThemeIcon.asClassName(settingsRemoveIcon), - enabled: true, - id: 'workbench.action.removeListItem', - tooltip: this.getLocalizedStrings().deleteActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } - ] as IAction[]; - } - - protected override renderHeader() { - const header = $('.setting-list-row-header'); - const hostHeader = append(header, $('.setting-list-object-key')); - const pathHeader = append(header, $('.setting-list-object-value')); - const { hostHeaderText, pathHeaderText } = this.getLocalizedStrings(); - - hostHeader.textContent = hostHeaderText; - pathHeader.textContent = pathHeaderText; - - return header; - } - - protected renderItem(item: IWorkspaceTrustUriDataItem): HTMLElement { - const rowElement = $('.setting-list-row'); - rowElement.classList.add('setting-list-object-row'); - - const hostElement = append(rowElement, $('.setting-list-object-key')); - const pathElement = append(rowElement, $('.setting-list-object-value')); - - hostElement.textContent = item.authority ? this.labelService.getHostLabel(item.scheme, item.authority) : localize('localAuthority', "Local"); - pathElement.textContent = item.scheme === Schemas.file ? URI.revive(item).fsPath : item.path; - - return rowElement; - } - - - protected renderEdit(item: IWorkspaceTrustUriDataItem, idx: number): HTMLElement { - const rowElement = $('.setting-list-edit-row'); - - const hostElement = append(rowElement, $('.setting-list-object-key')); - hostElement.textContent = item.authority ? this.labelService.getHostLabel(item.scheme, item.authority) : localize('localAuthority', "Local"); - - const updatedItem = () => { - if (item.scheme === Schemas.file) { - return URI.file(pathInput.value); - } else { - return URI.revive(item).with({ path: pathInput.value }); - } - }; - - const onKeyDown = (e: StandardKeyboardEvent) => { - if (e.equals(KeyCode.Enter)) { - this.handleItemChange(item, updatedItem(), idx); - } else if (e.equals(KeyCode.Escape)) { - this.cancelEdit(); - e.preventDefault(); - } - rowElement?.focus(); - }; - - const pathInput = new InputBox(rowElement, this.contextViewService, { - placeholder: this.getLocalizedStrings().inputPlaceholder - }); - - pathInput.element.classList.add('setting-list-valueInput'); - this.listDisposables.add(attachInputBoxStyler(pathInput, this.themeService, { - inputBackground: settingsSelectBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); - this.listDisposables.add(pathInput); - pathInput.value = item.scheme === Schemas.file ? URI.revive(item).fsPath : item.path; - - this.listDisposables.add( - addStandardDisposableListener(pathInput.inputElement, EventType.KEY_DOWN, onKeyDown) - ); - - const okButton = this._register(new Button(rowElement)); - okButton.label = localize('okButton', "OK"); - okButton.element.classList.add('setting-list-ok-button'); - - this.listDisposables.add(attachButtonStyler(okButton, this.themeService)); - this.listDisposables.add(okButton.onDidClick(() => this.handleItemChange(item, updatedItem(), idx))); - - const cancelButton = this._register(new Button(rowElement)); - cancelButton.label = localize('cancelButton', "Cancel"); - cancelButton.element.classList.add('setting-list-cancel-button'); - - this.listDisposables.add(attachButtonStyler(cancelButton, this.themeService)); - this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit())); - - this.listDisposables.add( - disposableTimeout(() => { - pathInput.focus(); - pathInput.select(); - }) - ); - - return rowElement; - } - - protected isItemNew(item: IWorkspaceTrustUriDataItem): boolean { - return item.path === ''; - } - - protected getLocalizedRowTitle(item: IWorkspaceTrustUriDataItem): string { - return localize('trustedRow', "Trusted Path: {0}", this.labelService.getUriLabel(URI.from(item))); - } - - protected getLocalizedStrings() { - return { - deleteActionTooltip: localize('removePath', "Remove Path"), - editActionTooltip: localize('editPath', "Edit Path"), - addButtonLabel: localize('addPath', "Add Path"), - hostHeaderText: localize('hostHeaderText', "Host"), - pathHeaderText: localize('pathHeaderText', "Path"), - inputPlaceholder: localize('pathInputPlaceholder', "Path Item..."), - }; - } -} - -interface IWorkspaceTrustSettingListItemTemplate extends IWorkspaceTrustSettingItemTemplate { - listWidget: WorkspaceTrustFolderSettingWidget; - validationErrorMessageElement: HTMLElement; -} - -export type WorkspaceTrustSettingListItemChangeType = 'added' | 'removed' | 'changed'; -export interface IWorkspaceTrustSettingChangeEvent { - key: string; - value: URI[] | undefined; // undefined => reset/unconfigure - type: WorkspaceTrustSettingListItemChangeType; -} - - -export class WorkspaceTrustSettingArrayRenderer extends Disposable implements ITreeRenderer { - templateId = 'template.setting.array'; - - static readonly CONTROL_CLASS = 'setting-control-focus-target'; - static readonly CONTROL_SELECTOR = '.' + WorkspaceTrustSettingArrayRenderer.CONTROL_CLASS; - static readonly CONTENTS_CLASS = 'setting-item-contents'; - static readonly CONTENTS_SELECTOR = '.' + WorkspaceTrustSettingArrayRenderer.CONTENTS_CLASS; - static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; - - static readonly SETTING_KEY_ATTR = 'data-key'; - static readonly SETTING_ID_ATTR = 'data-id'; - static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; - - protected readonly _onDidChangeSetting = this._register(new Emitter()); - readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; - - private readonly _onDidFocusSetting = this._register(new Emitter()); - readonly onDidFocusSetting: Event = this._onDidFocusSetting.event; - - private readonly _onDidChangeIgnoredSettings = this._register(new Emitter()); - readonly onDidChangeIgnoredSettings: Event = this._onDidChangeIgnoredSettings.event; - - constructor( - @IThemeService protected readonly _themeService: IThemeService, - @IContextViewService protected readonly _contextViewService: IContextViewService, - @IOpenerService protected readonly _openerService: IOpenerService, - @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @ICommandService protected readonly _commandService: ICommandService, - @IContextMenuService protected readonly _contextMenuService: IContextMenuService, - @IKeybindingService protected readonly _keybindingService: IKeybindingService, - @IConfigurationService protected readonly _configService: IConfigurationService, - ) { - super(); - } - - renderCommonTemplate(tree: any, _container: HTMLElement, typeClass: string): IWorkspaceTrustSettingItemTemplate { - _container.classList.add('setting-item'); - _container.classList.add('setting-item-' + typeClass); - - const container = append(_container, $(WorkspaceTrustSettingArrayRenderer.CONTENTS_SELECTOR)); - container.classList.add('settings-row-inner-container'); - const titleElement = append(container, $('.setting-item-title')); - const labelCategoryContainer = append(titleElement, $('.setting-item-cat-label-container')); - const labelElement = append(labelCategoryContainer, $('span.setting-item-label')); - const descriptionElement = append(container, $('.setting-item-description')); - const modifiedIndicatorElement = append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "Modified"); - - const valueElement = append(container, $('.setting-item-value')); - const controlElement = append(valueElement, $('div.setting-item-control')); - const toDispose = new DisposableStore(); - - const template: IWorkspaceTrustSettingItemTemplate = { - toDispose, - elementDisposables: new DisposableStore(), - containerElement: container, - labelElement, - descriptionElement, - controlElement - }; - - // Prevent clicks from being handled by list - toDispose.add(addDisposableListener(controlElement, EventType.MOUSE_DOWN, e => e.stopPropagation())); - - toDispose.add(addDisposableListener(titleElement, EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); - toDispose.add(addDisposableListener(titleElement, EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); - - return template; - } - - addSettingElementFocusHandler(template: IWorkspaceTrustSettingItemTemplate): void { - const focusTracker = trackFocus(template.containerElement); - template.toDispose.add(focusTracker); - focusTracker.onDidBlur(() => { - if (template.containerElement.classList.contains('focused')) { - template.containerElement.classList.remove('focused'); - } - }); - - focusTracker.onDidFocus(() => { - template.containerElement.classList.add('focused'); - - if (template.context) { - this._onDidFocusSetting.fire(template.context); - } - }); - } - - renderTemplate(container: HTMLElement): IWorkspaceTrustSettingListItemTemplate { - const common = this.renderCommonTemplate(null, container, 'list'); - const descriptionElement = common.containerElement.querySelector('.setting-item-description')!; - const validationErrorMessageElement = $('.setting-item-validation-message'); - descriptionElement.after(validationErrorMessageElement); - - const listWidget = this._instantiationService.createInstance(WorkspaceTrustFolderSettingWidget, common.controlElement); - listWidget.domNode.classList.add(WorkspaceTrustSettingArrayRenderer.CONTROL_CLASS); - common.toDispose.add(listWidget); - - const template: IWorkspaceTrustSettingListItemTemplate = { - ...common, - listWidget, - validationErrorMessageElement - }; - - this.addSettingElementFocusHandler(template); - - common.toDispose.add( - listWidget.onDidChangeList(e => { - const { list: newList, changeType } = this.computeNewList(template, e); - if (newList !== null && template.onChange) { - template.onChange(newList, changeType); - } - }) - ); - - return template; - } - - private computeNewList(template: IWorkspaceTrustSettingListItemTemplate, e: ISettingListChangeEvent): { list: URI[] | null, changeType: WorkspaceTrustSettingListItemChangeType } { - if (template.context) { - let newValue: URI[] = []; - - let changeType: WorkspaceTrustSettingListItemChangeType = 'changed'; - if (isArray(template.context.value)) { - newValue = [...template.context.value]; - } - - if (e.targetIndex !== undefined) { - // Delete value - if (!e.item?.path && e.originalItem.path && e.targetIndex > -1) { - newValue.splice(e.targetIndex, 1); - changeType = 'removed'; - } - // Update value - else if (e.item?.path && e.originalItem.path) { - if (e.targetIndex > -1) { - newValue[e.targetIndex] = URI.revive(e.item); - changeType = e.targetIndex < template.context.value.length ? 'changed' : 'added'; - } - // For some reason, we are updating and cannot find original value - // Just append the value in this case - else { - newValue.push(URI.revive(e.item)); - changeType = 'added'; - } - } - // Add value - else if (e.item?.path && !e.originalItem.path && e.targetIndex >= newValue.length) { - newValue.push(URI.revive(e.item)); - changeType = 'added'; - } - } - - return { list: newValue, changeType }; - } - - return { list: null, changeType: 'changed' }; - } - - renderElement(node: ITreeNode, index: number, template: IWorkspaceTrustSettingListItemTemplate): void { - const element = node.element; - template.context = element; - - template.containerElement.setAttribute(WorkspaceTrustSettingArrayRenderer.SETTING_KEY_ATTR, element.setting.key); - template.containerElement.setAttribute(WorkspaceTrustSettingArrayRenderer.SETTING_ID_ATTR, element.id); - - template.labelElement.textContent = element.displayLabel; - - template.descriptionElement.innerText = element.setting.description; - - const onChange = (value: any, type: WorkspaceTrustSettingListItemChangeType) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type }); - this.renderValue(element, template, onChange); - } - - protected renderValue(dataElement: WorkspaceTrustSettingsTreeEntry, template: IWorkspaceTrustSettingListItemTemplate, onChange: (value: URI[] | undefined, type: WorkspaceTrustSettingListItemChangeType) => void): void { - const value = getListDisplayValue(dataElement); - template.listWidget.setValue(value); - template.context = dataElement; - - template.onChange = (v, t) => { - onChange(v, t); - renderArrayValidations(dataElement, template, v, false); - }; - - renderArrayValidations(dataElement, template, value.map(v => URI.revive(v)), true); - } - - disposeTemplate(template: IWorkspaceTrustSettingItemTemplate): void { - dispose(template.toDispose); - } - - disposeElement(_element: ITreeNode, _index: number, template: IWorkspaceTrustSettingItemTemplate, _height: number | undefined): void { - if (template.elementDisposables) { - template.elementDisposables.clear(); - } - } -} - -export class WorkspaceTrustTree extends WorkbenchObjectTree { - constructor( - container: HTMLElement, - renderers: ITreeRenderer[], - @IContextKeyService contextKeyService: IContextKeyService, - @IListService listService: IListService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super('WorkspaceTrustTree', container, - new WorkspaceTrustTreeDelegate(), - renderers, - { - horizontalScrolling: false, - alwaysConsumeMouseWheel: false, - supportDynamicHeights: true, - identityProvider: { - getId(e) { - return e.id; - } - }, - accessibilityProvider: new WorkspaceTrustTreeAccessibilityProvider(), - styleController: id => new DefaultStyleController(createStyleSheet(container), id), - smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - multipleSelectionSupport: false, - }, - contextKeyService, - listService, - themeService, - configurationService, - keybindingService, - accessibilityService, - ); - - this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - // Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid - // applying an opacity to the link color. - const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.9)); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-description { color: ${fgWithOpacity}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .settings-toc-container .monaco-list-row:not(.selected) { color: ${fgWithOpacity}; }`); - - // Hack for subpixel antialiasing - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-title .setting-item-ignored { color: ${fgWithOpacity}; }`); - } - - const errorColor = theme.getColor(errorForeground); - if (errorColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-deprecation-message { color: ${errorColor}; }`); - } - - const invalidInputBackground = theme.getColor(inputValidationErrorBackground); - if (invalidInputBackground) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { background-color: ${invalidInputBackground}; }`); - } - - const invalidInputForeground = theme.getColor(inputValidationErrorForeground); - if (invalidInputForeground) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { color: ${invalidInputForeground}; }`); - } - - const invalidInputBorder = theme.getColor(inputValidationErrorBorder); - if (invalidInputBorder) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); - } - - const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); - if (focusedRowBackgroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list-row.focused .settings-row-inner-container { background-color: ${focusedRowBackgroundColor}; }`); - } - - const rowHoverBackgroundColor = theme.getColor(rowHoverBackground); - if (rowHoverBackgroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list-row:not(.focused) .settings-row-inner-container:hover { background-color: ${rowHoverBackgroundColor}; }`); - } - - const focusedRowBorderColor = theme.getColor(focusedRowBorder); - if (focusedRowBorderColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); - } - - const headerForegroundColor = theme.getColor(settingsHeaderForeground); - if (headerForegroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-label { color: ${headerForegroundColor}; }`); - } - - const focusBorderColor = theme.getColor(focusBorder); - if (focusBorderColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); - } - })); - - this.getHTMLElement().classList.add('settings-editor-tree'); - - this.disposables.add(attachStyler(themeService, { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listHoverOutline: editorBackground, - listFocusOutline: editorBackground, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground - }, colors => { - this.style(colors); - })); - - this.disposables.add(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('workbench.list.smoothScrolling')) { - this.updateOptions({ - smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling') - }); - } - })); - } - - protected override createModel(user: string, view: IList>, options: IObjectTreeOptions): ITreeModel { - return new NonCollapsibleObjectTreeModel(user, view, options); - } -} - -export class WorkspaceTrustTreeModel { - - settings: WorkspaceTrustSettingsTreeEntry[] = []; - - update(trustedFolders: URI[]): void { - this.settings = []; - this.settings.push(new WorkspaceTrustSettingsTreeEntry( - 'trustedFolders', - localize('trustedFolders', "Trusted Folders"), - localize('trustedFoldersDescription', "You trust the following folders and their children: "), - trustedFolders)); - } -} - -class WorkspaceTrustTreeAccessibilityProvider implements IListAccessibilityProvider { - getAriaLabel(element: WorkspaceTrustSettingsTreeEntry) { - if (element instanceof WorkspaceTrustSettingsTreeEntry) { - return `element.displayLabel`; - } - - return null; - } - - getWidgetAriaLabel() { - return localize('settings', "Workspace Trust Setting"); - } -} - -class WorkspaceTrustTreeDelegate extends CachedListVirtualDelegate { - - getTemplateId(element: WorkspaceTrustSettingsTreeEntry): string { - return 'template.setting.array'; - } - - hasDynamicHeight(element: WorkspaceTrustSettingsTreeEntry): boolean { - return true; - } - - protected estimateHeight(element: WorkspaceTrustSettingsTreeEntry): number { - return 104; - } -} - -function getListDisplayValue(element: WorkspaceTrustSettingsTreeEntry): IWorkspaceTrustUriDataItem[] { - if (!element.value || !isArray(element.value)) { - return []; - } - - return element.value; -} - -function renderArrayValidations(dataElement: WorkspaceTrustSettingsTreeEntry, template: IWorkspaceTrustSettingListItemTemplate, v: URI[] | undefined, arg3: boolean) { -} - diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/actions/installActions.ts b/lib/vscode/src/vs/workbench/electron-sandbox/actions/installActions.ts new file mode 100644 index 000000000000..37b8f4130e01 --- /dev/null +++ b/lib/vscode/src/vs/workbench/electron-sandbox/actions/installActions.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import Severity from 'vs/base/common/severity'; +import { Action2, ILocalizedString } from 'vs/platform/actions/common/actions'; +import product from 'vs/platform/product/common/product'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { IProductService } from 'vs/platform/product/common/productService'; + +const shellCommandCategory: ILocalizedString = { value: localize('shellCommand', "Shell Command"), original: 'Shell Command' }; + +export class InstallShellScriptAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.installCommandLine', + title: { + value: localize('install', "Install '{0}' command in PATH", product.applicationName), + original: `Install \'${product.applicationName}\' command in PATH` + }, + category: shellCommandCategory, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const productService = accessor.get(IProductService); + + try { + await nativeHostService.installShellCommand(); + + dialogService.show(Severity.Info, localize('successIn', "Shell command '{0}' successfully installed in PATH.", productService.applicationName), []); + } catch (error) { + dialogService.show(Severity.Error, toErrorMessage(error), [localize('ok', "OK"),]); + } + } +} + +export class UninstallShellScriptAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.uninstallCommandLine', + title: { + value: localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName), + original: `Uninstall \'${product.applicationName}\' command from PATH` + }, + category: shellCommandCategory, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const productService = accessor.get(IProductService); + + try { + await nativeHostService.uninstallShellCommand(); + + dialogService.show(Severity.Info, localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", productService.applicationName), []); + } catch (error) { + dialogService.show(Severity.Error, toErrorMessage(error), [localize('ok', "OK"),]); + } + } +} diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/lib/vscode/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 12cf59e82819..67fd94f32dea 100644 --- a/lib/vscode/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/lib/vscode/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -5,7 +5,6 @@ import 'vs/css!./media/actions'; import { URI } from 'vs/base/common/uri'; -import { Action } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; import { applyZoom } from 'vs/platform/windows/electron-sandbox/window'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -21,48 +20,66 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { Codicon } from 'vs/base/common/codicons'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class CloseCurrentWindowAction extends Action { +export class CloseWindowAction extends Action2 { static readonly ID = 'workbench.action.closeWindow'; - static readonly LABEL = localize('closeWindow', "Close Window"); - constructor( - id: string, - label: string, - @INativeHostService private readonly nativeHostService: INativeHostService - ) { - super(id, label); + constructor() { + super({ + id: CloseWindowAction.ID, + title: { + value: localize('closeWindow', "Close Window"), + mnemonicTitle: localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window"), + original: 'Close Window' + }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W }, + linux: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] }, + win: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] } + }, + menu: { + id: MenuId.MenubarFileMenu, + group: '6_close', + order: 4 + } + }); } - override async run(): Promise { - this.nativeHostService.closeWindow(); + override async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + + return nativeHostService.closeWindow(); } } -export abstract class BaseZoomAction extends Action { +abstract class BaseZoomAction extends Action2 { private static readonly SETTING_KEY = 'window.zoomLevel'; - private static readonly MAX_ZOOM_LEVEL = 9; + private static readonly MAX_ZOOM_LEVEL = 8; private static readonly MIN_ZOOM_LEVEL = -8; - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); + constructor(desc: Readonly) { + super(desc); } - protected async setConfiguredZoomLevel(level: number): Promise { + protected async setConfiguredZoomLevel(accessor: ServicesAccessor, level: number): Promise { + const configurationService = accessor.get(IConfigurationService); + level = Math.round(level); // when reaching smallest zoom, prevent fractional zoom levels if (level > BaseZoomAction.MAX_ZOOM_LEVEL || level < BaseZoomAction.MIN_ZOOM_LEVEL) { return; // https://github.com/microsoft/vscode/issues/48357 } - await this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level); + await configurationService.updateValue(BaseZoomAction.SETTING_KEY, level); applyZoom(level); } @@ -70,59 +87,98 @@ export abstract class BaseZoomAction extends Action { export class ZoomInAction extends BaseZoomAction { - static readonly ID = 'workbench.action.zoomIn'; - static readonly LABEL = localize('zoomIn', "Zoom In"); - - constructor( - id: string, - label: string, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, configurationService); + constructor() { + super({ + id: 'workbench.action.zoomIn', + title: { + value: localize('zoomIn', "Zoom In"), + mnemonicTitle: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"), + original: 'Zoom In' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.US_EQUAL, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_EQUAL, KeyMod.CtrlCmd | KeyCode.NUMPAD_ADD] + }, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '3_zoom', + order: 1 + } + }); } - override async run(): Promise { - this.setConfiguredZoomLevel(getZoomLevel() + 1); + override run(accessor: ServicesAccessor): Promise { + return super.setConfiguredZoomLevel(accessor, getZoomLevel() + 1); } } export class ZoomOutAction extends BaseZoomAction { - static readonly ID = 'workbench.action.zoomOut'; - static readonly LABEL = localize('zoomOut', "Zoom Out"); - - constructor( - id: string, - label: string, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, configurationService); + constructor() { + super({ + id: 'workbench.action.zoomOut', + title: { + value: localize('zoomOut', "Zoom Out"), + mnemonicTitle: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out"), + original: 'Zoom Out' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS, KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT], + linux: { + primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, + secondary: [KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT] + } + }, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '3_zoom', + order: 2 + } + }); } - override async run(): Promise { - this.setConfiguredZoomLevel(getZoomLevel() - 1); + override run(accessor: ServicesAccessor): Promise { + return super.setConfiguredZoomLevel(accessor, getZoomLevel() - 1); } } export class ZoomResetAction extends BaseZoomAction { - static readonly ID = 'workbench.action.zoomReset'; - static readonly LABEL = localize('zoomReset', "Reset Zoom"); - - constructor( - id: string, - label: string, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, configurationService); + constructor() { + super({ + id: 'workbench.action.zoomReset', + title: { + value: localize('zoomReset', "Reset Zoom"), + mnemonicTitle: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"), + original: 'Reset Zoom' + }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.NUMPAD_0 + }, + menu: { + id: MenuId.MenubarAppearanceMenu, + group: '3_zoom', + order: 3 + } + }); } - override async run(): Promise { - this.setConfiguredZoomLevel(0); + override run(accessor: ServicesAccessor): Promise { + return super.setConfiguredZoomLevel(accessor, 0); } } -export abstract class BaseSwitchWindow extends Action { +abstract class BaseSwitchWindow extends Action2 { private readonly closeWindowAction: IQuickInputButton = { iconClass: Codicon.removeClose.classNames, @@ -135,24 +191,22 @@ export abstract class BaseSwitchWindow extends Action { alwaysVisible: true }; - constructor( - id: string, - label: string, - private readonly quickInputService: IQuickInputService, - private readonly keybindingService: IKeybindingService, - private readonly modelService: IModelService, - private readonly modeService: IModeService, - private readonly nativeHostService: INativeHostService - ) { - super(id, label); + constructor(desc: Readonly) { + super(desc); } protected abstract isQuickNavigate(): boolean; - override async run(): Promise { - const currentWindowId = this.nativeHostService.windowId; + override async run(accessor: ServicesAccessor): Promise { + const quickInputService = accessor.get(IQuickInputService); + const keybindingService = accessor.get(IKeybindingService); + const modelService = accessor.get(IModelService); + const modeService = accessor.get(IModeService); + const nativeHostService = accessor.get(INativeHostService); - const windows = await this.nativeHostService.getWindows(); + const currentWindowId = nativeHostService.windowId; + + const windows = await nativeHostService.getWindows(); const placeHolder = localize('switchWindowPlaceHolder', "Select a window to switch to"); const picks = windows.map(window => { const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined; @@ -161,45 +215,43 @@ export abstract class BaseSwitchWindow extends Action { payload: window.id, label: window.title, ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, dirty window", window.title) : window.title, - iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), + iconClasses: getIconClasses(modelService, modeService, resource, fileKind), description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined, buttons: currentWindowId !== window.id ? window.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined }; }); const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; - const pick = await this.quickInputService.pick(picks, { + const pick = await quickInputService.pick(picks, { contextKey: 'inWindowsPicker', activeItem: picks[autoFocusIndex], placeHolder, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, onDidTriggerItemButton: async context => { - await this.nativeHostService.closeWindowById(context.item.payload); + await nativeHostService.closeWindowById(context.item.payload); context.removeItem(); } }); if (pick) { - this.nativeHostService.focusWindow({ windowId: pick.payload }); + nativeHostService.focusWindow({ windowId: pick.payload }); } } } -export class SwitchWindow extends BaseSwitchWindow { - - static readonly ID = 'workbench.action.switchWindow'; - static readonly LABEL = localize('switchWindow', "Switch Window..."); - - constructor( - id: string, - label: string, - @IQuickInputService quickInputService: IQuickInputService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @INativeHostService nativeHostService: INativeHostService - ) { - super(id, label, quickInputService, keybindingService, modelService, modeService, nativeHostService); +export class SwitchWindowAction extends BaseSwitchWindow { + + constructor() { + super({ + id: 'workbench.action.switchWindow', + title: { value: localize('switchWindow', "Switch Window..."), original: 'Switch Window...' }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_W } + } + }); } protected isQuickNavigate(): boolean { @@ -207,21 +259,14 @@ export class SwitchWindow extends BaseSwitchWindow { } } -export class QuickSwitchWindow extends BaseSwitchWindow { - - static readonly ID = 'workbench.action.quickSwitchWindow'; - static readonly LABEL = localize('quickSwitchWindow', "Quick Switch Window..."); - - constructor( - id: string, - label: string, - @IQuickInputService quickInputService: IQuickInputService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @INativeHostService nativeHostService: INativeHostService - ) { - super(id, label, quickInputService, keybindingService, modelService, modeService, nativeHostService); +export class QuickSwitchWindowAction extends BaseSwitchWindow { + + constructor() { + super({ + id: 'workbench.action.quickSwitchWindow', + title: { value: localize('quickSwitchWindow', "Quick Switch Window..."), original: 'Quick Switch Window...' }, + f1: true + }); } protected isQuickNavigate(): boolean { diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/lib/vscode/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 6b1eacf88676..59b1afc67098 100644 --- a/lib/vscode/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/lib/vscode/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -5,115 +5,101 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; -import product from 'vs/platform/product/common/product'; -import { SyncActionDescriptor, MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { ConfigureRuntimeArgumentsAction, ToggleDevToolsAction, ToggleSharedProcessAction, ReloadWindowWithExtensionsDisabledAction } from 'vs/workbench/electron-sandbox/actions/developerActions'; -import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseCurrentWindowAction, SwitchWindow, QuickSwitchWindow, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from 'vs/workbench/electron-sandbox/actions/windowActions'; +import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseWindowAction, SwitchWindowAction, QuickSwitchWindowAction, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from 'vs/workbench/electron-sandbox/actions/windowActions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IsMacContext } from 'vs/platform/contextkey/common/contextkeys'; -import { EditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { PartsSplash } from 'vs/workbench/electron-sandbox/splash'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { InstallShellScriptAction, UninstallShellScriptAction } from 'vs/workbench/electron-sandbox/actions/installActions'; +import { EditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; // Actions (function registerActions(): void { - const registry = Registry.as(Extensions.WorkbenchActions); // Actions: Zoom - (function registerZoomActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ZoomInAction, { primary: KeyMod.CtrlCmd | KeyCode.US_EQUAL, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_EQUAL, KeyMod.CtrlCmd | KeyCode.NUMPAD_ADD] }), 'View: Zoom In', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ZoomOutAction, { primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS, KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT], linux: { primary: KeyMod.CtrlCmd | KeyCode.US_MINUS, secondary: [KeyMod.CtrlCmd | KeyCode.NUMPAD_SUBTRACT] } }), 'View: Zoom Out', CATEGORIES.View.value); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ZoomResetAction, { primary: KeyMod.CtrlCmd | KeyCode.NUMPAD_0 }), 'View: Reset Zoom', CATEGORIES.View.value); - })(); + registerAction2(ZoomInAction); + registerAction2(ZoomOutAction); + registerAction2(ZoomResetAction); // Actions: Window - (function registerWindowActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(SwitchWindow, { primary: 0, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_W } }), 'Switch Window...'); - registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickSwitchWindow), 'Quick Switch Window...'); - - // Close window - registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseCurrentWindowAction, { - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W }, - linux: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] }, - win: { primary: KeyMod.Alt | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W] } - } - ), 'Close Window'); + registerAction2(SwitchWindowAction); + registerAction2(QuickSwitchWindowAction); + registerAction2(CloseWindowAction); - // Close the window when the last editor is closed by reusing the same keybinding - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: CloseCurrentWindowAction.ID, + if (isMacintosh) { + // macOS: behave like other native apps that have documents + // but can run without a document opened and allow to close + // the window when the last document is closed + // (https://github.com/microsoft/vscode/issues/126042) + KeybindingsRegistry.registerKeybindingRule({ + id: CloseWindowAction.ID, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(EditorsVisibleContext.toNegated(), SingleEditorGroupsContext), - primary: KeyMod.CtrlCmd | KeyCode.KEY_W, - handler: accessor => { - const nativeHostService = accessor.get(INativeHostService); - nativeHostService.closeWindow(); - } + primary: KeyMod.CtrlCmd | KeyCode.KEY_W }); + } - // Quit - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.quit', - weight: KeybindingWeight.WorkbenchContrib, - handler(accessor: ServicesAccessor) { - const nativeHostService = accessor.get(INativeHostService); - nativeHostService.quit(); - }, - when: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q }, - linux: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q } - }); - })(); + // Actions: Install Shell Script (macOS only) + if (isMacintosh) { + registerAction2(InstallShellScriptAction); + registerAction2(UninstallShellScriptAction); + } + + // Quit + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.action.quit', + weight: KeybindingWeight.WorkbenchContrib, + handler(accessor: ServicesAccessor) { + const nativeHostService = accessor.get(INativeHostService); + nativeHostService.quit(); + }, + when: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q }, + linux: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Q } + }); // Actions: macOS Native Tabs - (function registerMacOSNativeTabsActions(): void { - if (isMacintosh) { - [ - { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, - { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, - { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, - { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, - { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, - { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } - ].forEach(command => { - CommandsRegistry.registerCommand(command.id, command.handler); + if (isMacintosh) { + [ + { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, + { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, + { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, + { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, + { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, + { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } + ].forEach(command => { + CommandsRegistry.registerCommand(command.id, command.handler); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command, - when: ContextKeyExpr.equals('config.window.nativeTabs', true) - }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command, + when: ContextKeyExpr.equals('config.window.nativeTabs', true) }); - } - })(); + }); + } // Actions: Developer - (function registerDeveloperActions(): void { - registerAction2(ReloadWindowWithExtensionsDisabledAction); - registerAction2(ConfigureRuntimeArgumentsAction); - registerAction2(ToggleSharedProcessAction); - registerAction2(ToggleDevToolsAction); - })(); + registerAction2(ReloadWindowWithExtensionsDisabledAction); + registerAction2(ConfigureRuntimeArgumentsAction); + registerAction2(ToggleSharedProcessAction); + registerAction2(ToggleDevToolsAction); })(); // Menu (function registerMenu(): void { - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: CloseCurrentWindowAction.ID, - title: localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window") - }, - order: 4 - }); + // Quit MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: 'z_Exit', command: { @@ -123,56 +109,6 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; order: 1, when: IsMacContext.toNegated() }); - - // Zoom - - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_zoom', - command: { - id: ZoomInAction.ID, - title: localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") - }, - order: 1 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_zoom', - command: { - id: ZoomOutAction.ID, - title: localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") - }, - order: 2 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_zoom', - command: { - id: ZoomResetAction.ID, - title: localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") - }, - order: 3 - }); - - if (!!product.reportIssueUrl) { - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '3_feedback', - command: { - id: 'workbench.action.openIssueReporter', - title: localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") - }, - order: 3 - }); - } - - // Tools - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '5_tools', - command: { - id: 'workbench.action.openProcessExplorer', - title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") - }, - order: 2 - }); })(); // Configuration @@ -386,3 +322,10 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; jsonRegistry.registerSchema(argvDefinitionFileSchemaId, schema); })(); + +// Workbench Contributions +(function registerWorkbenchContributions() { + + // Splash + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(PartsSplash, LifecyclePhase.Starting); +})(); diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts b/lib/vscode/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts index 46ad42f83615..266f21e127c2 100644 --- a/lib/vscode/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts +++ b/lib/vscode/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Separator } from 'vs/base/common/actions'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { isMacintosh } from 'vs/base/common/platform'; @@ -48,11 +47,6 @@ export class NativeMenubarControl extends MenubarControl { ) { super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService); - if (isMacintosh) { - this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService)); - this.topLevelTitles['Preferences'] = localize('mPreferences', "Preferences"); - } - for (const topLevelMenuName of Object.keys(this.topLevelTitles)) { const menu = this.menus[topLevelMenuName]; if (menu) { diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/lib/vscode/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index fc7dbe32bfb3..4f84e7b6ea46 100644 --- a/lib/vscode/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/lib/vscode/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -11,14 +11,11 @@ import { Event } from 'vs/base/common/event'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IProcessEnvironment, isWindows, OperatingSystem } from 'vs/base/common/platform'; -import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { isWindows } from 'vs/base/common/platform'; import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; -import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminalInstanceService'; -import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { SearchService } from 'vs/workbench/services/search/common/searchService'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -28,8 +25,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { IShellLaunchConfigResolveOptions, ITerminalProfile, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; //#region Environment @@ -268,23 +263,6 @@ registerSingleton(IExtensionService, SimpleExtensionService); //#endregion -//#region Webview - -class SimpleWebviewService implements IWebviewService { - declare readonly _serviceBrand: undefined; - - readonly activeWebview = undefined; - readonly onDidChangeActiveWebview = Event.None; - - createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } - createWebviewOverlay(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewOverlay { throw new Error('Method not implemented.'); } -} - -registerSingleton(IWebviewService, SimpleWebviewService); - -//#endregion - - //#region Tunnel class SimpleTunnelService implements ITunnelService { @@ -296,6 +274,7 @@ class SimpleTunnelService implements ITunnelService { canMakePublic = false; onTunnelOpened = Event.None; onTunnelClosed = Event.None; + hasTunnelProvider = false; canTunnel(uri: URI): boolean { return false; } openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined { return undefined; } @@ -309,34 +288,6 @@ registerSingleton(ITunnelService, SimpleTunnelService); //#endregion -//#region Terminal Instance - -class SimpleTerminalInstanceService extends TerminalInstanceService { } - -registerSingleton(ITerminalInstanceService, SimpleTerminalInstanceService); - -//#endregion - - -//#region Terminal Profile Resolver Service - -class SimpleTerminalProfileResolverService implements ITerminalProfileResolverService { - - _serviceBrand: undefined; - - resolveIcon(shellLaunchConfig: IShellLaunchConfig, os: OperatingSystem): void { } - async resolveShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig, options: IShellLaunchConfigResolveOptions): Promise { } - getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { throw new Error('Method not implemented.'); } - getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { throw new Error('Method not implemented.'); } - getDefaultShellArgs(options: IShellLaunchConfigResolveOptions): Promise { throw new Error('Method not implemented.'); } - getShellEnvironment(remoteAuthority: string | undefined): Promise { throw new Error('Method not implemented.'); } - getSafeConfigValue(key: string, os: OperatingSystem): unknown | undefined { throw new Error('Method not implemented.'); } - getSafeConfigValueFullKey(key: string): unknown | undefined { throw new Error('Method not implemented.'); } -} - -registerSingleton(ITerminalProfileResolverService, SimpleTerminalProfileResolverService); - - //#region Search Service class SimpleSearchService extends SearchService { diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/shared.desktop.main.ts b/lib/vscode/src/vs/workbench/electron-sandbox/shared.desktop.main.ts index fa857a32a3d7..26bbfa796cfb 100644 --- a/lib/vscode/src/vs/workbench/electron-sandbox/shared.desktop.main.ts +++ b/lib/vscode/src/vs/workbench/electron-sandbox/shared.desktop.main.ts @@ -208,7 +208,7 @@ export abstract class SharedDesktopMain extends Disposable { await result; } - // Uri Identity + // URI Identity const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); @@ -264,7 +264,7 @@ export abstract class SharedDesktopMain extends Disposable { ]); // Workspace Trust Service - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, environmentService, storageService, uriIdentityService, configurationService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, storageService, uriIdentityService, environmentService, configurationService, remoteAuthorityResolverService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/splash.ts b/lib/vscode/src/vs/workbench/electron-sandbox/splash.ts new file mode 100644 index 000000000000..0ccd99880c89 --- /dev/null +++ b/lib/vscode/src/vs/workbench/electron-sandbox/splash.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; +import { getTotalHeight, getTotalWidth } from 'vs/base/browser/dom'; +import { Color } from 'vs/base/common/color'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { getThemeTypeSelector, IThemeService } from 'vs/platform/theme/common/themeService'; +import { DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; +import * as themes from 'vs/workbench/common/theme'; +import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import * as perf from 'vs/base/common/performance'; +import { assertIsDefined } from 'vs/base/common/types'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; + +export class PartsSplash { + + private static readonly _splashElementId = 'monaco-parts-splash'; + + private readonly _disposables = new DisposableStore(); + + private _didChangeTitleBarStyle?: boolean; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @ILifecycleService lifecycleService: ILifecycleService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, + @IConfigurationService configService: IConfigurationService, + @INativeHostService private readonly _nativeHostService: INativeHostService + ) { + lifecycleService.when(LifecyclePhase.Restored).then(_ => { + this._removePartsSplash(); + perf.mark('code/didRemovePartsSplash'); + }); + + Event.debounce(Event.any( + onDidChangeFullscreen, + editorGroupsService.onDidLayout + ), () => { }, 800)(this._savePartsSplash, this, this._disposables); + + configService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('window.titleBarStyle')) { + this._didChangeTitleBarStyle = true; + this._savePartsSplash(); + } + }, this, this._disposables); + + _themeService.onDidColorThemeChange(_ => { + this._savePartsSplash(); + }, this, this._disposables); + } + + private _savePartsSplash() { + const theme = this._themeService.getColorTheme(); + + this._nativeHostService.saveWindowSplash({ + baseTheme: getThemeTypeSelector(theme.type), + colorInfo: { + foreground: theme.getColor(foreground)?.toString(), + background: Color.Format.CSS.formatHex(theme.getColor(editorBackground) || themes.WORKBENCH_BACKGROUND(theme)), + editorBackground: theme.getColor(editorBackground)?.toString(), + titleBarBackground: theme.getColor(themes.TITLE_BAR_ACTIVE_BACKGROUND)?.toString(), + activityBarBackground: theme.getColor(themes.ACTIVITY_BAR_BACKGROUND)?.toString(), + sideBarBackground: theme.getColor(themes.SIDE_BAR_BACKGROUND)?.toString(), + statusBarBackground: theme.getColor(themes.STATUS_BAR_BACKGROUND)?.toString(), + statusBarNoFolderBackground: theme.getColor(themes.STATUS_BAR_NO_FOLDER_BACKGROUND)?.toString(), + windowBorder: theme.getColor(themes.WINDOW_ACTIVE_BORDER)?.toString() ?? theme.getColor(themes.WINDOW_INACTIVE_BORDER)?.toString() + }, + layoutInfo: !this._shouldSaveLayoutInfo() ? undefined : { + sideBarSide: this._layoutService.getSideBarPosition() === Position.RIGHT ? 'right' : 'left', + editorPartMinWidth: DEFAULT_EDITOR_MIN_DIMENSIONS.width, + titleBarHeight: this._layoutService.isVisible(Parts.TITLEBAR_PART) ? getTotalHeight(assertIsDefined(this._layoutService.getContainer(Parts.TITLEBAR_PART))) : 0, + activityBarWidth: this._layoutService.isVisible(Parts.ACTIVITYBAR_PART) ? getTotalWidth(assertIsDefined(this._layoutService.getContainer(Parts.ACTIVITYBAR_PART))) : 0, + sideBarWidth: this._layoutService.isVisible(Parts.SIDEBAR_PART) ? getTotalWidth(assertIsDefined(this._layoutService.getContainer(Parts.SIDEBAR_PART))) : 0, + statusBarHeight: this._layoutService.isVisible(Parts.STATUSBAR_PART) ? getTotalHeight(assertIsDefined(this._layoutService.getContainer(Parts.STATUSBAR_PART))) : 0, + windowBorder: this._layoutService.hasWindowBorder(), + windowBorderRadius: this._layoutService.getWindowBorderRadius() + } + }); + } + + private _shouldSaveLayoutInfo(): boolean { + return !isFullscreen() && !this._environmentService.isExtensionDevelopment && !this._didChangeTitleBarStyle; + } + + private _removePartsSplash(): void { + const element = document.getElementById(PartsSplash._splashElementId); + if (element) { + element.style.display = 'none'; + } + + // remove initial colors + const defaultStyles = document.head.getElementsByClassName('initialShellColors'); + if (defaultStyles.length) { + document.head.removeChild(defaultStyles[0]); + } + } + + dispose(): void { + this._disposables.dispose(); + } +} diff --git a/lib/vscode/src/vs/workbench/electron-sandbox/window.ts b/lib/vscode/src/vs/workbench/electron-sandbox/window.ts index d4e4e0ed71f8..de32fcf338db 100644 --- a/lib/vscode/src/vs/workbench/electron-sandbox/window.ts +++ b/lib/vscode/src/vs/workbench/electron-sandbox/window.ts @@ -10,7 +10,7 @@ import { equals } from 'vs/base/common/objects'; import { EventType, EventHelper, addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { Separator } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; -import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowMinimumSize, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; @@ -19,7 +19,7 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work import { applyZoom } from 'vs/platform/windows/electron-sandbox/window'; import { setFullscreen, getZoomLevel } from 'vs/base/browser/browser'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IBaseResourceEditorInput, IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { env } from 'vs/base/common/process'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; @@ -32,7 +32,7 @@ import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/work import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { IProductService } from 'vs/platform/product/common/productService'; -import { INotificationService, IPromptChoice, NeverShowAgainScope, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -186,28 +186,6 @@ export class NativeWindow extends Disposable { // Message support ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => this.notificationService.info(message)); - // Shell Environment Issue Notifications - const choices: IPromptChoice[] = [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667') - }]; - - ipcRenderer.on('vscode:showShellEnvSlowWarning', () => this.notificationService.prompt( - Severity.Warning, - localize('shellEnvSlowWarning', "Resolving your shell environment is taking very long. Please review your shell configuration."), - choices, - { - sticky: true, - neverShowAgain: { id: 'ignoreShellEnvSlowWarning', scope: NeverShowAgainScope.GLOBAL } - } - )); - - ipcRenderer.on('vscode:showShellEnvTimeoutError', () => this.notificationService.prompt( - Severity.Error, - localize('shellEnvTimeoutError', "Unable to resolve your shell environment in a reasonable time. Please review your shell configuration."), - choices - )); - // Fullscreen Events ipcRenderer.on('vscode:enterFullScreen', async () => { await this.lifecycleService.when(LifecyclePhase.Ready); @@ -547,6 +525,21 @@ export class NativeWindow extends Disposable { } } } + + // Assume `uri` this is a workspace uri, let's see if we can handle it + await this.fileService.activateProvider(uri.scheme); + + if (this.fileService.canHandleResource(uri)) { + return { + resolved: URI.from({ + scheme: this.productService.urlProtocol, + path: 'workspace', + query: uri.toString() + }), + dispose() { } + }; + } + return undefined; } }); @@ -662,7 +655,7 @@ export class NativeWindow extends Disposable { // In wait mode, listen to changes to the editors and wait until the files // are closed that the user wants to wait for. When this happens we delete // the wait marker file to signal to the outside that editing is done. - this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(p => URI.revive(p.fileUri)))); + this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri)))); } } @@ -678,17 +671,21 @@ export class NativeWindow extends Disposable { private async openResources(resources: Array, diffMode: boolean): Promise { await this.lifecycleService.when(LifecyclePhase.Ready); + const editors: IBaseResourceEditorInput[] = []; + // In diffMode we open 2 resources as diff if (diffMode && resources.length === 2 && resources[0].resource && resources[1].resource) { - return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } }); - } - - // For one file, just put it into the current active editor - if (resources.length === 1) { - return this.editorService.openEditor(resources[0]); + const diffEditor: IResourceDiffEditorInput = { + originalInput: { resource: resources[0].resource }, + modifiedInput: { resource: resources[1].resource }, + options: { pinned: true } + }; + editors.push(diffEditor); + } else { + editors.push(...resources); } - // Otherwise open all - return this.editorService.openEditors(resources); + // Open as editors + return this.editorService.openEditors(editors, undefined, { validateTrust: true }); } } diff --git a/lib/vscode/src/vs/workbench/services/activity/common/activity.ts b/lib/vscode/src/vs/workbench/services/activity/common/activity.ts index 8eda8c67e8eb..1f9fc1f0d1f8 100644 --- a/lib/vscode/src/vs/workbench/services/activity/common/activity.ts +++ b/lib/vscode/src/vs/workbench/services/activity/common/activity.ts @@ -46,7 +46,7 @@ export interface IBadge { class BaseBadge implements IBadge { - constructor(public readonly descriptorFn: (arg: any) => string) { + constructor(readonly descriptorFn: (arg: any) => string) { this.descriptorFn = descriptorFn; } @@ -57,7 +57,7 @@ class BaseBadge implements IBadge { export class NumberBadge extends BaseBadge { - constructor(public readonly number: number, descriptorFn: (num: number) => string) { + constructor(readonly number: number, descriptorFn: (num: number) => string) { super(descriptorFn); this.number = number; @@ -70,13 +70,13 @@ export class NumberBadge extends BaseBadge { export class TextBadge extends BaseBadge { - constructor(public readonly text: string, descriptorFn: () => string) { + constructor(readonly text: string, descriptorFn: () => string) { super(descriptorFn); } } export class IconBadge extends BaseBadge { - constructor(public readonly icon: ThemeIcon, descriptorFn: () => string) { + constructor(readonly icon: ThemeIcon, descriptorFn: () => string) { super(descriptorFn); } } diff --git a/lib/vscode/src/vs/workbench/services/authentication/browser/authenticationService.ts b/lib/vscode/src/vs/workbench/services/authentication/browser/authenticationService.ts index bc5357496edb..8652c0c8dd20 100644 --- a/lib/vscode/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/lib/vscode/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -37,7 +37,7 @@ export interface IAccountUsage { lastUsed: number; } -const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser', 'ms-vscode.remotehub', 'ms-vscode.remotehub-insiders', 'github.codespaces']; +const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'ms-vscode.remotehub', 'ms-vscode.remotehub-insiders', 'github.remotehub', 'github.remotehub-insiders', 'github.codespaces']; export function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { const accountKey = `${providerId}-${accountName}-usages`; @@ -338,7 +338,7 @@ export class AuthenticationService extends Disposable implements IAuthentication } Object.keys(existingRequestsForProvider).forEach(requestedScopes => { - if (addedSessions.some(session => session.scopes.slice().sort().join('') === requestedScopes)) { + if (addedSessions.some(session => session.scopes.slice().join('') === requestedScopes)) { const sessionRequest = existingRequestsForProvider[requestedScopes]; sessionRequest?.disposables.forEach(item => item.dispose()); @@ -565,9 +565,11 @@ export class AuthenticationService extends Disposable implements IAuthentication id: `${providerId}${extensionId}Access`, title: nls.localize({ key: 'accessRequest', - comment: ['The placeholder {0} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count'] + comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] }, - "Grant access to {0}... (1)", extensionName) + "Grant access to {0} for {1}... (1)", + this.getLabel(providerId), + extensionName) } }); @@ -602,7 +604,7 @@ export class AuthenticationService extends Disposable implements IAuthentication if (provider) { const providerRequests = this._signInRequestItems.get(providerId); - const scopesList = scopes.sort().join(''); + const scopesList = scopes.join(''); const extensionHasExistingRequest = providerRequests && providerRequests[scopesList] && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); @@ -615,12 +617,12 @@ export class AuthenticationService extends Disposable implements IAuthentication group: '2_signInRequests', command: { id: `${extensionId}signIn`, - title: nls.localize( - { - key: 'signInRequest', - comment: ['The placeholder {0} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.'] - }, - "Sign in to use {0} (1)", + title: nls.localize({ + key: 'signInRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] + }, + "Sign in with {0} to use {1} (1)", + provider.label, extensionName) } }); diff --git a/lib/vscode/src/vs/workbench/services/banner/browser/bannerService.ts b/lib/vscode/src/vs/workbench/services/banner/browser/bannerService.ts new file mode 100644 index 000000000000..15f9fe15afda --- /dev/null +++ b/lib/vscode/src/vs/workbench/services/banner/browser/bannerService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILinkDescriptor } from 'vs/platform/opener/browser/link'; + + +export interface IBannerItem { + readonly id: string; + readonly icon: Codicon; + readonly message: string | MarkdownString; + readonly actions?: ILinkDescriptor[]; + readonly ariaLabel?: string; + readonly onClose?: () => void; +} + +export const IBannerService = createDecorator('bannerService'); + +export interface IBannerService { + readonly _serviceBrand: undefined; + + focus(): void; + focusNextAction(): void; + focusPreviousAction(): void; + hide(id: string): void; + show(item: IBannerItem): void; +} diff --git a/lib/vscode/src/vs/workbench/services/clipboard/browser/clipboardService.ts b/lib/vscode/src/vs/workbench/services/clipboard/browser/clipboardService.ts index 7e0402782855..50e847ce24d4 100644 --- a/lib/vscode/src/vs/workbench/services/clipboard/browser/clipboardService.ts +++ b/lib/vscode/src/vs/workbench/services/clipboard/browser/clipboardService.ts @@ -12,13 +12,16 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { once } from 'vs/base/common/functional'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { isSafari } from 'vs/base/browser/browser'; +import { ILogService } from 'vs/platform/log/common/log'; export class BrowserClipboardService extends BaseBrowserClipboardService { constructor( @INotificationService private readonly notificationService: INotificationService, @IOpenerService private readonly openerService: IOpenerService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService ) { super(); } @@ -31,10 +34,16 @@ export class BrowserClipboardService extends BaseBrowserClipboardService { try { return await navigator.clipboard.readText(); } catch (error) { + this.logService.error(error); + if (!!this.environmentService.extensionTestsLocationURI) { return ''; // do not ask for input in tests (https://github.com/microsoft/vscode/issues/112264) } + if (isSafari) { + return ''; // Safari does not seem to provide anyway to enable cipboard access (https://github.com/microsoft/vscode-internalbacklog/issues/2162#issuecomment-852042867) + } + return new Promise(resolve => { // Inform user about permissions problem (https://github.com/microsoft/vscode/issues/112089) diff --git a/lib/vscode/src/vs/workbench/services/configuration/common/configuration.ts b/lib/vscode/src/vs/workbench/services/configuration/common/configuration.ts index 9cff28447ca7..105b65223d71 100644 --- a/lib/vscode/src/vs/workbench/services/configuration/common/configuration.ts +++ b/lib/vscode/src/vs/workbench/services/configuration/common/configuration.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; -import { Registry } from 'vs/platform/registry/common/platform'; export const FOLDER_CONFIG_FOLDER_NAME = '.vscode'; export const FOLDER_SETTINGS_NAME = 'settings'; @@ -48,14 +47,6 @@ export interface IConfigurationCache { } -export function filterSettingsRequireWorkspaceTrust(settings: ReadonlyArray): ReadonlyArray { - const configurationRegistry = Registry.as(Extensions.Configuration); - return settings.filter(key => { - const property = configurationRegistry.getConfigurationProperties()[key]; - return property.restricted && property.scope !== ConfigurationScope.APPLICATION && property.scope !== ConfigurationScope.MACHINE; - }); -} - export type RestrictedSettings = { default: ReadonlyArray; userLocal?: ReadonlyArray; diff --git a/lib/vscode/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/lib/vscode/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index 68a71a2b08c1..be8987984e41 100644 --- a/lib/vscode/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/lib/vscode/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -26,6 +26,13 @@ export interface IConfigurationResolverService { */ resolveAnyAsync(folder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary): Promise; + /** + * Recursively resolves all variables in the given config. + * Returns a copy of it with substituted values and a map of variables and their resolution. + * Keys in the map will be of the format input:variableName or command:variableName. + */ + resolveAnyMap(folder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary): Promise<{ newConfig: any, resolvedVariables: Map }>; + /** * Recursively resolves all variables (including commands and user input) in the given config and returns a copy of it with substituted values. * If a "variables" dictionary (with names -> command ids) is given, command variables are first mapped through it before being resolved. diff --git a/lib/vscode/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/lib/vscode/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index cb4efa5ee73c..0e5a3781144c 100644 --- a/lib/vscode/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/lib/vscode/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -99,7 +99,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe return this.resolveAnyBase(workspaceFolder, config, commandValueMapping); } - protected async resolveAnyMap(workspaceFolder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary): Promise<{ newConfig: any, resolvedVariables: Map }> { + public async resolveAnyMap(workspaceFolder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary): Promise<{ newConfig: any, resolvedVariables: Map }> { const resolvedVariables = new Map(); const newConfig = await this.resolveAnyBase(workspaceFolder, config, commandValueMapping, resolvedVariables); return { newConfig, resolvedVariables }; @@ -141,7 +141,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe // loop through all variables occurrences in 'value' const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { - // disallow attempted nesting, see #77289 + // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. if (variable.includes(AbstractVariableResolverService.VARIABLE_LHS)) { return match; } @@ -152,6 +152,10 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe resolvedVariables.set(variable, resolvedValue); } + if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(AbstractVariableResolverService.VARIABLE_REGEXP)) { + resolvedValue = this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); + } + return resolvedValue; }); diff --git a/lib/vscode/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/lib/vscode/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index deefcdc30ba6..b6ef5caeb414 100644 --- a/lib/vscode/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/lib/vscode/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -25,6 +25,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { stripIcons } from 'vs/base/common/iconLabels'; import { coalesce } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; export class ContextMenuService extends Disposable implements IContextMenuService { @@ -32,6 +33,9 @@ export class ContextMenuService extends Disposable implements IContextMenuServic private impl: IContextMenuService; + private readonly _onDidShowContextMenu = this._register(new Emitter()); + readonly onDidShowContextMenu = this._onDidShowContextMenu.event; + constructor( @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @@ -56,6 +60,7 @@ export class ContextMenuService extends Disposable implements IContextMenuServic showContextMenu(delegate: IContextMenuDelegate): void { this.impl.showContextMenu(delegate); + this._onDidShowContextMenu.fire(); } } @@ -63,6 +68,8 @@ class NativeContextMenuService extends Disposable implements IContextMenuService declare readonly _serviceBrand: undefined; + readonly onDidShowContextMenu = new Emitter().event; + constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, diff --git a/lib/vscode/src/vs/workbench/services/decorations/browser/decorationsService.ts b/lib/vscode/src/vs/workbench/services/decorations/browser/decorationsService.ts index 6ece15214299..0502cdfa4500 100644 --- a/lib/vscode/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/lib/vscode/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -75,12 +75,9 @@ class DecorationRule { const { color, letter } = data; // label createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(theme, color)};`, element); - // icon if (ThemeIcon.isThemeIcon(letter)) { this._createIconCSSRule(letter, color, element, theme); - } - // letter - else if (letter) { + } else if (letter) { createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letter}"; color: ${getColor(theme, color)};`, element); } } @@ -93,6 +90,7 @@ class DecorationRule { // icon (only show first) const icon = data.find(d => ThemeIcon.isThemeIcon(d.letter))?.letter as ThemeIcon | undefined; if (icon) { + // todo@jrieken this is fishy. icons should be just like letter and not mute bubble badge this._createIconCSSRule(icon, color, element, theme); } else { // badge @@ -112,14 +110,26 @@ class DecorationRule { } private _createIconCSSRule(icon: ThemeIcon, color: string | undefined, element: HTMLStyleElement, theme: IColorTheme) { - const codicon = iconRegistry.get(icon.id); + + const index = icon.id.lastIndexOf('~'); + const id = index < 0 ? icon.id : icon.id.substr(0, index); + const modifier = index < 0 ? '' : icon.id.substr(index + 1); + + const codicon = iconRegistry.get(id); if (!codicon || !('fontCharacter' in codicon.definition)) { return; } const charCode = parseInt(codicon.definition.fontCharacter.substr(1), 16); createCSSRule( `.${this.iconBadgeClassName}::after`, - `content: "${String.fromCharCode(charCode)}"; color: ${getColor(theme, color)}; font-family: codicon; font-size: 16px; padding-right: 14px; font-weight: normal`, + `content: "${String.fromCharCode(charCode)}"; + color: ${getColor(theme, color)}; + font-family: codicon; + font-size: 16px; + padding-right: 14px; + font-weight: normal; + ${modifier === 'spin' ? 'animation: codicon-spin 1.5s steps(30) infinite' : ''}; + `, element ); } diff --git a/lib/vscode/src/vs/workbench/services/editor/browser/codeEditorService.ts b/lib/vscode/src/vs/workbench/services/editor/browser/codeEditorService.ts index dcef4106dfbf..05d2321431a7 100644 --- a/lib/vscode/src/vs/workbench/services/editor/browser/codeEditorService.ts +++ b/lib/vscode/src/vs/workbench/services/editor/browser/codeEditorService.ts @@ -8,12 +8,13 @@ import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkbenchEditorConfiguration, TextEditorOptions } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { isEqual } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; export class CodeEditorService extends CodeEditorServiceImpl { @@ -60,8 +61,7 @@ export class CodeEditorService extends CodeEditorServiceImpl { ) { const targetEditor = activeTextEditorControl.getModifiedEditor(); - const textOptions = TextEditorOptions.create(input.options); - textOptions.apply(targetEditor, ScrollType.Smooth); + applyTextEditorOptions(input.options, targetEditor, ScrollType.Smooth); return targetEditor; } diff --git a/lib/vscode/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/lib/vscode/src/vs/workbench/services/editor/browser/editorOverrideService.ts index a6816fe08994..5d87f892e2a6 100644 --- a/lib/vscode/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/lib/vscode/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -9,20 +9,21 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { basename, extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { EditorActivation, EditorOverride, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { EditorResourceAccessor, IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { EditorActivation, EditorOverride, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { EditorResourceAccessor, IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup, SideBySideEditor } from 'vs/workbench/common/editor'; import { IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Schemas } from 'vs/base/common/network'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { ContributedEditorInfo, ContributedEditorPriority, ContributionPointOptions, DEFAULT_EDITOR_ASSOCIATION, DiffEditorInputFactoryFunction, EditorAssociation, EditorAssociations, EditorInputFactoryFunction, editorsAssociationsSettingId, globMatchesResource, IEditorOverrideService, priorityToRank } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IKeyMods, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ILogService } from 'vs/platform/log/common/log'; interface IContributedEditorInput extends IEditorInput { viewType?: string; @@ -41,8 +42,13 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; - private _contributionPoints: Map = new Map(); + // Constants + private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; + private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; + + // Data Stores + private _contributionPoints: Map = new Map(); private cache: Set | undefined; constructor( @@ -52,12 +58,15 @@ export class EditorOverrideService extends Disposable implements IEditorOverride @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, - @IExtensionService private readonly extensionService: IExtensionService + @IExtensionService private readonly extensionService: IExtensionService, + @IHostService private readonly hostService: IHostService, + @ILogService private readonly logService: ILogService, ) { super(); // Read in the cache on statup this.cache = new Set(JSON.parse(this.storageService.get(EditorOverrideService.overrideCacheStorageID, StorageScope.GLOBAL, JSON.stringify([])))); this.storageService.remove(EditorOverrideService.overrideCacheStorageID, StorageScope.GLOBAL); + this.convertOldAssociationFormat(); this._register(this.storageService.onWillSaveState(() => { // We want to store the glob patterns we would activate on, this allows us to know if we need to await the ext host on startup for opening a resource @@ -68,9 +77,16 @@ export class EditorOverrideService extends Disposable implements IEditorOverride this.extensionService.onDidRegisterExtensions(() => { this.cache = undefined; }); + + // When the setting changes we want to ensure that it is properly converted + this._register(this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(editorsAssociationsSettingId)) { + this.convertOldAssociationFormat(); + } + })); } - async resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): Promise { + async resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup): Promise { // If it was an override before we await for the extensions to activate and then proceed with overriding or else they won't be registered if (this.cache && editor.resource && this.resourceMatchesCache(editor.resource)) { await this.extensionService.whenInstalledExtensionsRegistered(); @@ -81,11 +97,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Always ensure inputs have populated resource fields - if (editor instanceof DiffEditorInput) { - if ((!editor.modifiedInput.resource || !editor.originalInput.resource)) { - return { editor, options, group }; - } - } else if (!editor.resource) { + const resource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (!resource) { return { editor, options, group }; } @@ -106,7 +119,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Resolved the override as much as possible, now find a given contribution - const { contributionPoint, conflictingDefault } = this.getContributionPoint(editor instanceof DiffEditorInput ? editor.modifiedInput.resource! : editor.resource!, override); + const { contributionPoint, conflictingDefault } = this.getContributionPoint(resource, override); const selectedContribution = contributionPoint; if (!selectedContribution) { return { editor, options, group }; @@ -121,13 +134,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride if (selectedContribution.editorInfo.describes(editor)) { return; } - const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); + const input = await this.doOverrideEditorInput(resource, editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Wait one second to give the user ample time to see the current editor then ask them to configure a default - setTimeout(() => { - this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); - }, 1200); + // Show the conflicting default dialog + await this.doHandleConflictingDefaults(resource, selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } + // Add the group as we might've changed it with the quickpick if (input) { this.sendOverrideTelemetry(input.editor); @@ -143,10 +155,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride createEditorInput: EditorInputFactoryFunction, createDiffEditorInput?: DiffEditorInputFactoryFunction ): IDisposable { - if (this._contributionPoints.get(globPattern) === undefined) { - this._contributionPoints.set(globPattern, []); + let contributionPoint = this._contributionPoints.get(globPattern); + if (contributionPoint === undefined) { + contributionPoint = []; + this._contributionPoints.set(globPattern, contributionPoint); } - const remove = insert(this._contributionPoints.get(globPattern)!, { + const remove = insert(contributionPoint, { globPattern, editorInfo, options, @@ -156,41 +170,95 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return toDisposable(() => remove()); } - hasContributionPoint(schemeOrGlob: string): boolean { - return this._contributionPoints.has(schemeOrGlob); + hasContributionPoint(glob: string): boolean { + return this._contributionPoints.has(glob); } getAssociationsForResource(resource: URI): EditorAssociations { - const rawAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || []; - return rawAssociations.filter(association => association.filenamePattern && globMatchesResource(association.filenamePattern, resource)); + const associations = this.getAllUserAssociations(); + const matchingAssociations = associations.filter(association => association.filenamePattern && globMatchesResource(association.filenamePattern, resource)); + const allContributions: ContributionPoints = this._allContributions; + // Ensure that the settings are valid contribution points + return matchingAssociations.filter(association => allContributions.find(c => c.editorInfo.id === association.viewType)); } - updateUserAssociations(globPattern: string, editorID: string): void { - const newAssociation: EditorAssociation = { viewType: editorID, filenamePattern: globPattern }; - const currentAssociations = [...this.configurationService.getValue(editorsAssociationsSettingId)]; - - // First try updating existing association - for (let i = 0; i < currentAssociations.length; ++i) { - const existing = currentAssociations[i]; - if (existing.filenamePattern === newAssociation.filenamePattern) { - currentAssociations.splice(i, 1, newAssociation); - this.configurationService.updateValue(editorsAssociationsSettingId, currentAssociations); + private convertOldAssociationFormat(): void { + this.hostService.hadLastFocus().then(hadLastFocus => { + if (!hadLastFocus) { + return; + } + const rawAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || {}; + // If it's not an array, then it's the new format + if (!Array.isArray(rawAssociations)) { return; } + let newSettingObject = Object.create(null); + // Make the correctly formatted object from the array and then set that object + for (const association of rawAssociations) { + if (association.filenamePattern) { + newSettingObject[association.filenamePattern] = association.viewType; + } + } + this.logService.info(`Migrating ${editorsAssociationsSettingId}`); + this.configurationService.updateValue(editorsAssociationsSettingId, newSettingObject); + }); + } + + private getAllUserAssociations(): EditorAssociations { + let rawAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || {}; + + // If it's an array then it is old format + if (Array.isArray(rawAssociations)) { + // Make the correctly formatted object + const newValue = Object.create(null); + for (const association of rawAssociations) { + if (association.filenamePattern) { + newValue[association.filenamePattern] = association.viewType; + } + } + rawAssociations = newValue; } - // Otherwise, create a new one - currentAssociations.unshift(newAssociation); - this.configurationService.updateValue(editorsAssociationsSettingId, currentAssociations); + let associations = []; + for (const [key, value] of Object.entries(rawAssociations)) { + const association: EditorAssociation = { + filenamePattern: key, + viewType: value + }; + associations.push(association); + } + return associations; + } + + /** + * Returns all contributions as an array. Possible to contain duplicates + */ + private get _allContributions(): ContributionPoints { + return flatten(Array.from(this._contributionPoints.values())); + } + + updateUserAssociations(globPattern: string, editorID: string): void { + const newAssociation: EditorAssociation = { viewType: editorID, filenamePattern: globPattern }; + const currentAssociations = this.getAllUserAssociations(); + const newSettingObject = Object.create(null); + // Form the new setting object including the newest associations + for (const association of [...currentAssociations, newAssociation]) { + if (association.filenamePattern) { + newSettingObject[association.filenamePattern] = association.viewType; + } + } + this.configurationService.updateValue(editorsAssociationsSettingId, newSettingObject); } private findMatchingContributions(resource: URI): ContributionPoint[] { + // The user setting should be respected even if the editor doesn't specify that resource in package.json + const userSettings = this.getAssociationsForResource(resource); let contributions: ContributionPoint[] = []; // Then all glob patterns - for (const key of this._contributionPoints.keys()) { - const contributionPoints = this._contributionPoints.get(key)!; + for (const [key, contributionPoints] of this._contributionPoints) { for (const contributionPoint of contributionPoints) { - if (globMatchesResource(key, resource)) { + const foundInSettings = userSettings.find(setting => setting.viewType === contributionPoint.editorInfo.id); + if (foundInSettings || globMatchesResource(key, resource)) { contributions.push(contributionPoint); } } @@ -199,6 +267,11 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return contributions.sort((a, b) => priorityToRank(b.editorInfo.priority) - priorityToRank(a.editorInfo.priority)); } + public getEditorIds(resource: URI): string[] { + const contributionPoints = this.findMatchingContributions(resource); + return contributionPoints.map(contribution => contribution.editorInfo.id); + } + /** * Given a resource and an override selects the best possible contribution point * @returns The contribution point and whether there was another default which conflicted with it @@ -214,7 +287,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }; if (override) { // Specific overried passed in doesn't have to match the reosurce, it can be anything - const contributionPoints = flatten(Array.from(this._contributionPoints.values())); + const contributionPoints = this._allContributions; return { contributionPoint: findMatchingContribPoint(contributionPoints, override), conflictingDefault: false @@ -242,7 +315,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }; } - private async doOverrideEditorInput(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, selectedContribution: ContributionPoint): Promise { + private async doOverrideEditorInput(resource: URI, editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup, selectedContribution: ContributionPoint): Promise { // If no activation option is provided, populate it. if (options && typeof options.activation === 'undefined') { @@ -258,9 +331,6 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return inputWithOptions; } - // We only call this function from one place and there we do the check to ensure editor.resource is not undefined - const resource = editor.resource!; - // Respect options passed back const inputWithOptions = selectedContribution.createEditorInput(resource, options, group); options = inputWithOptions.options ?? options; @@ -325,16 +395,27 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return out; } - private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - const makeCurrentEditorDefault = () => { - const viewType = currentEditor.viewType; - if (viewType) { - this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); - } + private async doHandleConflictingDefaults(resource: URI, editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { + type StoredChoice = { + [key: string]: string[]; + }; + const contributionPoints = this.findMatchingContributions(resource); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const globForResource = `*${extname(resource)}`; + // Writes to the storage service that a choice has been made for the currently installed editors + const writeCurrentEditorsToStorage = () => { + storedChoices[globForResource] = []; + contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); + this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); }; + // If the user has already made a choice for this editor we don't want to ask them again + if (storedChoices[globForResource] && storedChoices[globForResource].find(editorID => editorID === currentEditor.viewType)) { + return; + } + const handle = this.notificationService.prompt(Severity.Warning, - localize('editorOverride.conflictingDefaults', 'Multiple editors want to be your default editor for this resource.'), + localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), [{ label: localize('editorOverride.configureDefault', 'Configure Default'), run: async () => { @@ -359,21 +440,23 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: makeCurrentEditorDefault + run: writeCurrentEditorsToStorage } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - makeCurrentEditorDefault(); + writeCurrentEditorsToStorage(); onCloseListener.dispose(); }); } - private mapContributionsToQuickPickEntry(resource: URI, group: IEditorGroup, alwaysUpdateSetting?: boolean) { + private mapContributionsToQuickPickEntry(resource: URI, group: IEditorGroup, showDefaultPicker?: boolean) { const currentEditor = firstOrDefault(group.findEditors(resource)); // If untitled, we want all contribution points - let contributionPoints = resource.scheme === Schemas.untitled ? distinct(flatten(Array.from(this._contributionPoints.values())), (contrib) => contrib.editorInfo.id) : this.findMatchingContributions(resource); - + let contributionPoints = resource.scheme === Schemas.untitled ? this._allContributions : this.findMatchingContributions(resource); + // We don't want duplicate Id entries + contributionPoints = distinct(contributionPoints, c => c.editorInfo.id); + const defaultSetting = this.getAssociationsForResource(resource)[0]?.viewType; // Not the most efficient way to do this, but we want to ensure the text editor is at the top of the quickpick contributionPoints = contributionPoints.sort((a, b) => { if (a.editorInfo.id === DEFAULT_EDITOR_ASSOCIATION.id) { @@ -384,37 +467,43 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return priorityToRank(b.editorInfo.priority) - priorityToRank(a.editorInfo.priority); } }); - const contribGroups: { defaults: Array, optional: Array } = { - defaults: [ - { type: 'separator', label: localize('editorOverride.picker.default', 'Defaults') } - ], - optional: [ - { type: 'separator', label: localize('editorOverride.picker.optional', 'Optional') } - ], - }; - // Get the matching contribtuions and call resolve whether they're active for the picker + const quickPickEntries: Array = []; + const currentlyActiveLabel = localize('promptOpenWith.currentlyActive', "Active"); + const currentDefaultLabel = localize('promptOpenWith.currentDefault', "Default"); + const currentDefaultAndActiveLabel = localize('promptOpenWith.currentDefaultAndActive', "Active and Default"); + // Default order = setting -> highest priority -> text + let defaultViewType = defaultSetting; + if (!defaultViewType && contributionPoints.length > 2 && contributionPoints[1]?.editorInfo.priority !== ContributedEditorPriority.option) { + defaultViewType = contributionPoints[1]?.editorInfo.id; + } + if (!defaultViewType) { + defaultViewType = DEFAULT_EDITOR_ASSOCIATION.id; + } + // Map the contributions to quickpick entries contributionPoints.forEach(contribPoint => { const isActive = currentEditor ? contribPoint.editorInfo.describes(currentEditor) : false; - const quickPickEntry = { + const isDefault = contribPoint.editorInfo.id === defaultViewType; + const quickPickEntry: IQuickPickItem = { id: contribPoint.editorInfo.id, label: contribPoint.editorInfo.label, - description: isActive ? localize('promptOpenWith.currentlyActive', "Currently Active") : undefined, + description: isActive && isDefault ? currentDefaultAndActiveLabel : isActive ? currentlyActiveLabel : isDefault ? currentDefaultLabel : undefined, detail: contribPoint.editorInfo.detail ?? contribPoint.editorInfo.priority, - buttons: alwaysUpdateSetting ? [] : [{ - iconClass: Codicon.gear.classNames, - tooltip: localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", extname(resource)) - }], }; - if (contribPoint.editorInfo.priority === ContributedEditorPriority.option) { - contribGroups.optional.push(quickPickEntry); - } else { - contribGroups.defaults.push(quickPickEntry); - } + quickPickEntries.push(quickPickEntry); }); - return [...contribGroups.defaults, ...contribGroups.optional]; + if (!showDefaultPicker) { + const separator: IQuickPickSeparator = { type: 'separator' }; + quickPickEntries.push(separator); + const configureDefaultEntry = { + id: EditorOverrideService.configureDefaultID, + label: localize('promptOpenWith.configureDefault', "Configure default editor for '{0}'...", `*${extname(resource)}`), + }; + quickPickEntries.push(configureDefaultEntry); + } + return quickPickEntries; } - private async doPickEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup, alwaysUpdateSetting?: boolean): Promise<[IEditorOptions, IEditorGroup | undefined] | undefined> { + private async doPickEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup, showDefaultPicker?: boolean): Promise<[IEditorOptions, IEditorGroup | undefined] | undefined> { type EditorOverridePick = { readonly item: IQuickPickItem; @@ -429,12 +518,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Text editor has the lowest priority because we - const editorOverridePicks = this.mapContributionsToQuickPickEntry(resource, group, alwaysUpdateSetting); + const editorOverridePicks = this.mapContributionsToQuickPickEntry(resource, group, showDefaultPicker); // Create editor override picker const editorOverridePicker = this.quickInputService.createQuickPick(); - const placeHolderMessage = alwaysUpdateSetting ? - localize('prompOpenWith.updateDefaultPlaceHolder', "Select new default editor for '{0}'", basename(resource)) : + const placeHolderMessage = showDefaultPicker ? + localize('prompOpenWith.updateDefaultPlaceHolder', "Select new default editor for '{0}'", `*${extname(resource)}`) : localize('promptOpenWith.placeHolder', "Select editor for '{0}'", basename(resource)); editorOverridePicker.placeholder = placeHolderMessage; editorOverridePicker.canAcceptInBackground = true; @@ -458,7 +547,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // If asked to always update the setting then update it even if the gear isn't clicked - if (alwaysUpdateSetting && result?.item.id) { + if (showDefaultPicker && result?.item.id) { this.updateUserAssociations(`*${extname(resource)}`, result.item.id,); } @@ -487,6 +576,11 @@ export class EditorOverrideService extends Disposable implements IEditorOverride // options and group to use accordingly if (picked) { + // If the user selected to configure default we trigger this picker again and tell it to show the default picker + if (picked.item.id === EditorOverrideService.configureDefaultID) { + return this.doPickEditorOverride(editor, options, group, true); + } + // Figure out target group let targetGroup: IEditorGroup | undefined; if (picked.keyMods?.alt || picked.keyMods?.ctrlCmd) { @@ -525,8 +619,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride const cacheStorage: Set = new Set(); // Store just the relative pattern pieces without any path info - for (const globPattern of this._contributionPoints.keys()) { - const contribPoint = this._contributionPoints.get(globPattern)!; + for (const [globPattern, contribPoint] of this._contributionPoints) { const nonOptional = !!contribPoint.find(c => c.editorInfo.priority !== ContributedEditorPriority.option && c.editorInfo.id !== DEFAULT_EDITOR_ASSOCIATION.id); // Don't keep a cache of the optional ones as those wouldn't be opened on start anyways if (!nonOptional) { @@ -540,7 +633,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } // Also store the users settings as those would have to activate on startup as well - const userAssociations = this.configurationService.getValue(editorsAssociationsSettingId) || []; + const userAssociations = this.getAllUserAssociations(); for (const association of userAssociations) { if (association.filenamePattern) { cacheStorage.add(association.filenamePattern); diff --git a/lib/vscode/src/vs/workbench/services/editor/browser/editorService.ts b/lib/vscode/src/vs/workbench/services/editor/browser/editorService.ts index 728db06f1cbe..8e03b938fe16 100644 --- a/lib/vscode/src/vs/workbench/services/editor/browser/editorService.ts +++ b/lib/vscode/src/vs/workbench/services/editor/browser/editorService.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IResourceEditorInput, ITextEditorOptions, IEditorOptions, EditorActivation, EditorOverride, IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; -import { SideBySideEditor, IEditorInput, IEditorPane, GroupIdentifier, IFileEditorInput, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputFactoryRegistry, EditorExtensions, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditorPane, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, isTextEditorPane, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { IResourceEditorInput, IEditorOptions, EditorActivation, EditorOverride, IResourceEditorInputIdentifier, ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { SideBySideEditor, IEditorInput, IEditorPane, GroupIdentifier, IFileEditorInput, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputFactoryRegistry, EditorExtensions, IEditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditorPane, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, isTextEditorPane, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, IEditorInputWithOptionsAndGroup, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { ResourceMap } from 'vs/base/common/map'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; @@ -14,30 +16,30 @@ import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, File import { Schemas } from 'vs/base/common/network'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { basename, joinPath, isEqual } from 'vs/base/common/resources'; +import { basename, joinPath } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; +import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { coalesce, distinct, firstOrDefault, insert } from 'vs/base/common/arrays'; +import { Disposable, IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { coalesce, distinct } from 'vs/base/common/arrays'; import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupView, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { isUndefined, withNullAsUndefined } from 'vs/base/common/types'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { Promises, timeout } from 'vs/base/common/async'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { indexOfPath } from 'vs/base/common/extpath'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ILogService } from 'vs/platform/log/common/log'; import { ContributedEditorPriority, DEFAULT_EDITOR_ASSOCIATION, IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; -type CachedEditorInput = ResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; +type CachedEditorInput = TextResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; export class EditorService extends Disposable implements EditorServiceImpl { @@ -73,8 +75,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @ILogService private readonly logService: ILogService, @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IHostService private readonly hostService: IHostService, ) { super(); @@ -110,6 +114,22 @@ export class EditorService extends Disposable implements EditorServiceImpl { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); } + private registerDefaultOverride(): void { + this._register(this.editorOverrideService.registerContributionPoint( + '*', + { + id: DEFAULT_EDITOR_ASSOCIATION.id, + label: DEFAULT_EDITOR_ASSOCIATION.displayName, + detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, + describes: (currentEditor) => this.fileEditorInputFactory.isFileEditorInput(currentEditor) && currentEditor.matches(this.activeEditor), + priority: ContributedEditorPriority.builtin + }, + {}, + resource => ({ editor: this.createEditorInput({ resource }) }), + diffEditor => ({ editor: diffEditor }) + )); + } + //#region Editor & group event handlers private lastActiveEditor: IEditorInput | undefined = undefined; @@ -284,7 +304,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { replacement: { ...moveResult.editor, options: { - ...moveResult.editor.options, + ...(moveResult.editor as IResourceEditorInputType).options, ...optionOverrides } } @@ -318,10 +338,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Handle deletes in opened editors depending on: - // - the user has not disabled the setting closeOnFileDelete - // - the file change is local - // - the input is a file that is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents) - if (this.closeOnFileDelete || !isExternal || (this.fileEditorInputFactory.isFileEditorInput(editor) && !editor.isResolved())) { + // - we close any editor when `closeOnFileDelete: true` + // - we close any editor when the delete occurred from within VSCode + // - we close any editor without resolved working copy assuming that + // this editor could not be opened after the file is gone + if (this.closeOnFileDelete || !isExternal || !this.workingCopyService.has(resource)) { // Do NOT close any opened editor that matches the resource path (either equal or being parent) of the // resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same @@ -365,7 +386,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { const editors: IEditorInput[] = []; function conditionallyAddEditor(editor: IEditorInput): void { - if (editor.isUntitled() && !options.includeUntitled) { + if (editor.hasCapability(EditorInputCapabilities.Untitled) && !options.includeUntitled) { return; } @@ -485,164 +506,48 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion - //#region editor overrides - - private readonly openEditorOverrides: IOpenEditorOverrideHandler[] = []; - - overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { - const remove = insert(this.openEditorOverrides, handler); - - return toDisposable(() => remove()); - } - - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][] { - const overrides: [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][] = []; - - // Collect contributed editor open overrides - for (const openEditorOverride of this.openEditorOverrides) { - if (typeof openEditorOverride.getEditorOverrides === 'function') { - try { - overrides.push(...openEditorOverride.getEditorOverrides(resource, options, group).map(val => [openEditorOverride, val] as [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry])); - } catch (error) { - this.logService.error(`Unexpected error getting editor overrides: ${error}`); - } - } - } - - // Ensure the default one is always present - if (!overrides.some(([, entry]) => entry.id === DEFAULT_EDITOR_ASSOCIATION.id)) { - overrides.unshift(this.getDefaultEditorOverride(resource)); - } - - return overrides; - } - - private registerDefaultOverride(): void { - this._register(this.editorOverrideService.registerContributionPoint( - '*', - { - id: DEFAULT_EDITOR_ASSOCIATION.id, - label: DEFAULT_EDITOR_ASSOCIATION.displayName, - detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, - describes: (currentEditor) => this.fileEditorInputFactory.isFileEditorInput(currentEditor) && currentEditor.matches(this.activeEditor), - priority: ContributedEditorPriority.builtin - }, - {}, - resource => ({ editor: this.createEditorInput({ resource }) }), - diffEditor => ({ editor: diffEditor }) - )); - } - - private getDefaultEditorOverride(resource: URI): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry] { - return [ - { - open: (editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => { - const resource = EditorResourceAccessor.getOriginalUri(editor); - if (!resource) { - return; - } - - const fileEditorInput = this.createEditorInput({ resource, forceFile: true }); - const textOptions: IEditorOptions | ITextEditorOptions = { ...options, override: EditorOverride.DISABLED }; - return { - override: (async () => { - - // Try to replace existing editors for resource - const existingEditor = firstOrDefault(this.findEditors(resource, group)); - if (existingEditor && !fileEditorInput.matches(existingEditor)) { - await this.replaceEditors([{ - editor: existingEditor, - replacement: fileEditorInput, - forceReplaceDirty: existingEditor.resource?.scheme === Schemas.untitled, - options: options ? EditorOptions.create(options) : undefined, - }], group); - } - - return this.openEditor(fileEditorInput, textOptions, group); - })() - }; - } - }, - { - id: DEFAULT_EDITOR_ASSOCIATION.id, - label: DEFAULT_EDITOR_ASSOCIATION.displayName, - detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, - active: this.fileEditorInputFactory.isFileEditorInput(this.activeEditor) && isEqual(this.activeEditor.resource, resource), - } - ]; - } - - private doOverrideOpenEditor(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup): Promise | undefined { - for (const openEditorOverride of this.openEditorOverrides) { - const result = openEditorOverride.open(editor, options, group); - const override = result?.override; - if (override) { - return override; - } - } - - return; - } - - //#endregion - //#region openEditor() - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: OpenInEditorGroup): Promise; - async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { + async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { const result = this.doResolveEditorOpenRequest(editor, optionsOrGroup, group); if (result) { const [resolvedGroup, resolvedEditor, resolvedOptions] = result; - // Override handling: pick editor or open specific - if (resolvedOptions?.override === EditorOverride.PICK || typeof resolvedOptions?.override === 'string') { - const resolvedInputWithOptionsAndGroup = await this.editorOverrideService.resolveEditorOverride(resolvedEditor, resolvedOptions, resolvedGroup); - if (!resolvedInputWithOptionsAndGroup) { - return undefined; // no editor was picked or registered for the identifier - } - - return (resolvedInputWithOptionsAndGroup.group ?? resolvedGroup).openEditor(resolvedInputWithOptionsAndGroup.editor, resolvedInputWithOptionsAndGroup.options ?? resolvedOptions); - } - - // Override handling: ask providers to override + // Override handling: request override from override service if (resolvedOptions?.override !== EditorOverride.DISABLED) { - // TODO@lramos15 this will get cleaned up soon, but since the override - // service no longer uses the override flow we must check that const resolvedInputWithOptionsAndGroup = await this.editorOverrideService.resolveEditorOverride(resolvedEditor, resolvedOptions, resolvedGroup); - // If we didn't override try the legacy overrides - if (!resolvedInputWithOptionsAndGroup || resolvedEditor.matches(resolvedInputWithOptionsAndGroup.editor)) { - const override = this.doOverrideOpenEditor(resolvedEditor, resolvedOptions, resolvedGroup); - if (override) { - return override; - } - } else { - return (resolvedInputWithOptionsAndGroup.group ?? resolvedGroup).openEditor(resolvedInputWithOptionsAndGroup.editor, resolvedInputWithOptionsAndGroup.options ?? resolvedOptions); + if (resolvedInputWithOptionsAndGroup) { + return (resolvedInputWithOptionsAndGroup.group ?? resolvedGroup).openEditor( + resolvedInputWithOptionsAndGroup.editor, + resolvedInputWithOptionsAndGroup.options ?? resolvedOptions + ); } } - // Override handling: disabled + // Override handling: disabled or no override found return resolvedGroup.openEditor(resolvedEditor, resolvedOptions); } return undefined; } - doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined { + doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, IEditorOptions | undefined] | undefined { let resolvedGroup: IEditorGroup | undefined; let candidateGroup: OpenInEditorGroup | undefined; let typedEditor: EditorInput | undefined; - let typedOptions: EditorOptions | undefined; + let options: IEditorOptions | undefined; // Typed Editor Support if (editor instanceof EditorInput) { typedEditor = editor; - typedOptions = this.toOptions(optionsOrGroup as IEditorOptions); + options = optionsOrGroup as IEditorOptions; candidateGroup = group; - resolvedGroup = this.findTargetGroup(typedEditor, typedOptions, candidateGroup); + resolvedGroup = this.findTargetGroup(typedEditor, options, candidateGroup); } // Untyped Text Editor Support @@ -650,19 +555,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { const textInput = editor as IResourceEditorInputType; typedEditor = this.createEditorInput(textInput); if (typedEditor) { - typedOptions = TextEditorOptions.from(textInput); + options = textInput.options; candidateGroup = optionsOrGroup as OpenInEditorGroup; - resolvedGroup = this.findTargetGroup(typedEditor, typedOptions, candidateGroup); + resolvedGroup = this.findTargetGroup(typedEditor, options, candidateGroup); } } if (typedEditor && resolvedGroup) { if ( this.editorGroupService.activeGroup !== resolvedGroup && // only if target group is not already active - typedOptions && !typedOptions.inactive && // never for inactive editors - typedOptions.preserveFocus && // only if preserveFocus - typeof typedOptions.activation !== 'number' && // only if activation is not already defined (either true or false) + options && !options.inactive && // never for inactive editors + options.preserveFocus && // only if preserveFocus + typeof options.activation !== 'number' && // only if activation is not already defined (either true or false) candidateGroup !== SIDE_GROUP // never for the SIDE_GROUP ) { // If the resolved group is not the active one, we typically @@ -674,10 +579,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { // group is it is opened as `SIDE_GROUP` with `preserveFocus:true`. // repeated Alt-clicking of files in the explorer always open // into the same side group and not cause a group to be created each time. - typedOptions.overwrite({ activation: EditorActivation.ACTIVATE }); + options.activation = EditorActivation.ACTIVATE; } - return [resolvedGroup, typedEditor, typedOptions]; + return [resolvedGroup, typedEditor, options]; } return undefined; @@ -763,26 +668,23 @@ export class EditorService extends Disposable implements EditorServiceImpl { return neighbourGroup; } - private toOptions(options?: IEditorOptions | ITextEditorOptions | EditorOptions): EditorOptions { - if (!options || options instanceof EditorOptions) { - return options as EditorOptions; - } - - const textOptions: ITextEditorOptions = options; - if (textOptions.selection || textOptions.viewState) { - return TextEditorOptions.create(options); - } - - return EditorOptions.create(options); - } - //#endregion //#region openEditors() - openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; - openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; - async openEditors(editors: Array, group?: OpenInEditorGroup): Promise { + openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + async openEditors(editors: Array, group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise { + + // Pass all editors to trust service to determine if + // we should proceed with opening the editors if we + // are asked to validate trust. + if (options?.validateTrust) { + const editorsTrusted = await this.handleWorkspaceTrust(editors); + if (!editorsTrusted) { + return []; + } + } // Convert to typed editors and options const typedEditors: IEditorInputWithOptions[] = editors.map(editor => { @@ -792,7 +694,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { return { editor: this.createEditorInput(editor), - options: TextEditorOptions.from(editor) + options: editor.options }; }); @@ -830,7 +732,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { mapGroupToEditors.set(targetGroup, targetGroupEditors); } - targetGroupEditors.push(editorOverride ?? { editor, options }); + targetGroupEditors.push(editorOverride ? + { editor: editorOverride.editor, options: editorOverride.options } : + { editor, options } + ); } } @@ -843,13 +748,82 @@ export class EditorService extends Disposable implements EditorServiceImpl { return coalesce(await Promises.settled(result)); } + private async handleWorkspaceTrust(editors: Array): Promise { + const { resources, diffMode } = this.extractEditorResources(editors); + + const trustResult = await this.workspaceTrustRequestService.requestOpenUris(resources); + switch (trustResult) { + case WorkspaceTrustUriResponse.Open: + return true; + case WorkspaceTrustUriResponse.OpenInNewWindow: + await this.hostService.openWindow(resources.map(resource => ({ fileUri: resource })), { forceNewWindow: true, diffMode }); + return false; + case WorkspaceTrustUriResponse.Cancel: + return false; + } + } + + private extractEditorResources(editors: Array): { resources: URI[], diffMode?: boolean } { + const resources = new ResourceMap(); + let diffMode = false; + + for (const editor of editors) { + + // Typed Editor + if (isEditorInputWithOptions(editor)) { + const resource = EditorResourceAccessor.getOriginalUri(editor.editor, { supportSideBySide: SideBySideEditor.BOTH }); + if (URI.isUri(resource)) { + resources.set(resource, true); + } else if (resource) { + if (resource.primary) { + resources.set(resource.primary, true); + } + + if (resource.secondary) { + resources.set(resource.secondary, true); + } + + diffMode = editor.editor instanceof DiffEditorInput; + } + } + + // Untyped editor + else { + const resourceDiffEditor = editor as IResourceDiffEditorInput; + if (resourceDiffEditor.originalInput && resourceDiffEditor.modifiedInput) { + const originalResourceEditor = resourceDiffEditor.originalInput as IResourceEditorInput; + if (URI.isUri(originalResourceEditor.resource)) { + resources.set(originalResourceEditor.resource, true); + } + + const modifiedResourceEditor = resourceDiffEditor.modifiedInput as IResourceEditorInput; + if (URI.isUri(modifiedResourceEditor.resource)) { + resources.set(modifiedResourceEditor.resource, true); + } + + diffMode = true; + } + + const resourceEditor = editor as IResourceEditorInput; + if (URI.isUri(resourceEditor.resource)) { + resources.set(resourceEditor.resource, true); + } + } + } + + return { + resources: Array.from(resources.keys()), + diffMode + }; + } + //#endregion //#region isOpened() isOpened(editor: IResourceEditorInputIdentifier): boolean { return this.editorsObserver.hasEditor({ - resource: this.asCanonicalEditorResource(editor.resource), + resource: this.uriIdentityService.asCanonicalUri(editor.resource), typeId: editor.typeId }); } @@ -956,7 +930,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { editor: replacementArg.editor, replacement: replacementArg.replacement, forceReplaceDirty: replacementArg.forceReplaceDirty, - options: this.toOptions(replacementArg.options) + options: replacementArg.options }); } else { const replacementArg = replaceEditorArg as IResourceEditorReplacement; @@ -964,7 +938,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { typedEditors.push({ editor: this.createEditorInput(replacementArg.editor), replacement: this.createEditorInput(replacementArg.replacement), - options: this.toOptions(replacementArg.replacement.options) + options: replacementArg.replacement.options }); } } @@ -995,22 +969,22 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Diff Editor Support const resourceDiffInput = input as IResourceDiffEditorInput; - if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { - const leftInput = this.createEditorInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile }); - const rightInput = this.createEditorInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile }); + if (resourceDiffInput.originalInput && resourceDiffInput.modifiedInput) { + const originalInput = this.createEditorInput({ ...resourceDiffInput.originalInput, forceFile: resourceDiffInput.forceFile }); + const modifiedInput = this.createEditorInput({ ...resourceDiffInput.modifiedInput, forceFile: resourceDiffInput.forceFile }); return this.instantiationService.createInstance(DiffEditorInput, resourceDiffInput.label, resourceDiffInput.description, - leftInput, - rightInput, + originalInput, + modifiedInput, undefined ); } // Untitled file support const untitledInput = input as IUntitledTextResourceEditorInput; - if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) { + if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource.scheme === Schemas.untitled)) { const untitledOptions = { mode: untitledInput.mode, initialValue: untitledInput.contents, @@ -1044,31 +1018,31 @@ export class EditorService extends Disposable implements EditorServiceImpl { }) as EditorInput; } - // Resource Editor Support - const resourceEditorInput = input as IResourceEditorInput; - if (resourceEditorInput.resource instanceof URI) { + // Text Resource Editor Support + const textResourceEditorInput = input as ITextResourceEditorInput; + if (textResourceEditorInput.resource instanceof URI) { // Derive the label from the path if not provided explicitly - const label = resourceEditorInput.label || basename(resourceEditorInput.resource); + const label = textResourceEditorInput.label || basename(textResourceEditorInput.resource); // We keep track of the preferred resource this input is to be created // with but it may be different from the canonical resource (see below) - const preferredResource = resourceEditorInput.resource; + const preferredResource = textResourceEditorInput.resource; // From this moment on, only operate on the canonical resource // to ensure we reduce the chance of opening the same resource // with different resource forms (e.g. path casing on Windows) - const canonicalResource = this.asCanonicalEditorResource(preferredResource); + const canonicalResource = this.uriIdentityService.asCanonicalUri(preferredResource); return this.createOrGetCached(canonicalResource, () => { // File - if (resourceEditorInput.forceFile || this.fileService.canHandleResource(canonicalResource)) { - return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService); + if (textResourceEditorInput.forceFile || this.fileService.canHandleResource(canonicalResource)) { + return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, textResourceEditorInput.label, textResourceEditorInput.description, textResourceEditorInput.encoding, textResourceEditorInput.mode, textResourceEditorInput.contents, this.instantiationService); } // Resource - return this.instantiationService.createInstance(ResourceEditorInput, canonicalResource, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.mode); + return this.instantiationService.createInstance(TextResourceEditorInput, canonicalResource, textResourceEditorInput.label, textResourceEditorInput.description, textResourceEditorInput.mode, textResourceEditorInput.contents); }, cachedInput => { // Untitled @@ -1077,23 +1051,27 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Files - else if (!(cachedInput instanceof ResourceEditorInput)) { + else if (!(cachedInput instanceof TextResourceEditorInput)) { cachedInput.setPreferredResource(preferredResource); - if (resourceEditorInput.label) { - cachedInput.setPreferredName(resourceEditorInput.label); + if (textResourceEditorInput.label) { + cachedInput.setPreferredName(textResourceEditorInput.label); } - if (resourceEditorInput.description) { - cachedInput.setPreferredDescription(resourceEditorInput.description); + if (textResourceEditorInput.description) { + cachedInput.setPreferredDescription(textResourceEditorInput.description); } - if (resourceEditorInput.encoding) { - cachedInput.setPreferredEncoding(resourceEditorInput.encoding); + if (textResourceEditorInput.encoding) { + cachedInput.setPreferredEncoding(textResourceEditorInput.encoding); } - if (resourceEditorInput.mode) { - cachedInput.setPreferredMode(resourceEditorInput.mode); + if (textResourceEditorInput.mode) { + cachedInput.setPreferredMode(textResourceEditorInput.mode); + } + + if (typeof textResourceEditorInput.contents === 'string') { + cachedInput.setPreferredContents(textResourceEditorInput.contents); } } @@ -1103,12 +1081,16 @@ export class EditorService extends Disposable implements EditorServiceImpl { cachedInput.setName(label); } - if (resourceEditorInput.description) { - cachedInput.setDescription(resourceEditorInput.description); + if (textResourceEditorInput.description) { + cachedInput.setDescription(textResourceEditorInput.description); + } + + if (textResourceEditorInput.mode) { + cachedInput.setPreferredMode(textResourceEditorInput.mode); } - if (resourceEditorInput.mode) { - cachedInput.setPreferredMode(resourceEditorInput.mode); + if (typeof textResourceEditorInput.contents === 'string') { + cachedInput.setPreferredContents(textResourceEditorInput.contents); } } }) as EditorInput; @@ -1117,28 +1099,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { throw new Error('Unknown input type'); } - private _modelService: IModelService | undefined = undefined; - private get modelService(): IModelService | undefined { - if (!this._modelService) { - this._modelService = this.instantiationService.invokeFunction(accessor => accessor.get(IModelService)); - } - - return this._modelService; - } - - private asCanonicalEditorResource(resource: URI): URI { - const canonicalResource: URI = this.uriIdentityService.asCanonicalUri(resource); - - // In the unlikely case that a model exists for the original resource but - // differs from the canonical resource, we print a warning as this means - // the model will not be able to be opened as editor. - if (!isEqual(resource, canonicalResource) && this.modelService?.getModel(resource)) { - this.logService.warn(`EditorService: a model exists for a resource that is not canonical: ${resource.toString(true)}`); - } - - return canonicalResource; - } - private createOrGetCached(resource: URI, factoryFn: () => CachedEditorInput, cachedFn?: (input: CachedEditorInput) => void): CachedEditorInput { // Return early if already cached @@ -1185,7 +1145,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { editorsToSaveSequentially.push(...uniqueEditors); } else { for (const { groupId, editor } of uniqueEditors) { - if (editor.isUntitled()) { + if (editor.hasCapability(EditorInputCapabilities.Untitled)) { editorsToSaveSequentially.push({ groupId, editor }); } else { editorsToSaveParallel.push({ groupId, editor }); @@ -1214,11 +1174,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Preserve view state by opening the editor first if the editor // is untitled or we "Save As". This also allows the user to review // the contents of the editor before making a decision. - let viewState: IEditorViewState | undefined = undefined; const editorPane = await this.openEditor(editor, undefined, groupId); - if (isTextEditorPane(editorPane)) { - viewState = editorPane.getViewState(); - } + const editorOptions: ITextEditorOptions = { + pinned: true, + viewState: isTextEditorPane(editorPane) ? editorPane.getViewState() : undefined + }; const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options); saveResults.push(result); @@ -1231,9 +1191,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { // only selected group) if the resulting editor is different from the // current one. if (!result.matches(editor)) { - const targetGroups = editor.isUntitled() ? this.editorGroupService.groups.map(group => group.id) /* untitled replaces across all groups */ : [groupId]; + const targetGroups = editor.hasCapability(EditorInputCapabilities.Untitled) ? this.editorGroupService.groups.map(group => group.id) /* untitled replaces across all groups */ : [groupId]; for (const group of targetGroups) { - await this.replaceEditors([{ editor, replacement: result, options: { pinned: true, viewState } }], group); + await this.replaceEditors([{ editor, replacement: result, options: editorOptions }], group); } } } @@ -1280,7 +1240,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { continue; } - if (!options?.includeUntitled && editor.isUntitled()) { + if (!options?.includeUntitled && editor.hasCapability(EditorInputCapabilities.Untitled)) { continue; } @@ -1340,10 +1300,10 @@ export class DelegatingEditorService implements IEditorService { @IEditorService private editorService: EditorService ) { } - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: OpenInEditorGroup): Promise; - async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { + async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { const result = this.editorService.doResolveEditorOpenRequest(editor, optionsOrGroup, group); if (result) { const [resolvedGroup, resolvedEditor, resolvedOptions] = result; @@ -1372,10 +1332,10 @@ export class DelegatingEditorService implements IEditorService { getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly IEditorIdentifier[] { return this.editorService.getEditors(order, options); } - openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; - openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; - openEditors(editors: Array, group?: OpenInEditorGroup): Promise { - return this.editorService.openEditors(editors, group); + openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise; + openEditors(editors: Array, group?: OpenInEditorGroup, options?: IOpenEditorsOptions): Promise { + return this.editorService.openEditors(editors, group, options); } replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; @@ -1392,9 +1352,6 @@ export class DelegatingEditorService implements IEditorService { findEditors(resource: IResourceEditorInputIdentifier, group: IEditorGroup | GroupIdentifier): IEditorInput | undefined; findEditors(arg1: URI | IResourceEditorInputIdentifier, arg2?: IEditorGroup | GroupIdentifier): readonly IEditorIdentifier[] | readonly IEditorInput[] | IEditorInput | undefined { return this.editorService.findEditors(arg1, arg2); } - overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); } - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) { return this.editorService.getEditorOverrides(resource, options, group); } - createEditorInput(input: IResourceEditorInputType): IEditorInput { return this.editorService.createEditorInput(input); } save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { return this.editorService.save(editors, options); } diff --git a/lib/vscode/src/vs/workbench/services/editor/common/editorGroupsService.ts b/lib/vscode/src/vs/workbench/services/editor/common/editorGroupsService.ts index f5ebf2884738..f7283b6c246a 100644 --- a/lib/vscode/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/lib/vscode/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -5,8 +5,8 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IEditorMoveEvent } from 'vs/workbench/common/editor'; -import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IEditorMoveEvent, IEditorOpenEvent } from 'vs/workbench/common/editor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDimension } from 'vs/editor/common/editorCommon'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -101,7 +101,7 @@ export interface ICloseAllEditorsOptions { export interface IEditorReplacement { editor: IEditorInput; replacement: IEditorInput; - options?: IEditorOptions | ITextEditorOptions; + options?: IEditorOptions; /** * Skips asking the user for confirmation and doesn't @@ -217,11 +217,6 @@ export interface IEditorGroupsService { */ readonly whenRestored: Promise; - /** - * Will return `true` as soon as `whenRestored` is resolved. - */ - isRestored(): boolean; - /** * Find out if the editor group service has UI state to restore * from a previous session. @@ -377,6 +372,7 @@ export const enum GroupChangeKind { EDITOR_MOVE, EDITOR_ACTIVE, EDITOR_LABEL, + EDITOR_CAPABILITIES, EDITOR_PIN, EDITOR_STICKY, EDITOR_DIRTY @@ -417,6 +413,12 @@ export interface IEditorGroup { */ readonly onWillMoveEditor: Event; + /** + * An event that is fired when an editor is about to be opened + * in the group. + */ + readonly onWillOpenEditor: Event; + /** * A unique identifier of this group that remains identical even if the * group is moved to different locations. @@ -519,7 +521,7 @@ export interface IEditorGroup { * @returns a promise that resolves around an IEditor instance unless * the call failed, or the editor was not opened as active editor. */ - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions): Promise; /** * Opens editors in this group. @@ -556,14 +558,14 @@ export interface IEditorGroup { /** * Move an editor from this group either within this group or to another group. */ - moveEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions | ITextEditorOptions): void; + moveEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions): void; /** * Copy an editor from this group to another group. * * Note: It is currently not supported to show the same editor more than once in the same group. */ - copyEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions | ITextEditorOptions): void; + copyEditor(editor: IEditorInput, target: IEditorGroup, options?: IEditorOptions): void; /** * Close an editor from the group. This may trigger a confirmation dialog if diff --git a/lib/vscode/src/vs/workbench/services/editor/common/editorOverrideService.ts b/lib/vscode/src/vs/workbench/services/editor/common/editorOverrideService.ts index 01bc16f3a0a6..824ea6a615fa 100644 --- a/lib/vscode/src/vs/workbench/services/editor/common/editorOverrideService.ts +++ b/lib/vscode/src/vs/workbench/services/editor/common/editorOverrideService.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; -import { Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { posix } from 'vs/base/common/path'; @@ -14,10 +12,10 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { Extensions as ConfigurationExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorExtensions, IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorInputWithOptions, IEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -44,40 +42,14 @@ export const DEFAULT_EDITOR_ASSOCIATION: IEditorType = { const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -const editorTypeSchemaAddition: IJSONSchema = { - type: 'string', - enum: [] -}; - const editorAssociationsConfigurationNode: IConfigurationNode = { ...workbenchConfigurationNodeBase, properties: { 'workbench.editorAssociations': { - type: 'array', - markdownDescription: localize('editor.editorAssociations', "Configure which editor to use for specific file types."), - items: { - type: 'object', - defaultSnippets: [{ - body: { - 'viewType': '$1', - 'filenamePattern': '$2' - } - }], - properties: { - 'viewType': { - anyOf: [ - { - type: 'string', - description: localize('editor.editorAssociations.viewType', "The unique id of the editor to use."), - }, - editorTypeSchemaAddition - ] - }, - 'filenamePattern': { - type: 'string', - description: localize('editor.editorAssociations.filenamePattern', "Glob pattern specifying which files the editor should be used for."), - } - } + type: 'object', + markdownDescription: localize('editor.editorAssociations', "Configure glob patterns to editors (e.g. `\"*.hex\": \"hexEditor.hexEdit\"`). These have precedence over the default behavior."), + additionalProperties: { + type: 'string' } } } @@ -89,68 +61,6 @@ export interface IEditorType { readonly providerDisplayName: string; } -export interface IEditorTypesHandler { - readonly onDidChangeEditorTypes: Event; - - getEditorTypes(): IEditorType[]; -} - -export interface IEditorAssociationsRegistry { - - /** - * Register handlers for editor types - */ - registerEditorTypesHandler(id: string, handler: IEditorTypesHandler): IDisposable; -} - -class EditorAssociationsRegistry implements IEditorAssociationsRegistry { - - private readonly editorTypesHandlers = new Map(); - - registerEditorTypesHandler(id: string, handler: IEditorTypesHandler): IDisposable { - if (this.editorTypesHandlers.has(id)) { - throw new Error(`An editor type handler with ${id} was already registered.`); - } - - this.editorTypesHandlers.set(id, handler); - this.updateEditorAssociationsSchema(); - - const editorTypeChangeEvent = handler.onDidChangeEditorTypes(() => { - this.updateEditorAssociationsSchema(); - }); - - return { - dispose: () => { - editorTypeChangeEvent.dispose(); - this.editorTypesHandlers.delete(id); - this.updateEditorAssociationsSchema(); - } - }; - } - - private updateEditorAssociationsSchema() { - const enumValues: string[] = []; - const enumDescriptions: string[] = []; - - const editorTypes: IEditorType[] = [DEFAULT_EDITOR_ASSOCIATION]; - - for (const [, handler] of this.editorTypesHandlers) { - editorTypes.push(...handler.getEditorTypes()); - } - - for (const { id, providerDisplayName } of editorTypes) { - enumValues.push(id); - enumDescriptions.push(localize('editorAssociations.viewType.sourceDescription', "Source: {0}", providerDisplayName)); - } - - editorTypeSchemaAddition.enum = enumValues; - editorTypeSchemaAddition.enumDescriptions = enumDescriptions; - - configurationRegistry.notifyConfigurationSchemaUpdated(editorAssociationsConfigurationNode); - } -} - -Registry.add(EditorExtensions.Associations, new EditorAssociationsRegistry()); configurationRegistry.registerConfiguration(editorAssociationsConfigurationNode); //#endregion @@ -187,9 +97,9 @@ export type ContributedEditorInfo = { priority: ContributedEditorPriority; }; -export type EditorInputFactoryFunction = (resource: URI, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; +export type EditorInputFactoryFunction = (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; -export type DiffEditorInputFactoryFunction = (diffEditorInput: DiffEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; +export type DiffEditorInputFactoryFunction = (diffEditorInput: DiffEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) => IEditorInputWithOptions; export interface IEditorOverrideService { readonly _serviceBrand: undefined; @@ -229,7 +139,14 @@ export interface IEditorOverrideService { * @param group The current group * @returns An IEditorInputWithOptionsAndGroup if there is an available override or undefined if there is not */ - resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): Promise; + resolveEditorOverride(editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup): Promise; + + /** + * Given a resource returns all the editor ids that match that resource + * @param resource The resource + * @returns A list of editor ids + */ + getEditorIds(resource: URI): string[]; } //#endregion @@ -261,6 +178,6 @@ export function globMatchesResource(globPattern: string | glob.IRelativePattern, } const matchOnPath = typeof globPattern === 'string' && globPattern.indexOf(posix.sep) >= 0; const target = matchOnPath ? `${resource.scheme}:${resource.path}` : basename(resource); - return glob.match(globPattern, target.toLowerCase()); + return glob.match(typeof globPattern === 'string' ? globPattern.toLowerCase() : globPattern, target.toLowerCase()); } //#endregion diff --git a/lib/vscode/src/vs/workbench/services/editor/common/editorService.ts b/lib/vscode/src/vs/workbench/services/editor/common/editorService.ts index c3b0d1c27e5b..61982f9c4e41 100644 --- a/lib/vscode/src/vs/workbench/services/editor/common/editorService.ts +++ b/lib/vscode/src/vs/workbench/services/editor/common/editorService.ts @@ -4,17 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IResourceEditorInput, IEditorOptions, ITextEditorOptions, IResourceEditorInputIdentifier } from 'vs/platform/editor/common/editor'; +import { IResourceEditorInput, IEditorOptions, IResourceEditorInputIdentifier, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextEditorPane, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent } from 'vs/workbench/common/editor'; import { Event } from 'vs/base/common/event'; import { IEditor, IDiffEditor } from 'vs/editor/common/editorCommon'; import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; export const IEditorService = createDecorator('editorService'); -export type IResourceEditorInputType = IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput; +export type IResourceEditorInputType = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput; export interface IResourceEditorReplacement { readonly editor: IResourceEditorInputType; @@ -27,27 +26,6 @@ export type ACTIVE_GROUP_TYPE = typeof ACTIVE_GROUP; export const SIDE_GROUP = -2; export type SIDE_GROUP_TYPE = typeof SIDE_GROUP; -export interface IOpenEditorOverrideEntry { - readonly id: string; - readonly label: string; - readonly active: boolean; - readonly detail?: string; -} - -export interface IOpenEditorOverrideHandler { - open(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined; - getEditorOverrides?(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[]; -} - -export interface IOpenEditorOverride { - - /** - * If defined, will prevent the opening of an editor and replace the resulting - * promise with the provided promise for the openEditor() call. - */ - override?: Promise; -} - export interface ISaveEditorsOptions extends ISaveOptions { /** @@ -73,6 +51,15 @@ export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRe export interface IRevertAllEditorsOptions extends IRevertOptions, IBaseSaveRevertAllEditorOptions { } +export interface IOpenEditorsOptions { + + /** + * Whether to validate trust when opening editors + * that are potentially not inside the workspace. + */ + readonly validateTrust?: boolean; +} + export interface IEditorService { readonly _serviceBrand: undefined; @@ -80,14 +67,14 @@ export interface IEditorService { /** * Emitted when the currently active editor changes. * - * @see `IEditorService.activeEditorPane` + * @see {@link IEditorService.activeEditorPane} */ readonly onDidActiveEditorChange: Event; /** * Emitted when any of the current visible editors changes. * - * @see `IEditorService.visibleEditorPanes` + * @see {@link IEditorService.visibleEditorPanes} */ readonly onDidVisibleEditorsChange: Event; @@ -100,7 +87,7 @@ export interface IEditorService { * The currently active editor pane or `undefined` if none. The editor pane is * the workbench container for editors of any kind. * - * @see `IEditorService.activeEditor` for access to the active editor input + * @see {@link IEditorService.activeEditor} for access to the active editor input */ readonly activeEditorPane: IVisibleEditorPane | undefined; @@ -115,7 +102,7 @@ export interface IEditorService { * The currently active text editor control or `undefined` if there is currently no active * editor or the active editor widget is neither a text nor a diff editor. * - * @see `IEditorService.activeEditor` + * @see {@link IEditorService.activeEditor} */ readonly activeTextEditorControl: IEditor | IDiffEditor | undefined; @@ -129,7 +116,7 @@ export interface IEditorService { /** * All editor panes that are currently visible across all editor groups. * - * @see `IEditorService.visibleEditors` for access to the visible editor inputs + * @see {@link IEditorService.visibleEditors} for access to the visible editor inputs */ readonly visibleEditorPanes: readonly IVisibleEditorPane[]; @@ -179,8 +166,9 @@ export interface IEditorService { * @returns the editor that opened or `undefined` if the operation failed or the editor was not * opened to be active. */ - openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; openEditor(editor: IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; /** @@ -194,8 +182,8 @@ export interface IEditorService { * @returns the editors that opened. The array can be empty or have less elements for editors * that failed to open or were instructed to open as inactive. */ - openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditors(editors: IResourceEditorInputType[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, options?: IOpenEditorsOptions): Promise; + openEditors(editors: IResourceEditorInputType[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, options?: IOpenEditorsOptions): Promise; /** * Replaces editors in an editor group with the provided replacement. @@ -233,18 +221,6 @@ export interface IEditorService { findEditors(resource: URI, group: IEditorGroup | GroupIdentifier): readonly IEditorInput[]; findEditors(resource: IResourceEditorInputIdentifier, group: IEditorGroup | GroupIdentifier): IEditorInput | undefined; - /** - * Get all available editor overrides for the editor input. - */ - getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][]; - - /** - * Allows to override the opening of editors by installing a handler that will - * be called each time an editor is about to open allowing to override the - * operation to open a different editor. - */ - overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable; - /** * Converts a lightweight input to a workbench editor input. */ diff --git a/lib/vscode/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/lib/vscode/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 46301315015a..d248e6695c98 100644 --- a/lib/vscode/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/lib/vscode/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, ITestInstantiationService, TestServiceAccessor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor'; +import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -196,10 +196,10 @@ suite('EditorGroupsService', () => { const downGroup = part.addGroup(rightGroup, GroupDirection.DOWN); const rootGroupInput = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(rootGroupInput, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(rootGroupInput, { pinned: true }); const rightGroupInput = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - await rightGroup.openEditor(rightGroupInput, EditorOptions.create({ pinned: true })); + await rightGroup.openEditor(rightGroupInput, { pinned: true }); assert.strictEqual(part.groups.length, 3); @@ -293,7 +293,7 @@ suite('EditorGroupsService', () => { const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input, { pinned: true }); const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT, { activate: true }); const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN); assert.strictEqual(groupAddedCounter, 2); @@ -323,13 +323,13 @@ suite('EditorGroupsService', () => { const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await rightGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rightGroup.openEditor(input2, { pinned: true }); const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN); - await downGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await downGroup.openEditor(input3, { pinned: true }); part.activateGroup(rootGroup); @@ -347,8 +347,6 @@ suite('EditorGroupsService', () => { await part.whenReady; await part.whenRestored; - - assert.strictEqual(part.isRestored(), true); }); test('options', async () => { @@ -380,6 +378,7 @@ suite('EditorGroupsService', () => { let editorCloseCounter = 0; let editorPinCounter = 0; let editorStickyCounter = 0; + let editorCapabilitiesCounter = 0; const editorGroupChangeListener = group.onDidGroupChange(e => { if (e.kind === GroupChangeKind.EDITOR_OPEN) { assert.ok(e.editor); @@ -393,6 +392,9 @@ suite('EditorGroupsService', () => { } else if (e.kind === GroupChangeKind.EDITOR_PIN) { assert.ok(e.editor); editorPinCounter++; + } else if (e.kind === GroupChangeKind.EDITOR_CAPABILITIES) { + assert.ok(e.editor); + editorCapabilitiesCounter++; } else if (e.kind === GroupChangeKind.EDITOR_STICKY) { assert.ok(e.editor); editorStickyCounter++; @@ -412,8 +414,8 @@ suite('EditorGroupsService', () => { const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - await group.openEditor(input, EditorOptions.create({ pinned: true })); - await group.openEditor(inputInactive, EditorOptions.create({ inactive: true })); + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputInactive, { inactive: true }); assert.strictEqual(group.isActive(input), true); assert.strictEqual(group.isActive(inputInactive), false); @@ -421,6 +423,7 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.contains(inputInactive), true); assert.strictEqual(group.isEmpty, false); assert.strictEqual(group.count, 2); + assert.strictEqual(editorCapabilitiesCounter, 0); assert.strictEqual(editorDidOpenCounter, 2); assert.strictEqual(activeEditorChangeCounter, 1); assert.strictEqual(group.getEditorByIndex(0), input); @@ -428,6 +431,12 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(input), 0); assert.strictEqual(group.getIndexOfEditor(inputInactive), 1); + input.capabilities = EditorInputCapabilities.RequiresTrust; + assert.strictEqual(editorCapabilitiesCounter, 1); + + inputInactive.capabilities = EditorInputCapabilities.Singleton; + assert.strictEqual(editorCapabilitiesCounter, 2); + assert.strictEqual(group.previewEditor, inputInactive); assert.strictEqual(group.isPinned(inputInactive), false); group.pinEditor(inputInactive); @@ -1146,8 +1155,8 @@ suite('EditorGroupsService', () => { const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - await group.openEditor(input, EditorOptions.create({ pinned: true })); - await group.openEditor(inputInactive, EditorOptions.create({ inactive: true })); + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputInactive, { inactive: true }); assert.strictEqual(group.stickyCount, 0); assert.strictEqual(group.isSticky(input), false); @@ -1208,7 +1217,7 @@ suite('EditorGroupsService', () => { const inputSticky = new TestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID); - await group.openEditor(inputSticky, EditorOptions.create({ sticky: true })); + await group.openEditor(inputSticky, { sticky: true }); assert.strictEqual(group.stickyCount, 2); assert.strictEqual(group.isSticky(input), false); @@ -1219,7 +1228,7 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(inputSticky), 1); assert.strictEqual(group.getIndexOfEditor(input), 2); - await group.openEditor(input, EditorOptions.create({ sticky: true })); + await group.openEditor(input, { sticky: true }); assert.strictEqual(group.stickyCount, 3); assert.strictEqual(group.isSticky(input), true); @@ -1274,6 +1283,48 @@ suite('EditorGroupsService', () => { rightGroupListener.dispose(); }); + test('onWillOpenEditor', async () => { + const [part] = await createPart(); + const group = part.activeGroup; + assert.strictEqual(group.isEmpty, true); + + const rightGroup = part.addGroup(group, GroupDirection.RIGHT); + + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const secondInput = new TestFileEditorInput(URI.file('foo/bar/second'), TEST_EDITOR_INPUT_ID); + const thirdInput = new TestFileEditorInput(URI.file('foo/bar/third'), TEST_EDITOR_INPUT_ID); + + let leftFiredCount = 0; + const leftGroupListener = group.onWillOpenEditor(() => { + leftFiredCount++; + }); + + let rightFiredCount = 0; + const rightGroupListener = rightGroup.onWillOpenEditor(() => { + rightFiredCount++; + }); + + await group.openEditor(input); + assert.strictEqual(leftFiredCount, 1); + assert.strictEqual(rightFiredCount, 0); + + rightGroup.openEditor(secondInput); + assert.strictEqual(leftFiredCount, 1); + assert.strictEqual(rightFiredCount, 1); + + group.openEditor(thirdInput); + assert.strictEqual(leftFiredCount, 2); + assert.strictEqual(rightFiredCount, 1); + + // Ensure move fires the open event too + rightGroup.moveEditor(secondInput, group); + assert.strictEqual(leftFiredCount, 3); + assert.strictEqual(rightFiredCount, 1); + + leftGroupListener.dispose(); + rightGroupListener.dispose(); + }); + test('copyEditor with context (across groups)', async () => { const [part] = await createPart(); const group = part.activeGroup; diff --git a/lib/vscode/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/lib/vscode/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 4d13117812b9..87a63784961b 100644 --- a/lib/vscode/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/lib/vscode/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { EditorActivation, EditorOverride } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { EditorsOrder, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput, ITestInstantiationService, registerTestResourceEditor, registerTestSideBySideEditor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroup, IEditorGroupsService, GroupDirection, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { timeout } from 'vs/base/common/async'; import { toResource } from 'vs/base/test/common/utils'; @@ -30,6 +30,12 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { isLinux } from 'vs/base/common/platform'; import { MockScopableContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ContributedEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; suite('EditorService', () => { @@ -60,6 +66,7 @@ suite('EditorService', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); @@ -226,6 +233,104 @@ suite('EditorService', () => { assert.strictEqual(part.activeGroup.getIndexOfEditor(replaceInput), 0); }); + test('openEditors() handles workspace trust (typed editors)', async () => { + const [part, service, accessor] = await createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4); + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + + // Trust: cancel + let trustEditorUris: URI[] = []; + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => { + trustEditorUris = uris; + return WorkspaceTrustUriResponse.Cancel; + }; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 0); + assert.strictEqual(trustEditorUris.length, 4); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input1.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input2.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input3.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input4.resource.toString()), true); + + // Trust: open in new window + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.OpenInNewWindow; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 0); + + // Trust: allow + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.Open; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 3); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + + test('openEditors() ignores trust when `validateTrust: false', async () => { + const [part, service, accessor] = await createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4); + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + + // Trust: cancel + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.Cancel; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }]); + assert.strictEqual(part.activeGroup.count, 3); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + + test('openEditors() extracts proper resources from untyped editors for workspace trust', async () => { + const [part, service, accessor] = await createEditorService(); + + const input = { resource: URI.parse('my://resource-openEditors') }; + const otherInput: IResourceDiffEditorInput = { + originalInput: { resource: URI.parse('my://resource2-openEditors') }, + modifiedInput: { resource: URI.parse('my://resource3-openEditors') } + }; + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + let trustEditorUris: URI[] = []; + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => { + trustEditorUris = uris; + return oldHandler(uris); + }; + + await service.openEditors([input, otherInput], undefined, { validateTrust: true }); + assert.strictEqual(part.activeGroup.count, 0); + assert.strictEqual(trustEditorUris.length, 3); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.originalInput.resource?.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.modifiedInput.resource?.toString()), true); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + test('caching', function () { const instantiationService = workbenchInstantiationService(); const service = instantiationService.createInstance(EditorService); @@ -317,6 +422,16 @@ suite('EditorService', () => { assert(input instanceof FileEditorInput); contentInput = input; assert.strictEqual(contentInput.getPreferredMode(), mode); + let fileModel = (await contentInput.resolve() as ITextFileEditorModel); + assert.strictEqual(fileModel.textEditorModel?.getModeId(), mode); + + // Untyped Input (file, contents) + input = service.createEditorInput({ resource: toResource.call(this, '/index.html'), contents: 'My contents' }); + assert(input instanceof FileEditorInput); + contentInput = input; + fileModel = (await contentInput.resolve() as ITextFileEditorModel); + assert.strictEqual(fileModel.textEditorModel?.getValue(), 'My contents'); + assert.strictEqual(fileModel.isDirty(), true); // Untyped Input (file, different mode) input = service.createEditorInput({ resource: toResource.call(this, '/index.html'), mode: 'text' }); @@ -361,14 +476,20 @@ suite('EditorService', () => { // Untyped Input (resource) input = service.createEditorInput({ resource: URI.parse('custom:resource') }); - assert(input instanceof ResourceEditorInput); + assert(input instanceof TextResourceEditorInput); // Untyped Input (diff) - input = service.createEditorInput({ - leftResource: toResource.call(this, '/primary.html'), - rightResource: toResource.call(this, '/secondary.html') - }); + const resourceDiffInput = { + originalInput: { resource: toResource.call(this, '/primary.html') }, + modifiedInput: { resource: toResource.call(this, '/secondary.html') } + }; + input = service.createEditorInput(resourceDiffInput); assert(input instanceof DiffEditorInput); + assert.strictEqual(input.originalInput.resource?.toString(), resourceDiffInput.originalInput.resource.toString()); + assert.strictEqual(input.modifiedInput.resource?.toString(), resourceDiffInput.modifiedInput.resource.toString()); + const untypedDiffInput = input.asResourceEditorInput(0) as IResourceDiffEditorInput; + assert.strictEqual(untypedDiffInput.originalInput.resource?.toString(), resourceDiffInput.originalInput.resource.toString()); + assert.strictEqual(untypedDiffInput.modifiedInput.resource?.toString(), resourceDiffInput.modifiedInput.resource.toString()); }); test('delegate', function (done) { @@ -389,18 +510,18 @@ suite('EditorService', () => { createEditor(): void { } } - const ed = instantiationService.createInstance(MyEditor, 'my.editor'); + const editor = instantiationService.createInstance(MyEditor, 'my.editor'); - const inp = instantiationService.createInstance(ResourceEditorInput, URI.parse('my://resource-delegate'), 'name', 'description', undefined); + const input = instantiationService.createInstance(TextResourceEditorInput, URI.parse('my://resource-delegate'), 'name', 'description', undefined, undefined); const delegate = instantiationService.createInstance(DelegatingEditorService, async (group, delegate) => { assert.ok(group); done(); - return ed; + return editor; }); - delegate.openEditor(inp); + delegate.openEditor(input); }); test('close editor does not dispose when editor opened in other group', async () => { @@ -955,7 +1076,8 @@ suite('EditorService', () => { mtime: 0, name: 'resource2', size: 0, - isSymbolicLink: false + isSymbolicLink: false, + readonly: false })); await activeEditorChangePromise; @@ -998,32 +1120,105 @@ suite('EditorService', () => { assert.strictEqual(editorContextKeyService, part.activeGroup.activeEditorPane?.scopedContextKeyService); }); - test('overrideOpenEditor', async function () { - const [, service] = await createEditorService(); - - const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); - - let overrideCalled = false; - - const handler = service.overrideOpenEditor({ - open: editor => { - if (editor === input1) { - overrideCalled = true; - - return { override: service.openEditor(input2, { pinned: true }) }; - } - - return undefined; - } - }); - - await service.openEditor(input1, { pinned: true }); + test('editorOverrideService - openEditor', async function () { + const [, service, accessor] = await createEditorService(); + const editorOverrideService = accessor.editorOverrideService; + let overrideCount = 0; + const registrationDisposable = editorOverrideService.registerContributionPoint( + '*.md', + { + id: 'TestEditor', + label: 'Test Editor', + detail: 'Test Editor Provider', + describes: () => false, + priority: ContributedEditorPriority.builtin + }, + {}, + (resource) => { + overrideCount++; + return ({ editor: service.createEditorInput({ resource }) }); + }, + diffEditor => ({ editor: diffEditor }) + ); + assert.strictEqual(overrideCount, 0); + const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource1.txt'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://test/path/resource2.md'), TEST_EDITOR_INPUT_ID); + // Open editor input 1 and it shouln't trigger override as the glob doesn't match + await service.openEditor(input1); + assert.strictEqual(overrideCount, 0); + // Open editor input 2 and it should trigger override as the glob doesn match + await service.openEditor(input2); + assert.strictEqual(overrideCount, 1); + // Because we specify an override we shouldn't see it triggered even if it matches + await service.openEditor(input2, { override: 'default' }); + assert.strictEqual(overrideCount, 1); + + registrationDisposable.dispose(); + }); - assert.ok(overrideCalled); - assert.strictEqual(service.activeEditor, input2); + test('editorOverrideService - openEditors', async function () { + const [, service, accessor] = await createEditorService(); + const editorOverrideService = accessor.editorOverrideService; + let overrideCount = 0; + const registrationDisposable = editorOverrideService.registerContributionPoint( + '*.md', + { + id: 'TestEditor', + label: 'Test Editor', + detail: 'Test Editor Provider', + describes: () => false, + priority: ContributedEditorPriority.builtin + }, + {}, + (resource) => { + overrideCount++; + return ({ editor: service.createEditorInput({ resource }) }); + }, + diffEditor => ({ editor: diffEditor }) + ); + assert.strictEqual(overrideCount, 0); + const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource1.txt'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://test/path/resource2.txt'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('file://test/path/resource3.md'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('file://test/path/resource4.md'), TEST_EDITOR_INPUT_ID); + // Open editor input 1 and it shouln't trigger override as the glob doesn't match + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: input3 }, { editor: input4 }]); + assert.strictEqual(overrideCount, 2); + + registrationDisposable.dispose(); + }); - handler.dispose(); + test('editorOverrideService - replaceEditors', async function () { + const [part, service, accessor] = await createEditorService(); + const editorOverrideService = accessor.editorOverrideService; + let overrideCount = 0; + const registrationDisposable = editorOverrideService.registerContributionPoint( + '*.md', + { + id: 'TestEditor', + label: 'Test Editor', + detail: 'Test Editor Provider', + describes: () => false, + priority: ContributedEditorPriority.builtin + }, + {}, + (resource) => { + overrideCount++; + return ({ editor: service.createEditorInput({ resource }) }); + }, + diffEditor => ({ editor: diffEditor }) + ); + assert.strictEqual(overrideCount, 0); + const input1 = new TestFileEditorInput(URI.parse('file://test/path/resource2.md'), TEST_EDITOR_INPUT_ID); + // Open editor input 1 and it shouldn't trigger because I've disabled the override logic + await service.openEditor(input1, { override: EditorOverride.DISABLED }); + assert.strictEqual(overrideCount, 0); + await service.replaceEditors([{ + editor: input1, + replacement: input1, + }], part.activeGroup); + assert.strictEqual(overrideCount, 1); + registrationDisposable.dispose(); }); test('findEditors (in group)', async () => { diff --git a/lib/vscode/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/lib/vscode/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 8279ac530a32..d77f97eaf252 100644 --- a/lib/vscode/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/lib/vscode/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions, IEditorInputFactoryRegistry, EditorExtensions, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart, createEditorPart, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -17,6 +17,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; import { timeout } from 'vs/base/common/async'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; suite('EditorsObserver', function () { @@ -67,7 +68,7 @@ suite('EditorsObserver', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 1); @@ -84,8 +85,8 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditors(input2.resource), false); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), false); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); + await part.activeGroup.openEditor(input3, { pinned: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -98,7 +99,7 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId }), true); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -148,8 +149,8 @@ suite('EditorsObserver', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); - await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + await rootGroup.openEditor(input1, { pinned: true, activation: EditorActivation.ACTIVATE }); + await sideGroup.openEditor(input1, { pinned: true, activation: EditorActivation.ACTIVATE }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 2); @@ -160,7 +161,7 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditors(input1.resource), true); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), true); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + await rootGroup.openEditor(input1, { pinned: true, activation: EditorActivation.ACTIVATE }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 2); @@ -175,7 +176,7 @@ suite('EditorsObserver', function () { // the most recent editor, but rather put it behind const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true })); + await rootGroup.openEditor(input2, { inactive: true }); currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -221,13 +222,13 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), false); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), false); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(observer.hasEditors(input1.resource), true); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId }), false); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(observer.hasEditors(input1.resource), true); assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId }), true); @@ -258,13 +259,13 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: primary.resource, typeId: primary.typeId }), false); assert.strictEqual(observer.hasEditor({ resource: secondary.resource, typeId: secondary.typeId }), false); - await part.activeGroup.openEditor(input, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input, { pinned: true }); assert.strictEqual(observer.hasEditors(primary.resource), true); assert.strictEqual(observer.hasEditor({ resource: primary.resource, typeId: primary.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: secondary.resource, typeId: secondary.typeId }), false); - await part.activeGroup.openEditor(primary, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(primary, { pinned: true }); assert.strictEqual(observer.hasEditors(primary.resource), true); assert.strictEqual(observer.hasEditor({ resource: primary.resource, typeId: primary.typeId }), true); @@ -292,9 +293,9 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); let currentEditorsMRU = observer.editors; assert.strictEqual(currentEditorsMRU.length, 3); @@ -352,9 +353,9 @@ suite('EditorsObserver', function () { const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); const storage = new TestStorageService(); const observer = disposables.add(new EditorsObserver(part, storage)); @@ -399,11 +400,11 @@ suite('EditorsObserver', function () { const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input3, { pinned: true }); const storage = new TestStorageService(); const observer = disposables.add(new EditorsObserver(part, storage)); @@ -446,7 +447,7 @@ suite('EditorsObserver', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); const storage = new TestStorageService(); const observer = disposables.add(new EditorsObserver(part, storage)); @@ -483,10 +484,10 @@ suite('EditorsObserver', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); assert.strictEqual(rootGroup.count, 3); assert.strictEqual(rootGroup.contains(input1), false); @@ -514,7 +515,7 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId }), true); const input5 = new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID); - await sideGroup.openEditor(input5, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input5, { pinned: true }); assert.strictEqual(rootGroup.count, 1); assert.strictEqual(rootGroup.contains(input1), false); @@ -544,10 +545,10 @@ suite('EditorsObserver', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); assert.strictEqual(rootGroup.count, 3); // 1 editor got closed due to our limit! assert.strictEqual(rootGroup.contains(input1), false); @@ -559,10 +560,10 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId }), true); assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId }), true); - await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input1, { pinned: true }); + await sideGroup.openEditor(input2, { pinned: true }); + await sideGroup.openEditor(input3, { pinned: true }); + await sideGroup.openEditor(input4, { pinned: true }); assert.strictEqual(sideGroup.count, 3); assert.strictEqual(sideGroup.contains(input1), false); @@ -610,10 +611,10 @@ suite('EditorsObserver', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, sticky: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true, sticky: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); assert.strictEqual(rootGroup.count, 3); assert.strictEqual(rootGroup.contains(input1), true); diff --git a/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts b/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts index 4bbd3c44e5c6..536a4e8f372c 100644 --- a/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts @@ -131,6 +131,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment // settings sync. @memoize get userRoamingDataHome(): URI { return joinPath(URI.file(this.userDataPath).with({ scheme: Schemas.vscodeRemote }), 'User'); } + @memoize get userDataPath(): string { const dataPath = this.payload?.get('userDataPath'); @@ -243,25 +244,15 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get disableExtensions() { return this.payload?.get('disableExtensions') === 'true'; } - private get webviewEndpoint(): string { - // TODO@matt: get fallback from product service - return this.options.webviewEndpoint || 'https://{{uuid}}.vscode-webview-test.com/{{commit}}'; - } - @memoize get webviewExternalEndpoint(): string { - return (this.webviewEndpoint).replace('{{commit}}', this.productService.commit || '23a2409675bc1bde94f3532bc7c5826a6e99e4b6'); - } - - @memoize - get webviewResourceRoot(): string { - return `${this.webviewExternalEndpoint}/vscode-resource/{{resource}}`; - } + const endpoint = this.options.webviewEndpoint + || this.productService.webviewContentExternalBaseUrlTemplate + || 'https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/'; - @memoize - get webviewCspSource(): string { - const uri = URI.parse(this.webviewEndpoint.replace('{{uuid}}', '*')); - return `${uri.scheme}://${uri.authority}`; + return endpoint + .replace('{{commit}}', this.payload?.get('webviewExternalEndpointCommit') ?? this.productService.commit ?? '97740a7d253650f9f186c211de5247e2577ce9f7') + .replace('{{quality}}', this.productService.quality || 'insider'); } @memoize @@ -273,6 +264,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get skipReleaseNotes(): boolean { return false; } + @memoize + get disableWorkspaceTrust(): boolean { return true; } + private payload: Map | undefined; constructor( diff --git a/lib/vscode/src/vs/workbench/services/environment/common/environmentService.ts b/lib/vscode/src/vs/workbench/services/environment/common/environmentService.ts index 6604c9823ae4..f8950d42da89 100644 --- a/lib/vscode/src/vs/workbench/services/environment/common/environmentService.ts +++ b/lib/vscode/src/vs/workbench/services/environment/common/environmentService.ts @@ -36,8 +36,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly extensionEnabledProposedApi?: string[]; readonly webviewExternalEndpoint: string; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; readonly skipReleaseNotes: boolean; diff --git a/lib/vscode/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/lib/vscode/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index 9fb4fb45d758..948a3aa89351 100644 --- a/lib/vscode/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/lib/vscode/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -68,21 +68,6 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get webviewExternalEndpoint(): string { return `${Schemas.vscodeWebview}://{{uuid}}`; } - @memoize - get webviewResourceRoot(): string { - // On desktop, this endpoint is only used for the service worker to identify resource loads and - // should never actually be requested. - // - // Required due to https://github.com/electron/electron/issues/28528 - return 'https://{{uuid}}.vscode-webview-test.com/vscode-resource/{{resource}}'; - } - - @memoize - get webviewCspSource(): string { - const uri = URI.parse(this.webviewResourceRoot.replace('{{uuid}}', '*')); - return `${uri.scheme}://${uri.authority}`; - } - @memoize get skipReleaseNotes(): boolean { return !!this.args['skip-release-notes']; } diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts index cd3b096a9a38..0684def27081 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts @@ -299,7 +299,6 @@ registerAction2(class extends Action2 { // DONE and identified extension const res = await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [localize('report', "Report Issue & Continue"), localize('done', "Continue")], - // [], { detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id), checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true }, diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 3683aa847e99..06a8083eeb00 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -26,7 +26,7 @@ import { IExtensionBisectService } from 'vs/workbench/services/extensionManageme import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -95,7 +95,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (this._isDisabledByExtensionKind(extension)) { return EnablementState.DisabledByExtensionKind; } - if (this._isEnabled(extension) && this._isDisabledByTrustRequirement(extension)) { + if (this._isEnabled(extension) && this._isDisabledByWorkspaceTrust(extension)) { return EnablementState.DisabledByTrustRequirement; } return this._getEnablementState(extension.identifier); @@ -160,14 +160,10 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } const result = await Promises.settled(extensions.map(e => { - if (this._isDisabledByTrustRequirement(e)) { - return this.workspaceTrustRequestService.requestWorkspaceTrust({ modal: true }) + if (this._isDisabledByWorkspaceTrust(e)) { + return this.workspaceTrustRequestService.requestWorkspaceTrust() .then(trustState => { - if (trustState) { - return this._setEnablement(e, newState); - } else { - return Promise.resolve(false); - } + return Promise.resolve(trustState ?? false); }); } else { return this._setEnablement(e, newState); @@ -233,8 +229,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _isDisabledByVirtualWorkspace(extension: IExtension): boolean { - if (getVirtualWorkspaceScheme(this.contextService.getWorkspace()) !== undefined) { - return !this.extensionManifestPropertiesService.canSupportVirtualWorkspace(extension.manifest); + if (isVirtualWorkspace(this.contextService.getWorkspace())) { + return this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.manifest) === false; } return false; } @@ -272,12 +268,16 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } - private _isDisabledByTrustRequirement(extension: IExtension): boolean { + isDisabledByWorkspaceTrust(extension: IExtension): boolean { + return this._isEnabled(extension) && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.manifest) === false; + } + + private _isDisabledByWorkspaceTrust(extension: IExtension): boolean { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { return false; } - return this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.manifest) === false; + return this.isDisabledByWorkspaceTrust(extension); } private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { @@ -416,7 +416,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _onDidInstallExtension({ local, error }: DidInstallExtensionEvent): void { - if (local && !error && this._isDisabledByTrustRequirement(local)) { + if (local && !error && this._isDisabledByWorkspaceTrust(local)) { this._onEnablementChanged.fire([local]); } } @@ -427,15 +427,12 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } - private async _getExtensionsByWorkspaceTrustRequirement(): Promise { - const extensions = await this.extensionManagementService.getInstalled(); - return extensions.filter(e => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(e.manifest) === false); - } - public async updateEnablementByWorkspaceTrustRequirement(): Promise { - const extensions = await this._getExtensionsByWorkspaceTrustRequirement(); - if (extensions.length) { - this._onEnablementChanged.fire(extensions); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const disabledExtensions = installedExtensions.filter(e => this.isDisabledByWorkspaceTrust(e)); + + if (disabledExtensions.length) { + this._onEnablementChanged.fire(disabledExtensions); } } diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index edb80d1af335..64fe5d821b94 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -81,6 +81,12 @@ export interface IWorkbenchExtensionEnablementService { */ isDisabledGlobally(extension: IExtension): boolean; + /** + * Returns `true` if the given extension identifier is enabled by the user but it it + * disabled due to the fact that the current window/folder/workspace is not trusted. + */ + isDisabledByWorkspaceTrust(extension: IExtension): boolean; + /** * Enable or disable the given extension. * if `workspace` is `true` then enablement is done for workspace, otherwise globally. diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 367b1ba6c2dc..26fba882a363 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,7 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, INSTALL_ERROR_NOT_SUPPORTED + ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, INSTALL_ERROR_NOT_SUPPORTED, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -23,10 +23,10 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { canceled } from 'vs/base/common/errors'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; -import { isWeb } from 'vs/base/common/platform'; import { Promises } from 'vs/base/common/async'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { isWeb } from 'vs/base/common/platform'; export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { @@ -172,35 +172,35 @@ export class ExtensionManagementService extends Disposable implements IWorkbench .map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier); } - async install(vsix: URI): Promise { + async install(vsix: URI, options?: InstallVSIXOptions): Promise { if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { const manifest = await this.getManifest(vsix); if (isLanguagePackExtension(manifest)) { // Install on both servers - const [local] = await Promises.settled([this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer].map(server => this.installVSIX(vsix, server))); + const [local] = await Promises.settled([this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer].map(server => this.installVSIX(vsix, server, options))); return local; } if (this.extensionManifestPropertiesService.prefersExecuteOnUI(manifest)) { // Install only on local server - return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer, options); } // Install only on remote server - return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer, options); } if (this.extensionManagementServerService.localExtensionManagementServer) { - return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer, options); } if (this.extensionManagementServerService.remoteExtensionManagementServer) { - return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer); + return this.installVSIX(vsix, this.extensionManagementServerService.remoteExtensionManagementServer, options); } return Promise.reject('No Servers to Install'); } - protected async installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { + protected async installVSIX(vsix: URI, server: IExtensionManagementServer, options: InstallVSIXOptions | undefined): Promise { const manifest = await this.getManifest(vsix); if (manifest) { await this.checkForWorkspaceTrust(manifest); - return server.extensionManagementService.install(vsix); + return server.extensionManagementService.install(vsix, options); } return Promise.reject('Unable to get the extension manifest.'); } @@ -312,7 +312,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (isWeb && this.extensionManagementServerService.remoteExtensionManagementServer) { return this.extensionManagementServerService.remoteExtensionManagementServer; } - + // Local server can accept any extension. So return local server if not compatible server found. return this.extensionManagementServerService.localExtensionManagementServer; } @@ -366,7 +366,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench protected async checkForWorkspaceTrust(manifest: IExtensionManifest): Promise { if (this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(manifest) === false) { const trustState = await this.workspaceTrustRequestService.requestWorkspaceTrust({ - modal: true, message: localize('extensionInstallWorkspaceTrustMessage', "Enabling this extension requires a trusted workspace."), buttons: [ { label: localize('extensionInstallWorkspaceTrustButton', "Trust Workspace & Install"), type: 'ContinueWithTrust' }, diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index f8faa18b51cd..ff8f2cc6a045 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -25,7 +25,7 @@ import { Event } from 'vs/base/common/event'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { localize } from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { isArray } from 'vs/base/common/types'; +import { isArray, isFunction } from 'vs/base/common/types'; interface IUserExtension { identifier: IExtensionIdentifier; @@ -76,8 +76,16 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async readSystemExtensions(): Promise { - const extensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions(); - return extensions.concat(this.getStaticExtensions(true)); + let [builtinExtensions, staticExtensions] = await Promise.all([ + this.builtinExtensionsScannerService.scanBuiltinExtensions(), + this.getStaticExtensions(true) + ]); + + if (isFunction(this.environmentService.options?.builtinExtensionsFilter)) { + builtinExtensions = builtinExtensions.filter(e => this.environmentService.options!.builtinExtensionsFilter!(e.identifier.id)); + } + + return [...builtinExtensions, ...staticExtensions]; } /** @@ -259,6 +267,10 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } canAddExtension(galleryExtension: IGalleryExtension): boolean { + if (this.environmentService.options?.assumeGalleryExtensionsAreAddressable) { + return true; + } + return !!galleryExtension.properties.webExtension && !!galleryExtension.webResource; } @@ -267,7 +279,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten throw new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", galleryExtension.displayName || galleryExtension.name)); } - const extensionLocation = galleryExtension.webResource!; + const extensionLocation = joinPath(galleryExtension.assetUri, 'Microsoft.VisualStudio.Code.WebResources', 'extension'); const packageNLSUri = joinPath(extensionLocation, 'package.nls.json'); const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None); const packageNLSExists = isSuccess(context); diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index 1a11ff33fa13..fad465e76c83 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { generateUuid } from 'vs/base/common/uuid'; -import { ILocalExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IExtensionGalleryService, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionManagementService as BaseExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -38,7 +38,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataAutoSyncEnablementService, userDataSyncResourceEnablementService, dialogService, workspaceTrustRequestService, extensionManifestPropertiesService); } - protected override async installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { + protected override async installVSIX(vsix: URI, server: IExtensionManagementServer, options: InstallVSIXOptions | undefined): Promise { if (vsix.scheme === Schemas.vscodeRemote && server === this.extensionManagementServerService.localExtensionManagementServer) { const downloadedLocation = joinPath(this.environmentService.tmpDir, generateUuid()); await this.downloadService.download(vsix, downloadedLocation); @@ -47,7 +47,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { const manifest = await this.getManifest(vsix); if (manifest) { await this.checkForWorkspaceTrust(manifest); - return server.extensionManagementService.install(vsix); + return server.extensionManagementService.install(vsix, options); } return Promise.reject('Unable to get the extension manifest.'); diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 3478bc93d96c..4f0174456c7d 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -41,8 +41,8 @@ export class NativeRemoteExtensionManagementService extends WebRemoteExtensionMa this.localExtensionManagementService = localExtensionManagementServer.extensionManagementService; } - override async install(vsix: URI): Promise { - const local = await super.install(vsix); + override async install(vsix: URI, options?: InstallVSIXOptions): Promise { + const local = await super.install(vsix, options); await this.installUIDependenciesAndPackedExtensions(local); return local; } diff --git a/lib/vscode/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/lib/vscode/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 49e0126904f9..1318c3f2d1aa 100644 --- a/lib/vscode/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/lib/vscode/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -55,6 +55,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { const storageService = createStorageService(instantiationService); const extensionManagementService = instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidInstallExtension: new Emitter().event, onDidUninstallExtension: new Emitter().event } as IExtensionManagementService); const extensionManagementServerService = instantiationService.get(IExtensionManagementServerService) || instantiationService.stub(IExtensionManagementServerService, { localExtensionManagementServer: { extensionManagementService } }); + const workspaceTrustManagementService = instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); super( storageService, new GlobalExtensionEnablementService(storageService), @@ -69,9 +70,9 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()), instantiationService.get(IHostService), new class extends mock() { override isDisabledByBisect() { return false; } }, - instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()), + workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, - instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService())) + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), workspaceTrustManagementService)) ); } diff --git a/lib/vscode/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts b/lib/vscode/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts index 0d76094d021d..3d993c896ca2 100644 --- a/lib/vscode/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts +++ b/lib/vscode/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts @@ -44,6 +44,7 @@ export interface IExtensionRecommendationsService { getConfigBasedRecommendations(): Promise<{ important: string[], others: string[] }>; getWorkspaceRecommendations(): Promise; getKeymapRecommendations(): string[]; + getLanguageRecommendations(): string[]; } export type IgnoredRecommendationChangeNotification = { diff --git a/lib/vscode/src/vs/workbench/services/extensions/browser/extensionService.ts b/lib/vscode/src/vs/workbench/services/extensions/browser/extensionService.ts index c0722aff9916..1ab766411c38 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -185,8 +185,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); - localExtensions = this._checkEnabledAndProposedAPI(localExtensions); - remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); + localExtensions = this._checkEnabledAndProposedAPI(localExtensions, false); + remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); const remoteAgentConnection = this._remoteAgentService.getConnection(); this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); @@ -196,7 +196,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const result = this._registry.deltaExtensions(remoteExtensions.concat(localExtensions), []); if (result.removedDueToLooping.length > 0) { - this._logOrShowMessage(Severity.Error, nls.localize('looping', 'The following extensions contain dependency loops and have been disabled: {0}', result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); + this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', '))); } if (remoteEnv && remoteAgentConnection) { diff --git a/lib/vscode/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/lib/vscode/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 129ab322e21e..6c46aa161132 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -8,7 +8,7 @@ import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/li import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -255,7 +255,13 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { // Extension is not installed else { - const galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier); + let galleryExtension: IGalleryExtension | undefined; + + try { + galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier) ?? undefined; + } catch (err) { + return; + } if (!galleryExtension) { return; @@ -277,7 +283,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { await this.progressService.withProgress({ location: ProgressLocation.Notification, title: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) - }, () => this.extensionManagementService.installFromGallery(galleryExtension)); + }, () => this.extensionManagementService.installFromGallery(galleryExtension!)); this.notificationService.prompt( Severity.Info, diff --git a/lib/vscode/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/lib/vscode/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index fee40e289d8e..bf44e6b8d705 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -360,8 +360,6 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._environmentService.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: workspace.configuration || undefined, diff --git a/lib/vscode/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/lib/vscode/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 8fe00fa65dad..8b1903e969bf 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as perf from 'vs/base/common/performance'; import { isEqualOrParent } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -51,7 +51,7 @@ export function parseScannedExtension(extension: ITranslatedScannedExtension): I class DeltaExtensionsQueueItem { constructor( public readonly toAdd: IExtension[], - public readonly toRemove: string[] + public readonly toRemove: string[] | IExtension[] ) { } } @@ -68,6 +68,69 @@ export const enum ExtensionRunningPreference { Remote } +class LockCustomer { + public readonly promise: Promise; + private _resolve!: (value: IDisposable) => void; + + constructor( + public readonly name: string + ) { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + }); + } + + resolve(value: IDisposable): void { + this._resolve(value); + } +} + +class Lock { + private readonly _pendingCustomers: LockCustomer[] = []; + private _isLocked = false; + + public async acquire(customerName: string): Promise { + const customer = new LockCustomer(customerName); + this._pendingCustomers.push(customer); + this._advance(); + return customer.promise; + } + + private _advance(): void { + if (this._isLocked) { + // cannot advance yet + return; + } + if (this._pendingCustomers.length === 0) { + // no more waiting customers + return; + } + + const customer = this._pendingCustomers.shift()!; + + this._isLocked = true; + let customerHoldsLock = true; + + let logLongRunningCustomerTimeout = setTimeout(() => { + if (customerHoldsLock) { + console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`); + } + }, 30 * 1000 /* 30 seconds */); + + const releaseLock = () => { + if (!customerHoldsLock) { + return; + } + clearTimeout(logLongRunningCustomerTimeout); + customerHoldsLock = false; + this._isLocked = false; + this._advance(); + }; + + customer.resolve(toDisposable(releaseLock)); + } +} + export abstract class AbstractExtensionService extends Disposable implements IExtensionService { public _serviceBrand: undefined; @@ -88,6 +151,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx public readonly onDidChangeResponsiveChange: Event = this._onDidChangeResponsiveChange.event; protected readonly _registry: ExtensionDescriptionRegistry; + private readonly _registryLock: Lock; + private readonly _installedExtensionsReady: Barrier; protected readonly _isDev: boolean; private readonly _extensionsMessages: Map; @@ -98,7 +163,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _deltaExtensionsQueue: DeltaExtensionsQueueItem[]; private _inHandleDeltaExtensions: boolean; - private readonly _onDidFinishHandleDeltaExtensions = this._register(new Emitter()); protected _runningLocation: Map; @@ -130,6 +194,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx })); this._registry = new ExtensionDescriptionRegistry([]); + this._registryLock = new Lock(); + this._installedExtensionsReady = new Barrier(); this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment; this._extensionsMessages = new Map(); @@ -151,14 +217,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._register(this._extensionEnablementService.onEnablementChanged((extensions) => { let toAdd: IExtension[] = []; - let toRemove: string[] = []; + let toRemove: IExtension[] = []; for (const extension of extensions) { if (this._safeInvokeIsEnabled(extension)) { // an extension has been enabled toAdd.push(extension); } else { // an extension has been disabled - toRemove.push(extension.identifier.id); + toRemove.push(extension); } } this._handleDeltaExtensions(new DeltaExtensionsQueueItem(toAdd, toRemove)); @@ -207,20 +273,23 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return; } - while (this._deltaExtensionsQueue.length > 0) { - const item = this._deltaExtensionsQueue.shift()!; - try { - this._inHandleDeltaExtensions = true; + let lock: IDisposable | null = null; + try { + this._inHandleDeltaExtensions = true; + lock = await this._registryLock.acquire('handleDeltaExtensions'); + while (this._deltaExtensionsQueue.length > 0) { + const item = this._deltaExtensionsQueue.shift()!; await this._deltaExtensions(item.toAdd, item.toRemove); - } finally { - this._inHandleDeltaExtensions = false; + } + } finally { + this._inHandleDeltaExtensions = false; + if (lock) { + lock.dispose(); } } - - this._onDidFinishHandleDeltaExtensions.fire(); } - private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[]): Promise { + private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[] | IExtension[]): Promise { let toAdd: IExtensionDescription[] = []; for (let i = 0, len = _toAdd.length; i < len; i++) { const extension = _toAdd[i]; @@ -240,13 +309,20 @@ export abstract class AbstractExtensionService extends Disposable implements IEx let toRemove: IExtensionDescription[] = []; for (let i = 0, len = _toRemove.length; i < len; i++) { - const extensionId = _toRemove[i]; + const extensionOrId = _toRemove[i]; + const extensionId = (typeof extensionOrId === 'string' ? extensionOrId : extensionOrId.identifier.id); + const extension = (typeof extensionOrId === 'string' ? null : extensionOrId); const extensionDescription = this._registry.getExtensionDescription(extensionId); if (!extensionDescription) { // ignore disabling/uninstalling an extension which is not running continue; } + if (extension && extensionDescription.extensionLocation.scheme !== extension.location.scheme) { + // this event is for a different extension than mine (maybe for the local extension, while I have the remote extension) + continue; + } + if (!this.canRemoveExtension(extensionDescription)) { // uses non-dynamic extension point or is activated continue; @@ -559,11 +635,17 @@ export abstract class AbstractExtensionService extends Disposable implements IEx public async startExtensionHosts(): Promise { this.stopExtensionHosts(); - if (this._inHandleDeltaExtensions) { - await Event.toPromise(this._onDidFinishHandleDeltaExtensions.event); - } + const lock = await this._registryLock.acquire('startExtensionHosts'); + try { + this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); - this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHost) { + await localProcessExtensionHost.ready(); + } + } finally { + lock.dispose(); + } } public async restartExtensionHost(): Promise { @@ -680,15 +762,23 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - protected _checkEnabledAndProposedAPI(extensions: IExtensionDescription[]): IExtensionDescription[] { + /** + * @argument extensions The extensions to be checked. + * @argument ignoreWorkspaceTrust Do not take workspace trust into account. + */ + protected _checkEnabledAndProposedAPI(extensions: IExtensionDescription[], ignoreWorkspaceTrust: boolean): IExtensionDescription[] { // enable or disable proposed API per extension this._checkEnableProposedApi(extensions); // keep only enabled extensions - return extensions.filter(extension => this._isEnabled(extension)); + return extensions.filter(extension => this._isEnabled(extension, ignoreWorkspaceTrust)); } - protected _isEnabled(extension: IExtensionDescription): boolean { + /** + * @argument extension The extension to be checked. + * @argument ignoreWorkspaceTrust Do not take workspace trust into account. + */ + protected _isEnabled(extension: IExtensionDescription, ignoreWorkspaceTrust: boolean): boolean { if (extension.isUnderDevelopment) { // Never disable extensions under development return true; @@ -699,7 +789,20 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return false; } - return this._safeInvokeIsEnabled(toExtension(extension)); + const ext = toExtension(extension); + + const isEnabled = this._safeInvokeIsEnabled(ext); + if (isEnabled) { + return true; + } + + if (ignoreWorkspaceTrust && this._safeInvokeIsDisabledByWorkspaceTrust(ext)) { + // This extension is disabled, but the reason for it being disabled + // is workspace trust, so we will consider it enabled + return true; + } + + return false; } protected _safeInvokeIsEnabled(extension: IExtension): boolean { @@ -710,6 +813,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } + protected _safeInvokeIsDisabledByWorkspaceTrust(extension: IExtension): boolean { + try { + return this._extensionEnablementService.isDisabledByWorkspaceTrust(extension); + } catch (err) { + return false; + } + } + protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void { const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null); for (let extensionDescription of affectedExtensions) { diff --git a/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostMain.ts b/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostMain.ts index 0cb9763625bb..0120d4dcdd92 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostMain.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostMain.ts @@ -36,6 +36,7 @@ export class ExtensionHostMain { private _isTerminating: boolean; private readonly _hostUtils: IHostUtils; + private readonly _rpcProtocol: RPCProtocol; private readonly _extensionService: IExtHostExtensionService; private readonly _logService: ILogService; private readonly _disposables = new DisposableStore(); @@ -48,15 +49,15 @@ export class ExtensionHostMain { ) { this._isTerminating = false; this._hostUtils = hostUtils; - const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); + this._rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); // ensure URIs are transformed and revived - initData = ExtensionHostMain._transform(initData, rpcProtocol); + initData = ExtensionHostMain._transform(initData, this._rpcProtocol); // bootstrap services const services = new ServiceCollection(...getSingletonServiceDescriptors()); services.set(IExtHostInitDataService, { _serviceBrand: undefined, ...initData }); - services.set(IExtHostRpcService, new ExtHostRpcService(rpcProtocol)); + services.set(IExtHostRpcService, new ExtHostRpcService(this._rpcProtocol)); services.set(IURITransformerService, new URITransformerService(uriTransformer)); services.set(IHostUtils, hostUtils); @@ -99,8 +100,8 @@ export class ExtensionHostMain { }; }); - const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService); - const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors); + const mainThreadExtensions = this._rpcProtocol.getProxy(MainContext.MainThreadExtensionService); + const mainThreadErrors = this._rpcProtocol.getProxy(MainContext.MainThreadErrors); errors.setUnexpectedErrorHandler(err => { const data = errors.transformErrorForSerialization(err); const extension = extensionErrors.get(err); @@ -124,9 +125,12 @@ export class ExtensionHostMain { this._disposables.dispose(); errors.setUnexpectedErrorHandler((err) => { - // TODO: write to log once we have one + this._logService.error(err); }); + // Invalidate all proxies + this._rpcProtocol.dispose(); + const extensionsDeactivated = this._extensionService.deactivateAll(); // Give extensions 1 second to wrap up any async dispose, then exit in at most 4 seconds diff --git a/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostManager.ts index b363ff4c1ffd..50ef4572c745 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -24,6 +24,7 @@ import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { timeout } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -132,6 +133,10 @@ export class ExtensionHostManager extends Disposable { return p.value; } + public async ready(): Promise { + await this._getProxy(); + } + private async _measureLatency(proxy: ExtHostExtensionServiceShape): Promise { const COUNT = 10; @@ -285,6 +290,15 @@ export class ExtensionHostManager extends Disposable { } } + public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + const proxy = await this._getProxy(); + if (!proxy) { + throw new Error(`Cannot resolve canonical URI`); + } + const result = await proxy.$getCanonicalURI(remoteAuthority, uri); + return URI.revive(result); + } + public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise { const proxy = await this._getProxy(); if (!proxy) { diff --git a/lib/vscode/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/lib/vscode/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts index 913b00826241..8afff68e3374 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType, ExtensionVirtualWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { isNonEmptyArray } from 'vs/base/common/arrays'; @@ -13,7 +13,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionUntrustedWorkspaceSupport } from 'vs/base/common/product'; import { Disposable } from 'vs/base/common/lifecycle'; -import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { isBoolean } from 'vs/base/common/types'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export const IExtensionManifestPropertiesService = createDecorator('extensionManifestPropertiesService'); @@ -30,7 +32,7 @@ export interface IExtensionManifestPropertiesService { getExtensionKind(manifest: IExtensionManifest): ExtensionKind[]; getExtensionUntrustedWorkspaceSupportType(manifest: IExtensionManifest): ExtensionUntrustedWorkpaceSupportType; - canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean; + getExtensionVirtualWorkspaceSupportType(manifest: IExtensionManifest): ExtensionVirtualWorkpaceSupportType; } export class ExtensionManifestPropertiesService extends Disposable implements IExtensionManifestPropertiesService { @@ -50,6 +52,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE constructor( @IProductService private readonly productService: IProductService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super(); @@ -123,7 +126,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE getExtensionUntrustedWorkspaceSupportType(manifest: IExtensionManifest): ExtensionUntrustedWorkpaceSupportType { // Workspace trust feature is disabled, or extension has no entry point - if (!isWorkspaceTrustEnabled(this.configurationService) || !manifest.main) { + if (!this.workspaceTrustManagementService.workspaceTrustEnabled || !manifest.main) { return true; } @@ -156,7 +159,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE return false; } - canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean { + getExtensionVirtualWorkspaceSupportType(manifest: IExtensionManifest): ExtensionVirtualWorkpaceSupportType { // check user configured const userConfiguredVirtualWorkspaceSupport = this.getConfiguredVirtualWorkspaceSupport(manifest); if (userConfiguredVirtualWorkspaceSupport !== undefined) { @@ -171,8 +174,14 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE } // check the manifest - if (manifest.capabilities?.virtualWorkspaces !== undefined) { - return manifest.capabilities?.virtualWorkspaces; + const virtualWorkspaces = manifest.capabilities?.virtualWorkspaces; + if (isBoolean(virtualWorkspaces)) { + return virtualWorkspaces; + } else if (virtualWorkspaces) { + const supported = virtualWorkspaces.supported; + if (isBoolean(supported) || supported === 'limited') { + return supported; + } } // check default from product diff --git a/lib/vscode/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/lib/vscode/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index dca05b7ca6dd..1e91d9bdff22 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -320,6 +320,16 @@ export const schema: IJSONSchema = { body: 'onAuthenticationRequest:${11:authenticationProviderId}', description: nls.localize('vscode.extension.activationEvents.onAuthenticationRequest', 'An activation event emitted whenever sessions are requested from the specified authentication provider.') }, + { + label: 'onRenderer', + description: nls.localize('vscode.extension.activationEvents.onRenderer', 'An activation event emitted whenever a notebook output renderer is used.'), + body: 'onRenderer:${11:rendererId}' + }, + { + label: 'onTerminalProfile', + body: 'onTerminalProfile:${1:terminalType}', + description: nls.localize('vscode.extension.activationEvents.onTerminalProfile', 'An activation event emitted when a specific terminal profile is launched.'), + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), @@ -421,8 +431,28 @@ export const schema: IJSONSchema = { properties: { virtualWorkspaces: { description: nls.localize('vscode.extension.capabilities.virtualWorkspaces', "Declares whether the extension should be enabled in virtual workspaces. A virtual workspace is a workspace which is not backed by any on-disk resources. When false, this extension will be automatically disabled in virtual workspaces. Default is true."), - type: 'boolean', - default: true + type: ['boolean', 'object'], + defaultSnippets: [ + { label: 'limited', body: { supported: '${1:limited}', description: '${2}' } }, + { label: 'false', body: { supported: false, description: '${2}' } }, + ], + default: true.valueOf, + properties: { + supported: { + markdownDescription: nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported', "Declares the level of support for virtual workspaces by the extension."), + type: ['string', 'boolean'], + enum: ['limited', true, false], + enumDescriptions: [ + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.limited', "The extension will be enabled in virtual workspaces with some functionality disabled."), + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.true', "The extension will be enabled in virtual workspaces with all functionality enabled."), + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.false', "The extension will not be enabled in virtual workspaces."), + ] + }, + description: { + type: 'string', + markdownDescription: nls.localize('vscode.extension.capabilities.virtualWorkspaces.description', "A description of how virtual workspaces affects the extensions behavior and why it is needed. This only applies when `supported` is not `true`."), + } + } }, untrustedWorkspaces: { description: nls.localize('vscode.extension.capabilities.untrustedWorkspaces', 'Declares how the extension should be handled in untrusted workspaces.'), diff --git a/lib/vscode/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/lib/vscode/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 4b07928a83d5..d238526f526c 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -235,9 +235,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: remoteInitData.globalStorageHome, - workspaceStorageHome: remoteInitData.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, + workspaceStorageHome: remoteInitData.workspaceStorageHome }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : { configuration: workspace.configuration, diff --git a/lib/vscode/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/lib/vscode/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index 792a4cc38165..aa78078e14ea 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as errors from 'vs/base/common/errors'; @@ -69,9 +68,10 @@ export class CachedExtensionScanner { const version = this._productService.version; const commit = this._productService.commit; + const date = this._productService.date; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.language; - const input = new ExtensionScannerInput(version, commit, locale, devMode, path, isBuiltin, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, translations); return ExtensionScanner.scanSingleExtension(input, log); } @@ -151,7 +151,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - const cacheRawContents = await fs.promises.readFile(cacheFile, 'utf8'); + const cacheRawContents = await pfs.Promises.readFile(cacheFile, 'utf8'); return JSON.parse(cacheRawContents); } catch (err) { // That's ok... @@ -165,7 +165,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - await fs.promises.mkdir(cacheFolder, { recursive: true }); + await pfs.Promises.mkdir(cacheFolder, { recursive: true }); } catch (err) { // That's ok... } @@ -184,7 +184,7 @@ export class CachedExtensionScanner { } try { - const folderStat = await fs.promises.stat(input.absoluteFolderPath); + const folderStat = await pfs.Promises.stat(input.absoluteFolderPath); input.mtime = folderStat.mtime.getTime(); } catch (err) { // That's ok... @@ -224,7 +224,7 @@ export class CachedExtensionScanner { private static async _readTranslationConfig(): Promise { if (platform.translationsConfigFile) { try { - const content = await fs.promises.readFile(platform.translationsConfigFile, 'utf8'); + const content = await pfs.Promises.readFile(platform.translationsConfigFile, 'utf8'); return JSON.parse(content) as Translations; } catch (err) { // no problemo @@ -245,6 +245,7 @@ export class CachedExtensionScanner { const version = productService.version; const commit = productService.commit; + const date = productService.date; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.language; @@ -253,7 +254,7 @@ export class CachedExtensionScanner { notificationService, environmentService, BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), log ); @@ -263,10 +264,10 @@ export class CachedExtensionScanner { const builtInExtensions = Promise.resolve(productService.builtInExtensions || []); const controlFilePath = joinPath(environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = fs.promises.readFile(controlFilePath, 'utf8') + const controlFile = pfs.Promises.readFile(controlFilePath, 'utf8') .then(raw => JSON.parse(raw), () => ({} as any)); - const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) .then(resolver => ExtensionScanner.scanExtensions(input, log, resolver)); @@ -279,7 +280,7 @@ export class CachedExtensionScanner { notificationService, environmentService, USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, environmentService.extensionsPath, false, false, translations), log )); @@ -288,7 +289,7 @@ export class CachedExtensionScanner { if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI) { const extDescsP = environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log + new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log ); }); developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { diff --git a/lib/vscode/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/lib/vscode/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index b3a09f4164dc..67fecffb3cef 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -15,7 +15,7 @@ import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsSc import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteTrustOption, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -42,12 +42,8 @@ import { Schemas } from 'vs/base/common/network'; import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { updateProxyConfigurationsScope } from 'vs/platform/request/common/request'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { Codicon } from 'vs/base/common/codicons'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; - -const MACHINE_PROMPT = false; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -75,7 +71,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, @ILogService private readonly _logService: ILogService, - @IDialogService private readonly _dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { @@ -144,7 +139,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten return { getInitData: async () => { if (isInitialStart) { - const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); + // Here we load even extensions that would be disabled by workspace trust + const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), /* ignore workspace trust */true); const runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, []); const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { @@ -343,11 +339,24 @@ export class ExtensionService extends AbstractExtensionService implements IExten const remoteAuthority = this._environmentService.remoteAuthority; const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; - const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions()); let remoteEnv: IRemoteAgentEnvironment | null = null; let remoteExtensions: IExtensionDescription[] = []; if (remoteAuthority) { + + this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => { + if (uri.scheme !== Schemas.vscodeRemote || uri.authority !== remoteAuthority) { + // The current remote authority resolver cannot give the canonical URI for this URI + return uri; + } + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; + return localProcessExtensionHost.getCanonicalURI(remoteAuthority, uri); + }); + + // Now that the canonical URI provider has been registered, we need to wait for the trust state to be + // calculated. The trust state will be used while resolving the authority, however the resolver can + // override the trust state through the resolver result. + await this._workspaceTrustManagementService.workspaceResolved; let resolverResult: ResolverResult; try { @@ -364,42 +373,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); + await this._startLocalExtensionHost(); return; } - let promptForMachineTrust = MACHINE_PROMPT; - - if (resolverResult.options?.trust === RemoteTrustOption.DisableTrust) { - promptForMachineTrust = false; - this._workspaceTrustManagementService.setWorkspaceTrust(true); - } else if (resolverResult.options?.trust === RemoteTrustOption.MachineTrusted) { - promptForMachineTrust = false; - } - - if (promptForMachineTrust) { - const dialogResult = await this._dialogService.show( - Severity.Info, - nls.localize('machineTrustQuestion', "Do you trust the machine you're connecting to?"), - [nls.localize('yes', "Yes, connect."), nls.localize('no', "No, do not connect.")], - { - cancelId: 1, - custom: { - icon: Codicon.remoteExplorer - }, - // checkbox: { label: nls.localize('remember', "Remember my choice"), checked: true } - } - ); - - if (dialogResult.choice !== 0) { - // Did not confirm trust - this._notificationService.notify({ severity: Severity.Warning, message: nls.localize('trustFailure', "Refused to connect to untrusted machine.") }); - // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); - return; - } - } - // set the resolved authority this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options); this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation); @@ -420,23 +397,31 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); - remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); if (!remoteEnv) { this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") }); // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); + await this._startLocalExtensionHost(); return; } updateProxyConfigurationsScope(remoteEnv.useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE); + } else { + + this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => uri); + } - await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv, remoteExtensions); + await this._startLocalExtensionHost(remoteAuthority, remoteEnv, remoteExtensions); } - private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null, remoteExtensions: IExtensionDescription[] = []): Promise { + private async _startLocalExtensionHost(remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null, remoteExtensions: IExtensionDescription[] = []): Promise { + // Ensure that the workspace trust state has been fully initialized so + // that the extension host can start with the correct set of extensions. + await this._workspaceTrustManagementService.workspaceTrustInitialized; + remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); + const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), false); this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); // remove non-UI extensions from the local extensions @@ -516,7 +501,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const allExtensions = await this._scanAllLocalExtensions(); const extension = allExtensions.filter(e => e.identifier.value === resolverExtensionId)[0]; if (extension) { - if (!this._isEnabled(extension)) { + if (!this._isEnabled(extension, false)) { const message = nls.localize('enableResolver', "Extension '{0}' is required to open the remote window.\nOK to enable?", recommendation.friendlyName); this._notificationService.prompt(Severity.Info, message, [{ diff --git a/lib/vscode/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/lib/vscode/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 72535eae5530..245da60c35b1 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -476,8 +476,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._environmentService.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: withNullAsUndefined(workspace.configuration), @@ -627,6 +625,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { // (graceful termination) protocol.send(createMessageOfType(MessageType.Terminate)); + protocol.getSocket().dispose(); + protocol.dispose(); // Give the extension host 10s, after which we will diff --git a/lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 5abd3c05236d..ed44762939e0 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -17,7 +17,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IURITransformer, URITransformer } from 'vs/base/common/uriIpc'; +import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; import { exists } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; @@ -177,6 +177,9 @@ function _createExtHostProtocol(): Promise { }); socket.once('error', reject); + socket.on('close', () => { + onTerminate('renderer closed the socket'); + }); }); } } @@ -313,6 +316,7 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise { + // NOTE@coder: add proxy agent patch proxyAgent.monkeyPatch(true); performance.mark(`code/extHost/willConnectToRenderer`); @@ -335,9 +339,11 @@ export async function startExtensionHostProcess(): Promise { // Attempt to load uri transformer let uriTransformer: IURITransformer | null = null; - if (initData.remote.authority) { + if (initData.remote.authority && args.uriTransformerPath) { try { - uriTransformer = new URITransformer(initData.remote.authority); + const rawURITransformerFactory = require.__$__nodeRequire(args.uriTransformerPath); + const rawURITransformer = rawURITransformerFactory(initData.remote.authority); + uriTransformer = new URITransformer(rawURITransformer); } catch (e) { console.error(e); } diff --git a/lib/vscode/src/vs/workbench/services/extensions/node/extensionPoints.ts b/lib/vscode/src/vs/workbench/services/extensions/node/extensionPoints.ts index 5506bddc81db..14106fd427a2 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as semver from 'vs/base/common/semver/semver'; @@ -30,14 +29,16 @@ export interface NlsConfiguration { abstract class ExtensionManifestHandler { protected readonly _ourVersion: string; + protected readonly _ourProductDate: string | undefined; protected readonly _log: ILog; protected readonly _absoluteFolderPath: string; protected readonly _isBuiltin: boolean; protected readonly _isUnderDevelopment: boolean; protected readonly _absoluteManifestPath: string; - constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) { + constructor(ourVersion: string, ourProductDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) { this._ourVersion = ourVersion; + this._ourProductDate = ourProductDate; this._log = log; this._absoluteFolderPath = absoluteFolderPath; this._isBuiltin = isBuiltin; @@ -58,7 +59,7 @@ class ExtensionManifestParser extends ExtensionManifestHandler { } public parse(): Promise { - return fs.promises.readFile(this._absoluteManifestPath).then((manifestContents) => { + return pfs.Promises.readFile(this._absoluteManifestPath).then((manifestContents) => { const errors: json.ParseError[] = []; const manifest = ExtensionManifestParser._fastParseJSON(manifestContents.toString(), errors); if (json.getNodeType(manifest) !== 'object') { @@ -91,8 +92,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private readonly _nlsConfig: NlsConfiguration; - constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) { - super(ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + constructor(ourVersion: string, ourProductDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) { + super(ourVersion, ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); this._nlsConfig = nlsConfig; } @@ -128,7 +129,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { let translationPath = this._nlsConfig.translations[translationId]; let localizedMessages: Promise; if (translationPath) { - localizedMessages = fs.promises.readFile(translationPath, 'utf8').then((content) => { + localizedMessages = pfs.Promises.readFile(translationPath, 'utf8').then((content) => { let errors: json.ParseError[] = []; let translationBundle: TranslationBundle = json.parse(content, errors); if (errors.length > 0) { @@ -153,7 +154,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { if (!messageBundle.localized) { return { values: undefined, default: messageBundle.original }; } - return fs.promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { + return pfs.Promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { let errors: json.ParseError[] = []; let messages: MessageBag = json.parse(messageBundleContent, errors); if (errors.length > 0) { @@ -202,7 +203,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private static resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { return new Promise<{ [key: string]: string; } | null>((c, e) => { if (originalMessageBundle) { - fs.promises.readFile(originalMessageBundle).then(originalBundleContent => { + pfs.Promises.readFile(originalMessageBundle).then(originalBundleContent => { c(json.parse(originalBundleContent.toString(), errors)); }, (err) => { c(null); @@ -318,7 +319,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { extensionDescription.isUnderDevelopment = this._isUnderDevelopment; let notices: string[] = []; - if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._absoluteFolderPath, extensionDescription, notices)) { + if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._ourProductDate, this._absoluteFolderPath, extensionDescription, notices)) { notices.forEach((error) => { this._log.error(this._absoluteFolderPath, error); }); @@ -344,7 +345,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return extensionDescription; } - private static isValidExtensionDescription(version: string, extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { + private static isValidExtensionDescription(version: string, productDate: string | undefined, extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { if (!ExtensionManifestValidator.baseIsValidExtensionDescription(extensionFolderPath, extensionDescription, notices)) { return false; @@ -355,7 +356,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return false; } - return isValidExtensionVersion(version, extensionDescription, notices); + return isValidExtensionVersion(version, productDate, extensionDescription, notices); } private static baseIsValidExtensionDescription(extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { @@ -453,6 +454,7 @@ export class ExtensionScannerInput { constructor( public readonly ourVersion: string, + public readonly ourProductDate: string | undefined, public readonly commit: string | undefined, public readonly locale: string | undefined, public readonly devMode: boolean, @@ -476,6 +478,7 @@ export class ExtensionScannerInput { public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { return ( a.ourVersion === b.ourVersion + && a.ourProductDate === b.ourProductDate && a.commit === b.commit && a.locale === b.locale && a.devMode === b.devMode @@ -512,23 +515,23 @@ export class ExtensionScanner { /** * Read the extension defined in `absoluteFolderPath` */ - private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise { + private static scanExtension(version: string, productDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise { absoluteFolderPath = path.normalize(absoluteFolderPath); - let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + let parser = new ExtensionManifestParser(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); return parser.parse().then((extensionDescription) => { if (extensionDescription === null) { return null; } - let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); + let nlsReplacer = new ExtensionManifestNLSReplacer(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); return nlsReplacer.replaceNLS(extensionDescription); }).then((extensionDescription) => { if (extensionDescription === null) { return null; } - let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + let validator = new ExtensionManifestValidator(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); return validator.validate(extensionDescription); }); } @@ -549,7 +552,7 @@ export class ExtensionScanner { let obsolete: { [folderName: string]: boolean; } = {}; if (!isBuiltin) { try { - const obsoleteFileContents = await fs.promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); + const obsoleteFileContents = await pfs.Promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); obsolete = JSON.parse(obsoleteFileContents); } catch (err) { // Don't care @@ -566,7 +569,7 @@ export class ExtensionScanner { } const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); + let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); let extensionDescriptions = arrays.coalesce(_extensionDescriptions); extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionIdentifierWithVersion({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version).key()]); @@ -601,7 +604,7 @@ export class ExtensionScanner { return pfs.SymlinkSupport.existsFile(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { if (exists) { const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { + return this.scanExtension(input.ourVersion, input.ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { if (extensionDescription === null) { return []; } @@ -620,7 +623,7 @@ export class ExtensionScanner { const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); + return this.scanExtension(input.ourVersion, input.ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); } public static mergeBuiltinExtensions(builtinExtensions: Promise, extraBuiltinExtensions: Promise): Promise { diff --git a/lib/vscode/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/lib/vscode/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index 71ce4b823c18..82b82e59c671 100644 --- a/lib/vscode/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/lib/vscode/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -12,11 +12,13 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; import { isWeb } from 'vs/base/common/platform'; +import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; suite('ExtensionManifestPropertiesService - ExtensionKind', () => { function check(manifest: Partial, expected: ExtensionKind[]): void { - const extensionManifestPropertiesService = new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService()); + const extensionManifestPropertiesService = new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustManagementService()); assert.deepStrictEqual(extensionManifestPropertiesService.deduceExtensionKind(manifest), expected); } @@ -80,6 +82,7 @@ if (!isWeb) { test('test extension workspace trust request when main entry point is missing', () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest(); assertUntrustedWorkspaceSupport(extensionMaifest, true); @@ -87,7 +90,7 @@ if (!isWeb) { test('test extension workspace trust request when workspace trust is disabled', async () => { instantiationService.stub(IProductService, >{}); - await testConfigurationService.setUserConfiguration('security', { workspace: { trust: { enabled: false } } }); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService(false)); const extensionMaifest = getExtensionManifest({ main: './out/extension.js' }); assertUntrustedWorkspaceSupport(extensionMaifest, true); @@ -95,37 +98,34 @@ if (!isWeb) { test('test extension workspace trust request when override exists in settings.json', async () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); - await testConfigurationService.setUserConfiguration('security', { workspace: { trust: { extensionUntrustedSupport: { 'pub.a': { supported: true } } } } }); + await testConfigurationService.setUserConfiguration('extensions', { supportUntrustedWorkspaces: { 'pub.a': { supported: true } } }); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, true); }); test('test extension workspace trust request when override for the version exists in settings.json', async () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); - await testConfigurationService.setUserConfiguration('security', { workspace: { trust: { extensionUntrustedSupport: { 'pub.a': { supported: true, version: '1.0.0' } } } } }); + await testConfigurationService.setUserConfiguration('extensions', { supportUntrustedWorkspaces: { 'pub.a': { supported: true, version: '1.0.0' } } }); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, true); }); test('test extension workspace trust request when override for a different version exists in settings.json', async () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); - await testConfigurationService.setUserConfiguration('security', { - workspace: { - trust: { - enabled: true, - extensionUntrustedSupport: { 'pub.a': { supported: true, version: '2.0.0' } } - } - } - }); + await testConfigurationService.setUserConfiguration('extensions', { supportUntrustedWorkspaces: { 'pub.a': { supported: true, version: '2.0.0' } } }); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, 'limited'); }); test('test extension workspace trust request when default exists in product.json', () => { instantiationService.stub(IProductService, >{ extensionUntrustedWorkspaceSupport: { 'pub.a': { default: true } } }); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js' }); assertUntrustedWorkspaceSupport(extensionMaifest, true); @@ -133,6 +133,7 @@ if (!isWeb) { test('test extension workspace trust request when override exists in product.json', () => { instantiationService.stub(IProductService, >{ extensionUntrustedWorkspaceSupport: { 'pub.a': { override: 'limited' } } }); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: true } } }); assertUntrustedWorkspaceSupport(extensionMaifest, 'limited'); @@ -140,6 +141,7 @@ if (!isWeb) { test('test extension workspace trust request when value exists in package.json', () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js', capabilities: { untrustedWorkspaces: { supported: 'limited' } } }); assertUntrustedWorkspaceSupport(extensionMaifest, 'limited'); @@ -147,6 +149,7 @@ if (!isWeb) { test('test extension workspace trust request when no value exists in package.json', () => { instantiationService.stub(IProductService, >{}); + instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); const extensionMaifest = getExtensionManifest({ main: './out/extension.js' }); assertUntrustedWorkspaceSupport(extensionMaifest, false); diff --git a/lib/vscode/src/vs/workbench/services/history/browser/history.ts b/lib/vscode/src/vs/workbench/services/history/browser/history.ts index 4ce401e8a18d..43e9ed6563c7 100644 --- a/lib/vscode/src/vs/workbench/services/history/browser/history.ts +++ b/lib/vscode/src/vs/workbench/services/history/browser/history.ts @@ -7,7 +7,8 @@ import { localize } from 'vs/nls'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditorPane, EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, EditorResourceAccessor, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditorPane, EditorExtensions, IEditorCloseEvent, IEditorInputFactoryRegistry, EditorResourceAccessor, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG, FileOperationEvent, FileOperation } from 'vs/platform/files/common/files'; @@ -398,7 +399,7 @@ export class HistoryService extends Disposable implements IHistoryService { return this.editorService.openEditor(location.input, options); } - return this.editorService.openEditor({ resource: (location.input as IResourceEditorInput).resource, options }); + return this.editorService.openEditor({ resource: location.input.resource, options }); } private handleEditorEventInNavigationStack(control: IEditorPane | undefined, event?: ICursorPositionChangedEvent): void { @@ -1011,7 +1012,7 @@ export class HistoryService extends Disposable implements IHistoryService { const editorSerializer = this.editorInputFactory.getEditorInputSerializer(editorInputJSON.typeId); if (editorSerializer) { const input = editorSerializer.deserialize(this.instantiationService, editorInputJSON.deserialized); - if (input) { + if (input instanceof EditorInput) { this.onEditorDispose(input, () => this.removeFromHistory(input), this.editorHistoryListeners); } diff --git a/lib/vscode/src/vs/workbench/services/history/test/browser/history.test.ts b/lib/vscode/src/vs/workbench/services/history/test/browser/history.test.ts index da0879a53ec8..9927273df89f 100644 --- a/lib/vscode/src/vs/workbench/services/history/test/browser/history.test.ts +++ b/lib/vscode/src/vs/workbench/services/history/test/browser/history.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; @@ -53,11 +52,11 @@ suite('HistoryService', function () { const [part, historyService, editorService] = await createServices(); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input1); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input2); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); @@ -78,10 +77,10 @@ suite('HistoryService', function () { assert.strictEqual(history.length, 0); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); history = historyService.getHistory(); assert.strictEqual(history.length, 2); @@ -98,7 +97,7 @@ suite('HistoryService', function () { assert.ok(!historyService.getLastActiveFile('foo')); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(historyService.getLastActiveFile('foo')?.toString(), input1.resource.toString()); }); @@ -109,10 +108,10 @@ suite('HistoryService', function () { const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input1); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input2, { pinned: true }); assert.strictEqual(part.activeGroup.activeEditor, input2); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); @@ -145,8 +144,8 @@ suite('HistoryService', function () { const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input1, { pinned: true }); + await sideGroup.openEditor(input2, { pinned: true }); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); historyService.openPreviouslyUsedEditor(); @@ -169,9 +168,9 @@ suite('HistoryService', function () { const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input1, { pinned: true }); + await part.activeGroup.openEditor(input2, { pinned: true }); + await part.activeGroup.openEditor(input3, { pinned: true }); let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); historyService.openPreviouslyUsedEditor(); @@ -179,7 +178,7 @@ suite('HistoryService', function () { assert.strictEqual(part.activeGroup.activeEditor, input2); await timeout(0); - await part.activeGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input4, { pinned: true }); editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); historyService.openPreviouslyUsedEditor(); diff --git a/lib/vscode/src/vs/workbench/services/host/browser/browserHostService.ts b/lib/vscode/src/vs/workbench/services/host/browser/browserHostService.ts index 8506a81b3575..7c5c8b61da83 100644 --- a/lib/vscode/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/lib/vscode/src/vs/workbench/services/host/browser/browserHostService.ts @@ -256,8 +256,8 @@ export class BrowserHostService extends Disposable implements IHostService { // Same Window: open via editor service in current window if (this.shouldReuse(options, true /* file */)) { editorService.openEditor({ - leftResource: editors[0].resource, - rightResource: editors[1].resource, + originalInput: { resource: editors[0].resource }, + modifiedInput: { resource: editors[1].resource }, options: { pinned: true } }); } @@ -292,7 +292,7 @@ export class BrowserHostService extends Disposable implements IHostService { openables = [openable]; } - editorService.openEditors(await pathsToEditors(openables, this.fileService)); + editorService.openEditors(await pathsToEditors(openables, this.fileService), undefined, { validateTrust: true }); } // New Window: open into empty window diff --git a/lib/vscode/src/vs/workbench/services/host/browser/host.ts b/lib/vscode/src/vs/workbench/services/host/browser/host.ts index 9e6a97e9c9c4..106c41c05d32 100644 --- a/lib/vscode/src/vs/workbench/services/host/browser/host.ts +++ b/lib/vscode/src/vs/workbench/services/host/browser/host.ts @@ -12,7 +12,7 @@ export const IHostService = createDecorator('hostService'); /** * A set of methods supported in both web and native environments. * - * @see `INativeHostService` for methods that are specific to native + * @see {@link INativeHostService} for methods that are specific to native * environments. */ export interface IHostService { diff --git a/lib/vscode/src/vs/workbench/services/hover/browser/hoverService.ts b/lib/vscode/src/vs/workbench/services/hover/browser/hoverService.ts index 0e1b809311c1..f89f70cd98e9 100644 --- a/lib/vscode/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/lib/vscode/src/vs/workbench/services/hover/browser/hoverService.ts @@ -8,11 +8,12 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions } from 'vs/workbench/services/hover/browser/hover'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { HoverWidget } from 'vs/workbench/services/hover/browser/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; export class HoverService implements IHoverService { declare readonly _serviceBrand: undefined; @@ -21,8 +22,10 @@ export class HoverService implements IHoverService { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextViewService private readonly _contextViewService: IContextViewService + @IContextViewService private readonly _contextViewService: IContextViewService, + @IContextMenuService contextMenuService: IContextMenuService ) { + contextMenuService.onDidShowContextMenu(() => this.hideHover()); } showHover(options: IHoverOptions, focus?: boolean): IDisposable | undefined { @@ -31,17 +34,28 @@ export class HoverService implements IHoverService { } this._currentHoverOptions = options; + const hoverDisposables = new DisposableStore(); const hover = this._instantiationService.createInstance(HoverWidget, options); - hover.onDispose(() => this._currentHoverOptions = undefined); + hover.onDispose(() => { + this._currentHoverOptions = undefined; + hoverDisposables.dispose(); + }); const provider = this._contextViewService as IContextViewProvider; provider.showContextView(new HoverContextViewDelegate(hover, focus)); hover.onRequestLayout(() => provider.layout()); + if ('targetElements' in options.target) { + for (const element of options.target.targetElements) { + hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover())); + } + } else { + hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover())); + } if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(e => this._intersectionChange(e, hover), { threshold: 0 }); const firstTargetElement = 'targetElements' in options.target ? options.target.targetElements[0] : options.target; observer.observe(firstTargetElement); - hover.onDispose(() => observer.disconnect()); + hoverDisposables.add(toDisposable(() => observer.disconnect())); } return hover; diff --git a/lib/vscode/src/vs/workbench/services/hover/browser/hoverWidget.ts b/lib/vscode/src/vs/workbench/services/hover/browser/hoverWidget.ts index e1312fe85e52..bf357e33278c 100644 --- a/lib/vscode/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/lib/vscode/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -18,6 +18,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { isString } from 'vs/base/common/types'; const $ = dom.$; type TargetRect = { @@ -66,7 +67,7 @@ export class HoverWidget extends Widget { ) { super(); - this._linkHandler = options.linkHandler || this._openerService.open; + this._linkHandler = options.linkHandler || (url => this._openerService.open(url, { allowCommands: (!isString(options.text) && options.text.isTrusted) })); this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target); diff --git a/lib/vscode/src/vs/workbench/services/issue/electron-sandbox/issueService.ts b/lib/vscode/src/vs/workbench/services/issue/electron-sandbox/issueService.ts index 2f630ed449b7..684aae6847a7 100644 --- a/lib/vscode/src/vs/workbench/services/issue/electron-sandbox/issueService.ts +++ b/lib/vscode/src/vs/workbench/services/issue/electron-sandbox/issueService.ts @@ -6,7 +6,7 @@ import { IssueReporterStyles, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground, listActiveSelectionBackground, listActiveSelectionForeground, listFocusOutline, listFocusBackground, listFocusForeground, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -19,6 +19,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export class WorkbenchIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -29,6 +30,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IProductService private readonly productService: IProductService, @ITASExperimentService private readonly experimentService: ITASExperimentService, @IAuthenticationService private readonly authenticationService: IAuthenticationService @@ -70,15 +72,24 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { }); } const experiments = await this.experimentService.getCurrentExperiments(); - const githubSessions = await this.authenticationService.getSessions('github'); - const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); + + let githubAccessToken = ''; + try { + const githubSessions = await this.authenticationService.getSessions('github'); + const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); + githubAccessToken = potentialSessions[0]?.accessToken; + } catch (e) { + // Ignore + } + const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = Object.assign({ styles: getIssueReporterStyles(theme), zoomLevel: getZoomLevel(), enabledExtensions: extensionData, experiments: experiments?.join('\n'), - githubAccessToken: potentialSessions[0]?.accessToken + restrictedMode: !this.workspaceTrustManagementService.isWorkpaceTrusted(), + githubAccessToken, }, dataOverrides); return this.issueService.openReporter(issueReporterData); } @@ -91,8 +102,14 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { styles: { backgroundColor: getColor(theme, editorBackground), color: getColor(theme, editorForeground), - hoverBackground: getColor(theme, listHoverBackground), - hoverForeground: getColor(theme, listHoverForeground) + listHoverBackground: getColor(theme, listHoverBackground), + listHoverForeground: getColor(theme, listHoverForeground), + listFocusBackground: getColor(theme, listFocusBackground), + listFocusForeground: getColor(theme, listFocusForeground), + listFocusOutline: getColor(theme, listFocusOutline), + listActiveSelectionBackground: getColor(theme, listActiveSelectionBackground), + listActiveSelectionForeground: getColor(theme, listActiveSelectionForeground), + listHoverOutline: getColor(theme, activeContrastBorder), }, platform: platform, applicationName: this.productService.applicationName diff --git a/lib/vscode/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/lib/vscode/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index b4062798117f..cdc7586ccf14 100644 --- a/lib/vscode/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/lib/vscode/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -993,6 +993,7 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { || (keyCode === KeyCode.End) || (keyCode === KeyCode.PageDown) || (keyCode === KeyCode.PageUp) + || (keyCode === KeyCode.Backspace) ) { // "Dispatch" on keyCode for these key codes to workaround issues with remote desktoping software // where the scan codes appear to be incorrect (see https://github.com/microsoft/vscode/issues/24107) diff --git a/lib/vscode/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts b/lib/vscode/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts index 64967e8838c0..d17478838fe1 100644 --- a/lib/vscode/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts +++ b/lib/vscode/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts @@ -5,11 +5,10 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { promises } from 'fs'; import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { ScanCodeBinding } from 'vs/base/common/scanCode'; -import { writeFile } from 'vs/base/node/pfs'; +import { Promises, writeFile } from 'vs/base/node/pfs'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper'; @@ -53,7 +52,7 @@ export function assertResolveUserBinding(mapper: IKeyboardMapper, parts: (Simple } export function readRawMapping(file: string): Promise { - return promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { + return Promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { let contents = buff.toString(); let func = new Function('define', contents); let rawMappings: T | null = null; @@ -67,7 +66,7 @@ export function readRawMapping(file: string): Promise { export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): Promise { const filePath = path.normalize(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}`)); - return promises.readFile(filePath).then((buff) => { + return Promises.readFile(filePath).then((buff) => { const expected = buff.toString().replace(/\r\n/g, '\n'); const actual = mapper.dumpDebugInfo().replace(/\r\n/g, '\n'); if (actual !== expected && writeFileIfDifferent) { diff --git a/lib/vscode/src/vs/workbench/services/layout/browser/layoutService.ts b/lib/vscode/src/vs/workbench/services/layout/browser/layoutService.ts index 1c385ecb0345..83d72d6341e8 100644 --- a/lib/vscode/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/lib/vscode/src/vs/workbench/services/layout/browser/layoutService.ts @@ -15,6 +15,7 @@ export const IWorkbenchLayoutService = refineServiceDecorator