diff --git a/.rebase/CHANGELOG.md b/.rebase/CHANGELOG.md index cd5898d20f5..a3ac4bd9288 100644 --- a/.rebase/CHANGELOG.md +++ b/.rebase/CHANGELOG.md @@ -2,6 +2,14 @@ The file to keep a list of changed files which will potentionaly help to resolve rebase conflicts. +#### @RomanNikitenko +https://github.com/che-incubator/che-code/pull/485 + +- code/package.json +- code/remote/package.json +- code/src/vs/workbench/api/node/proxyResolver.ts +--- + #### @RomanNikitenko https://github.com/che-incubator/che-code/pull/488 diff --git a/code/.eslint-ignore b/code/.eslint-ignore index 12da4a432e1..6fbdf94696e 100644 --- a/code/.eslint-ignore +++ b/code/.eslint-ignore @@ -12,6 +12,7 @@ **/extensions/markdown-math/notebook-out/** **/extensions/notebook-renderers/renderer-out/index.js **/extensions/simple-browser/media/index.js +**/extensions/terminal-suggest/src/completions/** **/extensions/typescript-language-features/test-workspace/** **/extensions/typescript-language-features/extension.webpack.config.js **/extensions/typescript-language-features/extension-browser.webpack.config.js diff --git a/code/.vscode/notebooks/endgame.github-issues b/code/.vscode/notebooks/endgame.github-issues index 2e4c8d82aac..ef066abaeaf 100644 --- a/code/.vscode/notebooks/endgame.github-issues +++ b/code/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"November 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\"" }, { "kind": 1, diff --git a/code/.vscode/notebooks/my-endgame.github-issues b/code/.vscode/notebooks/my-endgame.github-issues index b0be17265cd..b16f0025f56 100644 --- a/code/.vscode/notebooks/my-endgame.github-issues +++ b/code/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"November 2024\"\r\n\r\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/code/ThirdPartyNotices.txt b/code/ThirdPartyNotices.txt index 7f8ba3ad157..a0026469915 100644 --- a/code/ThirdPartyNotices.txt +++ b/code/ThirdPartyNotices.txt @@ -573,7 +573,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.7.8 - MIT +go-syntax 0.7.9 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -1562,6 +1562,22 @@ SOFTWARE. --------------------------------------------------------- +RedCMD/YAML-Syntax-Highlighter 1.3.2 - MIT +https://github.com/RedCMD/YAML-Syntax-Highlighter + +MIT License + +Copyright 2024 RedCMD + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + redhat-developer/vscode-java 1.26.0 - MIT https://github.com/redhat-developer/vscode-java @@ -1735,6 +1751,55 @@ SOFTWARE. --------------------------------------------------------- +Shopify/ruby-lsp 0.0.0 - MIT License +https://github.com/Shopify/ruby-lsp + +The MIT License (MIT) + +Copyright (c) 2022-present, Shopify Inc. + +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. + +================================================================================ +The following files and related configuration in package.json are based on a +sequence of adaptions: grammars/ruby.cson.json, grammars/erb.cson.json, +languages/erb.json. + +Copyright (c) 2016 Peng Lv +Copyright (c) 2017-2019 Stafford Brunk +https://github.com/rubyide/vscode-ruby + + Released under the MIT license + https://github.com/rubyide/vscode-ruby/blob/main/LICENSE.txt + +Copyright (c) 2014 GitHub Inc. +https://github.com/atom/language-ruby + + Released under the MIT license + https://github.com/atom/language-ruby/blob/master/LICENSE.md + +https://github.com/textmate/ruby.tmbundle + https://github.com/textmate/ruby.tmbundle#license +--------------------------------------------------------- + +--------------------------------------------------------- + sumneko/lua.tmbundle 1.0.0 - TextMate Bundle License https://github.com/sumneko/lua.tmbundle @@ -1964,51 +2029,6 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -textmate/ruby.tmbundle 0.0.0 - TextMate Bundle License -https://github.com/textmate/ruby.tmbundle - -Copyright (c) textmate-ruby.tmbundle project authors - -If not otherwise specified (see below), files in this folder fall under the following license: - -Permission to copy, use, modify, sell and distribute this -software is granted. This software is provided "as is" without -express or implied warranty, and with no claim as to its -suitability for any purpose. - -An exception is made for files in readable text which contain their own license information, -or files where an accompanying file exists (in the same directory) with a "-license" suffix added -to the base-name name of the original file, and an extension of txt, html, or similar. For example -"tidy" is accompanied by "tidy-license.txt". ---------------------------------------------------------- - ---------------------------------------------------------- - -textmate/yaml.tmbundle 0.0.0 - TextMate Bundle License -https://github.com/textmate/yaml.tmbundle - -Copyright (c) 2015 FichteFoll - -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. ---------------------------------------------------------- - ---------------------------------------------------------- - trond-snekvik/vscode-rst 1.5.3 - MIT https://github.com/trond-snekvik/vscode-rst diff --git a/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js b/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js index 2d747f56cc7..10fa9087454 100644 --- a/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js +++ b/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../product.json'), 'utf8')); -const shasum = crypto.createHash('sha256'); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const crypto_1 = __importDefault(require("crypto")); +const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../product.json'), 'utf8')); +const shasum = crypto_1.default.createHash('sha256'); for (const ext of productjson.builtInExtensions) { shasum.update(`${ext.name}@${ext.version}`); } diff --git a/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts b/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts index 53d6c501ea9..8abaaccb654 100644 --- a/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts +++ b/code/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../product.json'), 'utf8')); const shasum = crypto.createHash('sha256'); diff --git a/code/build/azure-pipelines/common/computeNodeModulesCacheKey.js b/code/build/azure-pipelines/common/computeNodeModulesCacheKey.js index 976e096fad2..c09c13be9d4 100644 --- a/code/build/azure-pipelines/common/computeNodeModulesCacheKey.js +++ b/code/build/azure-pipelines/common/computeNodeModulesCacheKey.js @@ -3,21 +3,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const crypto_1 = __importDefault(require("crypto")); const { dirs } = require('../../npm/dirs'); -const ROOT = path.join(__dirname, '../../../'); -const shasum = crypto.createHash('sha256'); -shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt'))); -shasum.update(fs.readFileSync(path.join(ROOT, '.npmrc'))); -shasum.update(fs.readFileSync(path.join(ROOT, 'build', '.npmrc'))); -shasum.update(fs.readFileSync(path.join(ROOT, 'remote', '.npmrc'))); +const ROOT = path_1.default.join(__dirname, '../../../'); +const shasum = crypto_1.default.createHash('sha256'); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'build/.cachesalt'))); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, '.npmrc'))); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'build', '.npmrc'))); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'remote', '.npmrc'))); // Add `package.json` and `package-lock.json` files for (const dir of dirs) { - const packageJsonPath = path.join(ROOT, dir, 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); + const packageJsonPath = path_1.default.join(ROOT, dir, 'package.json'); + const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()); const relevantPackageJsonSections = { dependencies: packageJson.dependencies, devDependencies: packageJson.devDependencies, @@ -26,8 +29,8 @@ for (const dir of dirs) { distro: packageJson.distro }; shasum.update(JSON.stringify(relevantPackageJsonSections)); - const packageLockPath = path.join(ROOT, dir, 'package-lock.json'); - shasum.update(fs.readFileSync(packageLockPath)); + const packageLockPath = path_1.default.join(ROOT, dir, 'package-lock.json'); + shasum.update(fs_1.default.readFileSync(packageLockPath)); } // Add any other command line arguments for (let i = 2; i < process.argv.length; i++) { diff --git a/code/build/azure-pipelines/common/computeNodeModulesCacheKey.ts b/code/build/azure-pipelines/common/computeNodeModulesCacheKey.ts index 0940c929b54..57b35dc78de 100644 --- a/code/build/azure-pipelines/common/computeNodeModulesCacheKey.ts +++ b/code/build/azure-pipelines/common/computeNodeModulesCacheKey.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; const { dirs } = require('../../npm/dirs'); const ROOT = path.join(__dirname, '../../../'); diff --git a/code/build/azure-pipelines/common/listNodeModules.js b/code/build/azure-pipelines/common/listNodeModules.js index aaa44c51a12..301b5f930b6 100644 --- a/code/build/azure-pipelines/common/listNodeModules.js +++ b/code/build/azure-pipelines/common/listNodeModules.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); if (process.argv.length !== 3) { console.error('Usage: node listNodeModules.js OUTPUT_FILE'); process.exit(-1); } -const ROOT = path.join(__dirname, '../../../'); +const ROOT = path_1.default.join(__dirname, '../../../'); function findNodeModulesFiles(location, inNodeModules, result) { - const entries = fs.readdirSync(path.join(ROOT, location)); + const entries = fs_1.default.readdirSync(path_1.default.join(ROOT, location)); for (const entry of entries) { const entryPath = `${location}/${entry}`; if (/(^\/out)|(^\/src$)|(^\/.git$)|(^\/.build$)/.test(entryPath)) { @@ -20,7 +23,7 @@ function findNodeModulesFiles(location, inNodeModules, result) { } let stat; try { - stat = fs.statSync(path.join(ROOT, entryPath)); + stat = fs_1.default.statSync(path_1.default.join(ROOT, entryPath)); } catch (err) { continue; @@ -37,5 +40,5 @@ function findNodeModulesFiles(location, inNodeModules, result) { } const result = []; findNodeModulesFiles('', false, result); -fs.writeFileSync(process.argv[2], result.join('\n') + '\n'); +fs_1.default.writeFileSync(process.argv[2], result.join('\n') + '\n'); //# sourceMappingURL=listNodeModules.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/common/listNodeModules.ts b/code/build/azure-pipelines/common/listNodeModules.ts index aca461f8b5f..fb85b25cfd1 100644 --- a/code/build/azure-pipelines/common/listNodeModules.ts +++ b/code/build/azure-pipelines/common/listNodeModules.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; if (process.argv.length !== 3) { console.error('Usage: node listNodeModules.js OUTPUT_FILE'); diff --git a/code/build/azure-pipelines/common/publish.js b/code/build/azure-pipelines/common/publish.js index bcebd076c28..599f12f47af 100644 --- a/code/build/azure-pipelines/common/publish.js +++ b/code/build/azure-pipelines/common/publish.js @@ -3,21 +3,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const stream_1 = require("stream"); const promises_1 = require("node:stream/promises"); -const yauzl = require("yauzl"); -const crypto = require("crypto"); +const yauzl_1 = __importDefault(require("yauzl")); +const crypto_1 = __importDefault(require("crypto")); const retry_1 = require("./retry"); const cosmos_1 = require("@azure/cosmos"); -const cp = require("child_process"); -const os = require("os"); +const child_process_1 = __importDefault(require("child_process")); +const os_1 = __importDefault(require("os")); const node_worker_threads_1 = require("node:worker_threads"); const msal_node_1 = require("@azure/msal-node"); const storage_blob_1 = require("@azure/storage-blob"); -const jws = require("jws"); +const jws_1 = __importDefault(require("jws")); const node_timers_1 = require("node:timers"); function e(name) { const result = process.env[name]; @@ -28,7 +31,7 @@ function e(name) { } function hashStream(hashName, stream) { return new Promise((c, e) => { - const shasum = crypto.createHash(hashName); + const shasum = crypto_1.default.createHash(hashName); stream .on('data', shasum.update.bind(shasum)) .on('error', e) @@ -50,38 +53,38 @@ function getCertificateBuffer(input) { } function getThumbprint(input, algorithm) { const buffer = getCertificateBuffer(input); - return crypto.createHash(algorithm).update(buffer).digest(); + return crypto_1.default.createHash(algorithm).update(buffer).digest(); } function getKeyFromPFX(pfx) { - const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); - const pemKeyPath = path.join(os.tmpdir(), 'key.pem'); + const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx'); + const pemKeyPath = path_1.default.join(os_1.default.tmpdir(), 'key.pem'); try { const pfxCertificate = Buffer.from(pfx, 'base64'); - fs.writeFileSync(pfxCertificatePath, pfxCertificate); - cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); - const raw = fs.readFileSync(pemKeyPath, 'utf-8'); + fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate); + child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); + const raw = fs_1.default.readFileSync(pemKeyPath, 'utf-8'); const result = raw.match(/-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g)[0]; return result; } finally { - fs.rmSync(pfxCertificatePath, { force: true }); - fs.rmSync(pemKeyPath, { force: true }); + fs_1.default.rmSync(pfxCertificatePath, { force: true }); + fs_1.default.rmSync(pemKeyPath, { force: true }); } } function getCertificatesFromPFX(pfx) { - const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); - const pemCertificatePath = path.join(os.tmpdir(), 'cert.pem'); + const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx'); + const pemCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pem'); try { const pfxCertificate = Buffer.from(pfx, 'base64'); - fs.writeFileSync(pfxCertificatePath, pfxCertificate); - cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); - const raw = fs.readFileSync(pemCertificatePath, 'utf-8'); + fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate); + child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); + const raw = fs_1.default.readFileSync(pemCertificatePath, 'utf-8'); const matches = raw.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g); return matches ? matches.reverse() : []; } finally { - fs.rmSync(pfxCertificatePath, { force: true }); - fs.rmSync(pemCertificatePath, { force: true }); + fs_1.default.rmSync(pfxCertificatePath, { force: true }); + fs_1.default.rmSync(pemCertificatePath, { force: true }); } } class ESRPReleaseService { @@ -122,7 +125,7 @@ class ESRPReleaseService { this.containerClient = containerClient; } async createRelease(version, filePath, friendlyFileName) { - const correlationId = crypto.randomUUID(); + const correlationId = crypto_1.default.randomUUID(); const blobClient = this.containerClient.getBlockBlobClient(correlationId); this.log(`Uploading ${filePath} to ${blobClient.url}`); await blobClient.uploadFile(filePath); @@ -161,8 +164,8 @@ class ESRPReleaseService { } } async submitRelease(version, filePath, friendlyFileName, correlationId, blobClient) { - const size = fs.statSync(filePath).size; - const hash = await hashStream('sha256', fs.createReadStream(filePath)); + const size = fs_1.default.statSync(filePath).size; + const hash = await hashStream('sha256', fs_1.default.createReadStream(filePath)); const message = { customerCorrelationId: correlationId, esrpCorrelationId: correlationId, @@ -192,7 +195,7 @@ class ESRPReleaseService { intent: 'filedownloadlinkgeneration' }, files: [{ - name: path.basename(filePath), + name: path_1.default.basename(filePath), friendlyFileName, tenantFileLocation: blobClient.url, tenantFileLocationType: 'AzureBlob', @@ -247,7 +250,7 @@ class ESRPReleaseService { return await res.json(); } async generateJwsToken(message) { - return jws.sign({ + return jws_1.default.sign({ header: { alg: 'RS256', crit: ['exp', 'x5t'], @@ -268,19 +271,19 @@ class State { set = new Set(); constructor() { const pipelineWorkspacePath = e('PIPELINE_WORKSPACE'); - const previousState = fs.readdirSync(pipelineWorkspacePath) + const previousState = fs_1.default.readdirSync(pipelineWorkspacePath) .map(name => /^artifacts_processed_(\d+)$/.exec(name)) .filter((match) => !!match) .map(match => ({ name: match[0], attempt: Number(match[1]) })) .sort((a, b) => b.attempt - a.attempt)[0]; if (previousState) { - const previousStatePath = path.join(pipelineWorkspacePath, previousState.name, previousState.name + '.txt'); - fs.readFileSync(previousStatePath, 'utf8').split(/\n/).filter(name => !!name).forEach(name => this.set.add(name)); + const previousStatePath = path_1.default.join(pipelineWorkspacePath, previousState.name, previousState.name + '.txt'); + fs_1.default.readFileSync(previousStatePath, 'utf8').split(/\n/).filter(name => !!name).forEach(name => this.set.add(name)); } const stageAttempt = e('SYSTEM_STAGEATTEMPT'); - this.statePath = path.join(pipelineWorkspacePath, `artifacts_processed_${stageAttempt}`, `artifacts_processed_${stageAttempt}.txt`); - fs.mkdirSync(path.dirname(this.statePath), { recursive: true }); - fs.writeFileSync(this.statePath, [...this.set.values()].map(name => `${name}\n`).join('')); + this.statePath = path_1.default.join(pipelineWorkspacePath, `artifacts_processed_${stageAttempt}`, `artifacts_processed_${stageAttempt}.txt`); + fs_1.default.mkdirSync(path_1.default.dirname(this.statePath), { recursive: true }); + fs_1.default.writeFileSync(this.statePath, [...this.set.values()].map(name => `${name}\n`).join('')); } get size() { return this.set.size; @@ -290,7 +293,7 @@ class State { } add(name) { this.set.add(name); - fs.appendFileSync(this.statePath, `${name}\n`); + fs_1.default.appendFileSync(this.statePath, `${name}\n`); } [Symbol.iterator]() { return this.set[Symbol.iterator](); @@ -336,7 +339,7 @@ async function downloadArtifact(artifact, downloadPath) { if (!res.ok) { throw new Error(`Unexpected status code: ${res.status}`); } - await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(res.body), fs.createWriteStream(downloadPath)); + await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(res.body), fs_1.default.createWriteStream(downloadPath)); } finally { clearTimeout(timeout); @@ -344,7 +347,7 @@ async function downloadArtifact(artifact, downloadPath) { } async function unzip(packagePath, outputPath) { return new Promise((resolve, reject) => { - yauzl.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { + yauzl_1.default.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { if (err) { return reject(err); } @@ -358,9 +361,9 @@ async function unzip(packagePath, outputPath) { if (err) { return reject(err); } - const filePath = path.join(outputPath, entry.fileName); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - const ostream = fs.createWriteStream(filePath); + const filePath = path_1.default.join(outputPath, entry.fileName); + fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true }); + const ostream = fs_1.default.createWriteStream(filePath); ostream.on('finish', () => { result.push(filePath); zipfile.readEntry(); @@ -523,7 +526,7 @@ async function processArtifact(artifact, filePath) { const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); const quality = e('VSCODE_QUALITY'); const version = e('BUILD_SOURCEVERSION'); - const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; + const friendlyFileName = `${quality}/${version}/${path_1.default.basename(filePath)}`; const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); const leasesContainerClient = blobServiceClient.getContainerClient('leases'); await leasesContainerClient.createIfNotExists(); @@ -546,8 +549,8 @@ async function processArtifact(artifact, filePath) { const isLegacy = artifact.name.includes('_legacy'); const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); const type = getRealType(unprocessedType); - const size = fs.statSync(filePath).size; - const stream = fs.createReadStream(filePath); + const size = fs_1.default.statSync(filePath).size; + const stream = fs_1.default.createReadStream(filePath); const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; log('Creating asset...'); @@ -627,12 +630,12 @@ async function main() { continue; } console.log(`[${artifact.name}] Found new artifact`); - const artifactZipPath = path.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`); + const artifactZipPath = path_1.default.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`); await (0, retry_1.retry)(async (attempt) => { const start = Date.now(); console.log(`[${artifact.name}] Downloading (attempt ${attempt})...`); await downloadArtifact(artifact, artifactZipPath); - const archiveSize = fs.statSync(artifactZipPath).size; + const archiveSize = fs_1.default.statSync(artifactZipPath).size; const downloadDurationS = (Date.now() - start) / 1000; const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS); console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`); diff --git a/code/build/azure-pipelines/common/publish.ts b/code/build/azure-pipelines/common/publish.ts index b8b99c3855b..39d189c05fa 100644 --- a/code/build/azure-pipelines/common/publish.ts +++ b/code/build/azure-pipelines/common/publish.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; import { Readable } from 'stream'; import type { ReadableStream } from 'stream/web'; import { pipeline } from 'node:stream/promises'; -import * as yauzl from 'yauzl'; -import * as crypto from 'crypto'; +import yauzl from 'yauzl'; +import crypto from 'crypto'; import { retry } from './retry'; import { CosmosClient } from '@azure/cosmos'; -import * as cp from 'child_process'; -import * as os from 'os'; +import cp from 'child_process'; +import os from 'os'; import { Worker, isMainThread, workerData } from 'node:worker_threads'; import { ConfidentialClientApplication } from '@azure/msal-node'; import { BlobClient, BlobServiceClient, BlockBlobClient, ContainerClient } from '@azure/storage-blob'; -import * as jws from 'jws'; +import jws from 'jws'; import { clearInterval, setInterval } from 'node:timers'; function e(name: string): string { diff --git a/code/build/azure-pipelines/common/sign-win32.js b/code/build/azure-pipelines/common/sign-win32.js index aa197bb1198..f4e3f27c1f2 100644 --- a/code/build/azure-pipelines/common/sign-win32.js +++ b/code/build/azure-pipelines/common/sign-win32.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const sign_1 = require("./sign"); -const path = require("path"); +const path_1 = __importDefault(require("path")); (0, sign_1.main)([ process.env['EsrpCliDllPath'], 'sign-windows', - path.dirname(process.argv[2]), - path.basename(process.argv[2]) + path_1.default.dirname(process.argv[2]), + path_1.default.basename(process.argv[2]) ]); //# sourceMappingURL=sign-win32.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/common/sign-win32.ts b/code/build/azure-pipelines/common/sign-win32.ts index c2f3dbda151..ad88435b5a3 100644 --- a/code/build/azure-pipelines/common/sign-win32.ts +++ b/code/build/azure-pipelines/common/sign-win32.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { main } from './sign'; -import * as path from 'path'; +import path from 'path'; main([ process.env['EsrpCliDllPath']!, diff --git a/code/build/azure-pipelines/common/sign.js b/code/build/azure-pipelines/common/sign.js index df25de29399..fd87772b3b8 100644 --- a/code/build/azure-pipelines/common/sign.js +++ b/code/build/azure-pipelines/common/sign.js @@ -3,25 +3,28 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.Temp = void 0; exports.main = main; -const cp = require("child_process"); -const fs = require("fs"); -const crypto = require("crypto"); -const path = require("path"); -const os = require("os"); +const child_process_1 = __importDefault(require("child_process")); +const fs_1 = __importDefault(require("fs")); +const crypto_1 = __importDefault(require("crypto")); +const path_1 = __importDefault(require("path")); +const os_1 = __importDefault(require("os")); class Temp { _files = []; tmpNameSync() { - const file = path.join(os.tmpdir(), crypto.randomBytes(20).toString('hex')); + const file = path_1.default.join(os_1.default.tmpdir(), crypto_1.default.randomBytes(20).toString('hex')); this._files.push(file); return file; } dispose() { for (const file of this._files) { try { - fs.unlinkSync(file); + fs_1.default.unlinkSync(file); } catch (err) { // noop @@ -126,20 +129,20 @@ function getParams(type) { function main([esrpCliPath, type, folderPath, pattern]) { const tmp = new Temp(); process.on('exit', () => tmp.dispose()); - const key = crypto.randomBytes(32); - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + const key = crypto_1.default.randomBytes(32); + const iv = crypto_1.default.randomBytes(16); + const cipher = crypto_1.default.createCipheriv('aes-256-cbc', key, iv); const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN'].trim(), 'utf8', 'hex') + cipher.final('hex'); const encryptionDetailsPath = tmp.tmpNameSync(); - fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); + fs_1.default.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); const encryptedTokenPath = tmp.tmpNameSync(); - fs.writeFileSync(encryptedTokenPath, encryptedToken); + fs_1.default.writeFileSync(encryptedTokenPath, encryptedToken); const patternPath = tmp.tmpNameSync(); - fs.writeFileSync(patternPath, pattern); + fs_1.default.writeFileSync(patternPath, pattern); const paramsPath = tmp.tmpNameSync(); - fs.writeFileSync(paramsPath, JSON.stringify(getParams(type))); - const dotnetVersion = cp.execSync('dotnet --version', { encoding: 'utf8' }).trim(); - const adoTaskVersion = path.basename(path.dirname(path.dirname(esrpCliPath))); + fs_1.default.writeFileSync(paramsPath, JSON.stringify(getParams(type))); + const dotnetVersion = child_process_1.default.execSync('dotnet --version', { encoding: 'utf8' }).trim(); + const adoTaskVersion = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(esrpCliPath))); const federatedTokenData = { jobId: process.env['SYSTEM_JOBID'], planId: process.env['SYSTEM_PLANID'], @@ -149,7 +152,7 @@ function main([esrpCliPath, type, folderPath, pattern]) { managedIdentityId: process.env['VSCODE_ESRP_CLIENT_ID'], managedIdentityTenantId: process.env['VSCODE_ESRP_TENANT_ID'], serviceConnectionId: process.env['VSCODE_ESRP_SERVICE_CONNECTION_ID'], - tempDirectory: os.tmpdir(), + tempDirectory: os_1.default.tmpdir(), systemAccessToken: encryptedTokenPath, encryptionKey: encryptionDetailsPath }; @@ -188,7 +191,7 @@ function main([esrpCliPath, type, folderPath, pattern]) { '-federatedTokenData', JSON.stringify(federatedTokenData) ]; try { - cp.execFileSync('dotnet', args, { stdio: 'inherit' }); + child_process_1.default.execFileSync('dotnet', args, { stdio: 'inherit' }); } catch (err) { console.error('ESRP failed'); diff --git a/code/build/azure-pipelines/common/sign.ts b/code/build/azure-pipelines/common/sign.ts index e5f42e87da2..19a288483c8 100644 --- a/code/build/azure-pipelines/common/sign.ts +++ b/code/build/azure-pipelines/common/sign.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; -import * as fs from 'fs'; -import * as crypto from 'crypto'; -import * as path from 'path'; -import * as os from 'os'; +import cp from 'child_process'; +import fs from 'fs'; +import crypto from 'crypto'; +import path from 'path'; +import os from 'os'; export class Temp { private _files: string[] = []; diff --git a/code/build/azure-pipelines/distro/mixin-npm.js b/code/build/azure-pipelines/distro/mixin-npm.js index 0c61bb3dcf4..87958a5d449 100644 --- a/code/build/azure-pipelines/distro/mixin-npm.js +++ b/code/build/azure-pipelines/distro/mixin-npm.js @@ -3,24 +3,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const { dirs } = require('../../npm/dirs'); function log(...args) { console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); } function mixin(mixinPath) { - if (!fs.existsSync(`${mixinPath}/node_modules`)) { + if (!fs_1.default.existsSync(`${mixinPath}/node_modules`)) { log(`Skipping distro npm dependencies: ${mixinPath} (no node_modules)`); return; } log(`Mixing in distro npm dependencies: ${mixinPath}`); - const distroPackageJson = JSON.parse(fs.readFileSync(`${mixinPath}/package.json`, 'utf8')); - const targetPath = path.relative('.build/distro/npm', mixinPath); + const distroPackageJson = JSON.parse(fs_1.default.readFileSync(`${mixinPath}/package.json`, 'utf8')); + const targetPath = path_1.default.relative('.build/distro/npm', mixinPath); for (const dependency of Object.keys(distroPackageJson.dependencies)) { - fs.rmSync(`./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true }); - fs.cpSync(`${mixinPath}/node_modules/${dependency}`, `./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true, dereference: true }); + fs_1.default.rmSync(`./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true }); + fs_1.default.cpSync(`${mixinPath}/node_modules/${dependency}`, `./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true, dereference: true }); } log(`Mixed in distro npm dependencies: ${mixinPath} ✔︎`); } diff --git a/code/build/azure-pipelines/distro/mixin-npm.ts b/code/build/azure-pipelines/distro/mixin-npm.ts index da5eb24ca28..6e32f10db50 100644 --- a/code/build/azure-pipelines/distro/mixin-npm.ts +++ b/code/build/azure-pipelines/distro/mixin-npm.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; const { dirs } = require('../../npm/dirs') as { dirs: string[] }; function log(...args: any[]): void { diff --git a/code/build/azure-pipelines/distro/mixin-quality.js b/code/build/azure-pipelines/distro/mixin-quality.js index 6e011b5a1e9..335f63ca1fc 100644 --- a/code/build/azure-pipelines/distro/mixin-quality.js +++ b/code/build/azure-pipelines/distro/mixin-quality.js @@ -3,9 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); function log(...args) { console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); } @@ -16,12 +19,12 @@ function main() { } log(`Mixing in distro quality...`); const basePath = `.build/distro/mixin/${quality}`; - for (const name of fs.readdirSync(basePath)) { - const distroPath = path.join(basePath, name); - const ossPath = path.relative(basePath, distroPath); + for (const name of fs_1.default.readdirSync(basePath)) { + const distroPath = path_1.default.join(basePath, name); + const ossPath = path_1.default.relative(basePath, distroPath); if (ossPath === 'product.json') { - const distro = JSON.parse(fs.readFileSync(distroPath, 'utf8')); - const oss = JSON.parse(fs.readFileSync(ossPath, 'utf8')); + const distro = JSON.parse(fs_1.default.readFileSync(distroPath, 'utf8')); + const oss = JSON.parse(fs_1.default.readFileSync(ossPath, 'utf8')); let builtInExtensions = oss.builtInExtensions; if (Array.isArray(distro.builtInExtensions)) { log('Overwriting built-in extensions:', distro.builtInExtensions.map(e => e.name)); @@ -41,10 +44,10 @@ function main() { log('Inheriting OSS built-in extensions', builtInExtensions.map(e => e.name)); } const result = { webBuiltInExtensions: oss.webBuiltInExtensions, ...distro, builtInExtensions }; - fs.writeFileSync(ossPath, JSON.stringify(result, null, '\t'), 'utf8'); + fs_1.default.writeFileSync(ossPath, JSON.stringify(result, null, '\t'), 'utf8'); } else { - fs.cpSync(distroPath, ossPath, { force: true, recursive: true }); + fs_1.default.cpSync(distroPath, ossPath, { force: true, recursive: true }); } log(distroPath, '✔︎'); } diff --git a/code/build/azure-pipelines/distro/mixin-quality.ts b/code/build/azure-pipelines/distro/mixin-quality.ts index b9b3c4f6c42..29c90f00a65 100644 --- a/code/build/azure-pipelines/distro/mixin-quality.ts +++ b/code/build/azure-pipelines/distro/mixin-quality.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; interface IBuiltInExtension { readonly name: string; diff --git a/code/build/azure-pipelines/publish-types/check-version.js b/code/build/azure-pipelines/publish-types/check-version.js index 9e93a7fa4c9..5bd80a69bbf 100644 --- a/code/build/azure-pipelines/publish-types/check-version.js +++ b/code/build/azure-pipelines/publish-types/check-version.js @@ -3,11 +3,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const cp = require("child_process"); +const child_process_1 = __importDefault(require("child_process")); let tag = ''; try { - tag = cp + tag = child_process_1.default .execSync('git describe --tags `git rev-list --tags --max-count=1`') .toString() .trim(); diff --git a/code/build/azure-pipelines/publish-types/check-version.ts b/code/build/azure-pipelines/publish-types/check-version.ts index 35c5a511593..4496ed93af1 100644 --- a/code/build/azure-pipelines/publish-types/check-version.ts +++ b/code/build/azure-pipelines/publish-types/check-version.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; +import cp from 'child_process'; let tag = ''; try { diff --git a/code/build/azure-pipelines/publish-types/update-types.js b/code/build/azure-pipelines/publish-types/update-types.js index ed2deded3fc..29f9bfcf66e 100644 --- a/code/build/azure-pipelines/publish-types/update-types.js +++ b/code/build/azure-pipelines/publish-types/update-types.js @@ -3,19 +3,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const cp = require("child_process"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const child_process_1 = __importDefault(require("child_process")); +const path_1 = __importDefault(require("path")); let tag = ''; try { - tag = cp + tag = child_process_1.default .execSync('git describe --tags `git rev-list --tags --max-count=1`') .toString() .trim(); const dtsUri = `https://raw.githubusercontent.com/microsoft/vscode/${tag}/src/vscode-dts/vscode.d.ts`; - const outPath = path.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); - cp.execSync(`curl ${dtsUri} --output ${outPath}`); + const outPath = path_1.default.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); + child_process_1.default.execSync(`curl ${dtsUri} --output ${outPath}`); updateDTSFile(outPath, tag); console.log(`Done updating vscode.d.ts at ${outPath}`); } @@ -25,9 +28,9 @@ catch (err) { process.exit(1); } function updateDTSFile(outPath, tag) { - const oldContent = fs.readFileSync(outPath, 'utf-8'); + const oldContent = fs_1.default.readFileSync(outPath, 'utf-8'); const newContent = getNewFileContent(oldContent, tag); - fs.writeFileSync(outPath, newContent); + fs_1.default.writeFileSync(outPath, newContent); } function repeat(str, times) { const result = new Array(times); diff --git a/code/build/azure-pipelines/publish-types/update-types.ts b/code/build/azure-pipelines/publish-types/update-types.ts index a727647e64a..0f99b07cf9a 100644 --- a/code/build/azure-pipelines/publish-types/update-types.ts +++ b/code/build/azure-pipelines/publish-types/update-types.ts @@ -3,9 +3,9 @@ * 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 path from 'path'; +import fs from 'fs'; +import cp from 'child_process'; +import path from 'path'; let tag = ''; try { diff --git a/code/build/azure-pipelines/upload-cdn.js b/code/build/azure-pipelines/upload-cdn.js index 8ec40a0108e..3174c8dfbaf 100644 --- a/code/build/azure-pipelines/upload-cdn.js +++ b/code/build/azure-pipelines/upload-cdn.js @@ -3,18 +3,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const es = require("event-stream"); -const Vinyl = require("vinyl"); -const vfs = require("vinyl-fs"); -const filter = require("gulp-filter"); -const gzip = require("gulp-gzip"); -const mime = require("mime"); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_1 = __importDefault(require("vinyl")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const gulp_gzip_1 = __importDefault(require("gulp-gzip")); +const mime_1 = __importDefault(require("mime")); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); -mime.define({ +mime_1.default.define({ 'application/typescript': ['ts'], 'application/json': ['code-snippets'], }); @@ -82,30 +85,30 @@ async function main() { cacheControl: 'max-age=31536000, public' } }); - const all = vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) - .pipe(filter(f => !f.isDirectory())); + const all = vinyl_fs_1.default.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) + .pipe((0, gulp_filter_1.default)(f => !f.isDirectory())); const compressed = all - .pipe(filter(f => MimeTypesToCompress.has(mime.lookup(f.path)))) - .pipe(gzip({ append: false })) + .pipe((0, gulp_filter_1.default)(f => MimeTypesToCompress.has(mime_1.default.lookup(f.path)))) + .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(azure.upload(options(true))); const uncompressed = all - .pipe(filter(f => !MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe((0, gulp_filter_1.default)(f => !MimeTypesToCompress.has(mime_1.default.lookup(f.path)))) .pipe(azure.upload(options(false))); - const out = es.merge(compressed, uncompressed) - .pipe(es.through(function (f) { + const out = event_stream_1.default.merge(compressed, uncompressed) + .pipe(event_stream_1.default.through(function (f) { console.log('Uploaded:', f.relative); files.push(f.relative); this.emit('data', f); })); console.log(`Uploading files to CDN...`); // debug await wait(out); - const listing = new Vinyl({ + const listing = new vinyl_1.default({ path: 'files.txt', contents: Buffer.from(files.join('\n')), stat: { mode: 0o666 } }); - const filesOut = es.readArray([listing]) - .pipe(gzip({ append: false })) + const filesOut = event_stream_1.default.readArray([listing]) + .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(azure.upload(options(true))); console.log(`Uploading: files.txt (${files.length} files)`); // debug await wait(filesOut); diff --git a/code/build/azure-pipelines/upload-cdn.ts b/code/build/azure-pipelines/upload-cdn.ts index a4a5857afe5..8ca5e03f1f8 100644 --- a/code/build/azure-pipelines/upload-cdn.ts +++ b/code/build/azure-pipelines/upload-cdn.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as Vinyl from 'vinyl'; -import * as vfs from 'vinyl-fs'; -import * as filter from 'gulp-filter'; -import * as gzip from 'gulp-gzip'; -import * as mime from 'mime'; +import es from 'event-stream'; +import Vinyl from 'vinyl'; +import vfs from 'vinyl-fs'; +import filter from 'gulp-filter'; +import gzip from 'gulp-gzip'; +import mime from 'mime'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); diff --git a/code/build/azure-pipelines/upload-nlsmetadata.js b/code/build/azure-pipelines/upload-nlsmetadata.js index de75dcb8b3a..146f804ce69 100644 --- a/code/build/azure-pipelines/upload-nlsmetadata.js +++ b/code/build/azure-pipelines/upload-nlsmetadata.js @@ -3,11 +3,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const es = require("event-stream"); -const vfs = require("vinyl-fs"); -const merge = require("gulp-merge-json"); -const gzip = require("gulp-gzip"); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); +const gulp_gzip_1 = __importDefault(require("gulp-gzip")); const identity_1 = require("@azure/identity"); const path = require("path"); const fs_1 = require("fs"); @@ -16,12 +19,12 @@ const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); function main() { return new Promise((c, e) => { - const combinedMetadataJson = es.merge( + const combinedMetadataJson = event_stream_1.default.merge( // vscode: we are not using `out-build/nls.metadata.json` here because // it includes metadata for translators for `keys`. but for our purpose // we want only the `keys` and `messages` as `string`. - es.merge(vfs.src('out-build/nls.keys.json', { base: 'out-build' }), vfs.src('out-build/nls.messages.json', { base: 'out-build' })) - .pipe(merge({ + event_stream_1.default.merge(vinyl_fs_1.default.src('out-build/nls.keys.json', { base: 'out-build' }), vinyl_fs_1.default.src('out-build/nls.messages.json', { base: 'out-build' })) + .pipe((0, gulp_merge_json_1.default)({ fileName: 'vscode.json', jsonSpace: '', concatArrays: true, @@ -37,7 +40,7 @@ function main() { } })), // extensions - vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe(merge({ + vinyl_fs_1.default.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vinyl_fs_1.default.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vinyl_fs_1.default.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe((0, gulp_merge_json_1.default)({ fileName: 'combined.nls.metadata.json', jsonSpace: '', concatArrays: true, @@ -93,11 +96,11 @@ function main() { return { [key]: parsedJson }; }, })); - const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); - es.merge(combinedMetadataJson, nlsMessagesJs) - .pipe(gzip({ append: false })) - .pipe(vfs.dest('./nlsMetadata')) - .pipe(es.through(function (data) { + const nlsMessagesJs = vinyl_fs_1.default.src('out-build/nls.messages.js', { base: 'out-build' }); + event_stream_1.default.merge(combinedMetadataJson, nlsMessagesJs) + .pipe((0, gulp_gzip_1.default)({ append: false })) + .pipe(vinyl_fs_1.default.dest('./nlsMetadata')) + .pipe(event_stream_1.default.through(function (data) { console.log(`Uploading ${data.path}`); // trigger artifact upload console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); diff --git a/code/build/azure-pipelines/upload-nlsmetadata.ts b/code/build/azure-pipelines/upload-nlsmetadata.ts index 89a9eb6c536..7337156f577 100644 --- a/code/build/azure-pipelines/upload-nlsmetadata.ts +++ b/code/build/azure-pipelines/upload-nlsmetadata.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as Vinyl from 'vinyl'; -import * as vfs from 'vinyl-fs'; -import * as merge from 'gulp-merge-json'; -import * as gzip from 'gulp-gzip'; +import es from 'event-stream'; +import Vinyl from 'vinyl'; +import vfs from 'vinyl-fs'; +import merge from 'gulp-merge-json'; +import gzip from 'gulp-gzip'; import { ClientAssertionCredential } from '@azure/identity'; import path = require('path'); import { readFileSync } from 'fs'; diff --git a/code/build/azure-pipelines/upload-sourcemaps.js b/code/build/azure-pipelines/upload-sourcemaps.js index 6f5f73fb8b0..fe267275b5b 100644 --- a/code/build/azure-pipelines/upload-sourcemaps.js +++ b/code/build/azure-pipelines/upload-sourcemaps.js @@ -3,23 +3,58 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const es = require("event-stream"); -const vfs = require("vinyl-fs"); -const util = require("../lib/util"); -// @ts-ignore -const deps = require("../lib/dependencies"); +const path_1 = __importDefault(require("path")); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const util = __importStar(require("../lib/util")); +const dependencies_1 = require("../lib/dependencies"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); // optionally allow to pass in explicit base/maps to upload const [, , base, maps] = process.argv; function src(base, maps = `${base}/**/*.map`) { - return vfs.src(maps, { base }) - .pipe(es.mapSync((f) => { + return vinyl_fs_1.default.src(maps, { base }) + .pipe(event_stream_1.default.mapSync((f) => { f.path = `${f.base}/core/${f.relative}`; return f; })); @@ -30,13 +65,13 @@ function main() { if (!base) { const vs = src('out-vscode-min'); // client source-maps only sources.push(vs); - const productionDependencies = deps.getProductionDependencies(root); - const productionDependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => `./${d}/**/*.map`); - const nodeModules = vfs.src(productionDependenciesSrc, { base: '.' }) - .pipe(util.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) - .pipe(util.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`))); + const productionDependencies = (0, dependencies_1.getProductionDependencies)(root); + const productionDependenciesSrc = productionDependencies.map((d) => path_1.default.relative(root, d)).map((d) => `./${d}/**/*.map`); + const nodeModules = vinyl_fs_1.default.src(productionDependenciesSrc, { base: '.' }) + .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) + .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`))); sources.push(nodeModules); - const extensionsOut = vfs.src(['.build/extensions/**/*.js.map', '!**/node_modules/**'], { base: '.build' }); + const extensionsOut = vinyl_fs_1.default.src(['.build/extensions/**/*.js.map', '!**/node_modules/**'], { base: '.build' }); sources.push(extensionsOut); } // specific client base/maps @@ -44,8 +79,8 @@ function main() { sources.push(src(base, maps)); } return new Promise((c, e) => { - es.merge(...sources) - .pipe(es.through(function (data) { + event_stream_1.default.merge(...sources) + .pipe(event_stream_1.default.through(function (data) { console.log('Uploading Sourcemap', data.relative); // debug this.emit('data', data); })) diff --git a/code/build/azure-pipelines/upload-sourcemaps.ts b/code/build/azure-pipelines/upload-sourcemaps.ts index 2eb5e696983..d2950368ee0 100644 --- a/code/build/azure-pipelines/upload-sourcemaps.ts +++ b/code/build/azure-pipelines/upload-sourcemaps.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as es from 'event-stream'; -import * as Vinyl from 'vinyl'; -import * as vfs from 'vinyl-fs'; +import path from 'path'; +import es from 'event-stream'; +import Vinyl from 'vinyl'; +import vfs from 'vinyl-fs'; import * as util from '../lib/util'; -// @ts-ignore -import * as deps from '../lib/dependencies'; +import { getProductionDependencies } from '../lib/dependencies'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); @@ -36,8 +35,8 @@ function main(): Promise { const vs = src('out-vscode-min'); // client source-maps only sources.push(vs); - const productionDependencies = deps.getProductionDependencies(root); - const productionDependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => `./${d}/**/*.map`); + const productionDependencies = getProductionDependencies(root); + const productionDependenciesSrc = productionDependencies.map((d: string) => path.relative(root, d)).map((d: string) => `./${d}/**/*.map`); const nodeModules = vfs.src(productionDependenciesSrc, { base: '.' }) .pipe(util.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`))); diff --git a/code/build/darwin/create-universal-app.js b/code/build/darwin/create-universal-app.js index bced5a7166f..535d46eb174 100644 --- a/code/build/darwin/create-universal-app.js +++ b/code/build/darwin/create-universal-app.js @@ -3,24 +3,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const fs = require("fs"); -const minimatch = require("minimatch"); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const minimatch_1 = __importDefault(require("minimatch")); const vscode_universal_bundler_1 = require("vscode-universal-bundler"); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); async function main(buildDir) { const arch = process.env['VSCODE_ARCH']; if (!buildDir) { throw new Error('Build dir not provided'); } - const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); + const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); const appName = product.nameLong + '.app'; - const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); - const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); - const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); - const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const x64AppPath = path_1.default.join(buildDir, 'VSCode-darwin-x64', appName); + const arm64AppPath = path_1.default.join(buildDir, 'VSCode-darwin-arm64', appName); + const asarRelativePath = path_1.default.join('Contents', 'Resources', 'app', 'node_modules.asar'); + const outAppPath = path_1.default.join(buildDir, `VSCode-darwin-${arch}`, appName); + const productJsonPath = path_1.default.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', @@ -37,18 +40,18 @@ async function main(buildDir) { x64ArchFiles: '*/kerberos.node', filesToSkipComparison: (file) => { for (const expected of filesToSkip) { - if (minimatch(file, expected)) { + if ((0, minimatch_1.default)(file, expected)) { return true; } } return false; } }); - const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); + const productJson = JSON.parse(fs_1.default.readFileSync(productJsonPath, 'utf8')); Object.assign(productJson, { darwinUniversalAssetId: 'darwin-universal' }); - fs.writeFileSync(productJsonPath, JSON.stringify(productJson, null, '\t')); + fs_1.default.writeFileSync(productJsonPath, JSON.stringify(productJson, null, '\t')); } if (require.main === module) { main(process.argv[2]).catch(err => { diff --git a/code/build/darwin/create-universal-app.ts b/code/build/darwin/create-universal-app.ts index e05f780b38d..9e013cdb10c 100644 --- a/code/build/darwin/create-universal-app.ts +++ b/code/build/darwin/create-universal-app.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; -import * as minimatch from 'minimatch'; +import path from 'path'; +import fs from 'fs'; +import minimatch from 'minimatch'; import { makeUniversalApp } from 'vscode-universal-bundler'; const root = path.dirname(path.dirname(__dirname)); diff --git a/code/build/darwin/sign.js b/code/build/darwin/sign.js index feb5834ff85..dff30fd0e18 100644 --- a/code/build/darwin/sign.js +++ b/code/build/darwin/sign.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const codesign = require("electron-osx-sign"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const electron_osx_sign_1 = __importDefault(require("electron-osx-sign")); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); function getElectronVersion() { - const npmrc = fs.readFileSync(path.join(root, '.npmrc'), 'utf8'); + const npmrc = fs_1.default.readFileSync(path_1.default.join(root, '.npmrc'), 'utf8'); const target = /^target="(.*)"$/m.exec(npmrc)[1]; return target; } @@ -24,25 +27,25 @@ async function main(buildDir) { if (!tempDir) { throw new Error('$AGENT_TEMPDIRECTORY not set'); } - const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); - const baseDir = path.dirname(__dirname); - const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); + const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); + const baseDir = path_1.default.dirname(__dirname); + const appRoot = path_1.default.join(buildDir, `VSCode-darwin-${arch}`); const appName = product.nameLong + '.app'; - const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); + const appFrameworkPath = path_1.default.join(appRoot, appName, 'Contents', 'Frameworks'); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; - const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); + const infoPlistPath = path_1.default.resolve(appRoot, appName, 'Contents', 'Info.plist'); const defaultOpts = { - app: path.join(appRoot, appName), + app: path_1.default.join(appRoot, appName), platform: 'darwin', - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), hardenedRuntime: true, 'pre-auto-entitlements': false, 'pre-embed-provisioning-profile': false, - keychain: path.join(tempDir, 'buildagent.keychain'), + keychain: path_1.default.join(tempDir, 'buildagent.keychain'), version: getElectronVersion(), identity, 'gatekeeper-assess': false @@ -58,21 +61,21 @@ async function main(buildDir) { }; const gpuHelperOpts = { ...defaultOpts, - app: path.join(appFrameworkPath, gpuHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + app: path_1.default.join(appFrameworkPath, gpuHelperAppName), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), }; const rendererHelperOpts = { ...defaultOpts, - app: path.join(appFrameworkPath, rendererHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + app: path_1.default.join(appFrameworkPath, rendererHelperAppName), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), }; const pluginHelperOpts = { ...defaultOpts, - app: path.join(appFrameworkPath, pluginHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + app: path_1.default.join(appFrameworkPath, pluginHelperAppName), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), }; // Only overwrite plist entries for x64 and arm64 builds, // universal will get its copy from the x64 build. @@ -99,10 +102,10 @@ async function main(buildDir) { `${infoPlistPath}` ]); } - await codesign.signAsync(gpuHelperOpts); - await codesign.signAsync(rendererHelperOpts); - await codesign.signAsync(pluginHelperOpts); - await codesign.signAsync(appOpts); + await electron_osx_sign_1.default.signAsync(gpuHelperOpts); + await electron_osx_sign_1.default.signAsync(rendererHelperOpts); + await electron_osx_sign_1.default.signAsync(pluginHelperOpts); + await electron_osx_sign_1.default.signAsync(appOpts); } if (require.main === module) { main(process.argv[2]).catch(err => { diff --git a/code/build/darwin/sign.ts b/code/build/darwin/sign.ts index 5b3413b79e1..ecf162743ef 100644 --- a/code/build/darwin/sign.ts +++ b/code/build/darwin/sign.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as codesign from 'electron-osx-sign'; +import fs from 'fs'; +import path from 'path'; +import codesign from 'electron-osx-sign'; import { spawn } from '@malept/cross-spawn-promise'; const root = path.dirname(path.dirname(__dirname)); diff --git a/code/build/darwin/verify-macho.js b/code/build/darwin/verify-macho.js index 947184324e2..e7a4eb28d70 100644 --- a/code/build/darwin/verify-macho.js +++ b/code/build/darwin/verify-macho.js @@ -3,9 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const assert = require("assert"); -const path = require("path"); +const assert_1 = __importDefault(require("assert")); +const path_1 = __importDefault(require("path")); const promises_1 = require("fs/promises"); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); const MACHO_PREFIX = 'Mach-O '; @@ -78,7 +81,7 @@ async function checkMachOFiles(appPath, arch) { } else if (header_magic === MACHO_UNIVERSAL_MAGIC_LE) { const num_binaries = header.readUInt32BE(4); - assert.equal(num_binaries, 2); + assert_1.default.equal(num_binaries, 2); const file_entries_size = file_header_entry_size * num_binaries; const file_entries = Buffer.alloc(file_entries_size); read(p, file_entries, 0, file_entries_size, 8).then(_ => { @@ -95,7 +98,7 @@ async function checkMachOFiles(appPath, arch) { } if (info.isDirectory()) { for (const child of await (0, promises_1.readdir)(p)) { - await traverse(path.resolve(p, child)); + await traverse(path_1.default.resolve(p, child)); } } }; @@ -103,8 +106,8 @@ async function checkMachOFiles(appPath, arch) { return invalidFiles; } const archToCheck = process.argv[2]; -assert(process.env['APP_PATH'], 'APP_PATH not set'); -assert(archToCheck === 'x64' || archToCheck === 'arm64' || archToCheck === 'universal', `Invalid architecture ${archToCheck} to check`); +(0, assert_1.default)(process.env['APP_PATH'], 'APP_PATH not set'); +(0, assert_1.default)(archToCheck === 'x64' || archToCheck === 'arm64' || archToCheck === 'universal', `Invalid architecture ${archToCheck} to check`); checkMachOFiles(process.env['APP_PATH'], archToCheck).then(invalidFiles => { if (invalidFiles.length > 0) { console.error('\x1b[31mThe following files are built for the wrong architecture:\x1b[0m'); diff --git a/code/build/darwin/verify-macho.ts b/code/build/darwin/verify-macho.ts index f418c44a230..61ebc71aba2 100644 --- a/code/build/darwin/verify-macho.ts +++ b/code/build/darwin/verify-macho.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import * as path from 'path'; +import assert from 'assert'; +import path from 'path'; import { open, stat, readdir, realpath } from 'fs/promises'; import { spawn, ExitCodeError } from '@malept/cross-spawn-promise'; diff --git a/code/build/filters.js b/code/build/filters.js index 1705d4b32c3..17e74c3871a 100644 --- a/code/build/filters.js +++ b/code/build/filters.js @@ -49,6 +49,7 @@ module.exports.unicodeFilter = [ '!extensions/ipynb/notebook-out/**', '!extensions/notebook-renderers/renderer-out/**', '!extensions/php-language-features/src/features/phpGlobalFunctions.ts', + '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', @@ -88,6 +89,7 @@ module.exports.indentationFilter = [ '!test/automation/out/**', '!test/monaco/out/**', '!test/smoke/out/**', + '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/typescript-language-features/resources/walkthroughs/**', '!extensions/typescript-language-features/package-manager/node-maintainer/**', @@ -170,10 +172,10 @@ module.exports.copyrightFilter = [ '!extensions/markdown-math/notebook-out/**', '!extensions/ipynb/notebook-out/**', '!extensions/simple-browser/media/codicon.css', + '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/node-maintainer/**', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', - '!src/vs/editor/test/node/classification/typescript-test.ts', ]; module.exports.tsFormattingFilter = [ diff --git a/code/build/gulpfile.vscode.js b/code/build/gulpfile.vscode.js index 030c39a861e..a63f693c95a 100644 --- a/code/build/gulpfile.vscode.js +++ b/code/build/gulpfile.vscode.js @@ -299,7 +299,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); const root = path.resolve(path.join(__dirname, '..')); const productionDependencies = getProductionDependencies(root); - const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!**/*.mk`]).flat(); + const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat().concat('!**/*.mk'); const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.js.map'])) diff --git a/code/build/lib/asar.js b/code/build/lib/asar.js index 19285ef7100..20c982a6621 100644 --- a/code/build/lib/asar.js +++ b/code/build/lib/asar.js @@ -3,18 +3,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAsar = createAsar; -const path = require("path"); -const es = require("event-stream"); +const path_1 = __importDefault(require("path")); +const event_stream_1 = __importDefault(require("event-stream")); const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); -const VinylFile = require("vinyl"); -const minimatch = require("minimatch"); +const vinyl_1 = __importDefault(require("vinyl")); +const minimatch_1 = __importDefault(require("minimatch")); function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFilename) { const shouldUnpackFile = (file) => { for (let i = 0; i < unpackGlobs.length; i++) { - if (minimatch(file.relative, unpackGlobs[i])) { + if ((0, minimatch_1.default)(file.relative, unpackGlobs[i])) { return true; } } @@ -22,7 +25,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile }; const shouldSkipFile = (file) => { for (const skipGlob of skipGlobs) { - if (minimatch(file.relative, skipGlob)) { + if ((0, minimatch_1.default)(file.relative, skipGlob)) { return true; } } @@ -32,7 +35,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile // node_modules.asar and node_modules const shouldDuplicateFile = (file) => { for (const duplicateGlob of duplicateGlobs) { - if (minimatch(file.relative, duplicateGlob)) { + if ((0, minimatch_1.default)(file.relative, duplicateGlob)) { return true; } } @@ -75,7 +78,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile // Create a closure capturing `onFileInserted`. filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}).then(() => onFileInserted(), () => onFileInserted()); }; - return es.through(function (file) { + return event_stream_1.default.through(function (file) { if (file.stat.isDirectory()) { return; } @@ -83,7 +86,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile throw new Error(`unknown item in stream!`); } if (shouldSkipFile(file)) { - this.queue(new VinylFile({ + this.queue(new vinyl_1.default({ base: '.', path: file.path, stat: file.stat, @@ -92,7 +95,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile return; } if (shouldDuplicateFile(file)) { - this.queue(new VinylFile({ + this.queue(new vinyl_1.default({ base: '.', path: file.path, stat: file.stat, @@ -103,10 +106,10 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); if (shouldUnpack) { // The file goes outside of xx.asar, in a folder xx.asar.unpacked - const relative = path.relative(folderPath, file.path); - this.queue(new VinylFile({ + const relative = path_1.default.relative(folderPath, file.path); + this.queue(new vinyl_1.default({ base: '.', - path: path.join(destFilename + '.unpacked', relative), + path: path_1.default.join(destFilename + '.unpacked', relative), stat: file.stat, contents: file.contents })); @@ -129,7 +132,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile } const contents = Buffer.concat(out); out.length = 0; - this.queue(new VinylFile({ + this.queue(new vinyl_1.default({ base: '.', path: destFilename, contents: contents diff --git a/code/build/lib/asar.ts b/code/build/lib/asar.ts index 0b225ab1624..5f2df925bde 100644 --- a/code/build/lib/asar.ts +++ b/code/build/lib/asar.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as es from 'event-stream'; +import path from 'path'; +import es from 'event-stream'; const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); -import * as VinylFile from 'vinyl'; -import * as minimatch from 'minimatch'; +import VinylFile from 'vinyl'; +import minimatch from 'minimatch'; declare class AsarFilesystem { readonly header: unknown; diff --git a/code/build/lib/builtInExtensions.js b/code/build/lib/builtInExtensions.js index ac784c03506..400ca6885a8 100644 --- a/code/build/lib/builtInExtensions.js +++ b/code/build/lib/builtInExtensions.js @@ -3,39 +3,75 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getExtensionStream = getExtensionStream; exports.getBuiltInExtensions = getBuiltInExtensions; -const fs = require("fs"); -const path = require("path"); -const os = require("os"); -const rimraf = require("rimraf"); -const es = require("event-stream"); -const rename = require("gulp-rename"); -const vfs = require("vinyl-fs"); -const ext = require("./extensions"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const root = path.dirname(path.dirname(__dirname)); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const os_1 = __importDefault(require("os")); +const rimraf_1 = __importDefault(require("rimraf")); +const event_stream_1 = __importDefault(require("event-stream")); +const gulp_rename_1 = __importDefault(require("gulp-rename")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const ext = __importStar(require("./extensions")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions || []; const webBuiltInExtensions = productjson.webBuiltInExtensions || []; -const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); +const controlFilePath = path_1.default.join(os_1.default.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; function log(...messages) { if (ENABLE_LOGGING) { - fancyLog(...messages); + (0, fancy_log_1.default)(...messages); } } function getExtensionPath(extension) { - return path.join(root, '.build', 'builtInExtensions', extension.name); + return path_1.default.join(root, '.build', 'builtInExtensions', extension.name); } function isUpToDate(extension) { - const packagePath = path.join(getExtensionPath(extension), 'package.json'); - if (!fs.existsSync(packagePath)) { + const packagePath = path_1.default.join(getExtensionPath(extension), 'package.json'); + if (!fs_1.default.existsSync(packagePath)) { return false; } - const packageContents = fs.readFileSync(packagePath, { encoding: 'utf8' }); + const packageContents = fs_1.default.readFileSync(packagePath, { encoding: 'utf8' }); try { const diskVersion = JSON.parse(packageContents).version; return (diskVersion === extension.version); @@ -47,71 +83,71 @@ function isUpToDate(extension) { function getExtensionDownloadStream(extension) { const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; return (galleryServiceUrl ? ext.fromMarketplace(galleryServiceUrl, extension) : ext.fromGithub(extension)) - .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)); + .pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); } function getExtensionStream(extension) { // if the extension exists on disk, use those files instead of downloading anew if (isUpToDate(extension)) { - log('[extensions]', `${extension.name}@${extension.version} up to date`, ansiColors.green('✔︎')); - return vfs.src(['**'], { cwd: getExtensionPath(extension), dot: true }) - .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)); + log('[extensions]', `${extension.name}@${extension.version} up to date`, ansi_colors_1.default.green('✔︎')); + return vinyl_fs_1.default.src(['**'], { cwd: getExtensionPath(extension), dot: true }) + .pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); } return getExtensionDownloadStream(extension); } function syncMarketplaceExtension(extension) { const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; - const source = ansiColors.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); + const source = ansi_colors_1.default.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); if (isUpToDate(extension)) { - log(source, `${extension.name}@${extension.version}`, ansiColors.green('✔︎')); - return es.readArray([]); + log(source, `${extension.name}@${extension.version}`, ansi_colors_1.default.green('✔︎')); + return event_stream_1.default.readArray([]); } - rimraf.sync(getExtensionPath(extension)); + rimraf_1.default.sync(getExtensionPath(extension)); return getExtensionDownloadStream(extension) - .pipe(vfs.dest('.build/builtInExtensions')) - .on('end', () => log(source, extension.name, ansiColors.green('✔︎'))); + .pipe(vinyl_fs_1.default.dest('.build/builtInExtensions')) + .on('end', () => log(source, extension.name, ansi_colors_1.default.green('✔︎'))); } function syncExtension(extension, controlState) { if (extension.platforms) { const platforms = new Set(extension.platforms); if (!platforms.has(process.platform)) { - log(ansiColors.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansiColors.green('✔︎')); - return es.readArray([]); + log(ansi_colors_1.default.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansi_colors_1.default.green('✔︎')); + return event_stream_1.default.readArray([]); } } switch (controlState) { case 'disabled': - log(ansiColors.blue('[disabled]'), ansiColors.gray(extension.name)); - return es.readArray([]); + log(ansi_colors_1.default.blue('[disabled]'), ansi_colors_1.default.gray(extension.name)); + return event_stream_1.default.readArray([]); case 'marketplace': return syncMarketplaceExtension(extension); default: - if (!fs.existsSync(controlState)) { - log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); - return es.readArray([]); + if (!fs_1.default.existsSync(controlState)) { + log(ansi_colors_1.default.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); + return event_stream_1.default.readArray([]); } - else if (!fs.existsSync(path.join(controlState, 'package.json'))) { - log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); - return es.readArray([]); + else if (!fs_1.default.existsSync(path_1.default.join(controlState, 'package.json'))) { + log(ansi_colors_1.default.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); + return event_stream_1.default.readArray([]); } - log(ansiColors.blue('[local]'), `${extension.name}: ${ansiColors.cyan(controlState)}`, ansiColors.green('✔︎')); - return es.readArray([]); + log(ansi_colors_1.default.blue('[local]'), `${extension.name}: ${ansi_colors_1.default.cyan(controlState)}`, ansi_colors_1.default.green('✔︎')); + return event_stream_1.default.readArray([]); } } function readControlFile() { try { - return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); + return JSON.parse(fs_1.default.readFileSync(controlFilePath, 'utf8')); } catch (err) { return {}; } } function writeControlFile(control) { - fs.mkdirSync(path.dirname(controlFilePath), { recursive: true }); - fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); + fs_1.default.mkdirSync(path_1.default.dirname(controlFilePath), { recursive: true }); + fs_1.default.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } function getBuiltInExtensions() { log('Synchronizing built-in extensions...'); - log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); + log(`You can manage built-in extensions with the ${ansi_colors_1.default.cyan('--builtin')} flag`); const control = readControlFile(); const streams = []; for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { @@ -121,7 +157,7 @@ function getBuiltInExtensions() { } writeControlFile(control); return new Promise((resolve, reject) => { - es.merge(streams) + event_stream_1.default.merge(streams) .on('error', reject) .on('end', resolve); }); diff --git a/code/build/lib/builtInExtensions.ts b/code/build/lib/builtInExtensions.ts index 8b831d42d44..9b1ec7356ef 100644 --- a/code/build/lib/builtInExtensions.ts +++ b/code/build/lib/builtInExtensions.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import * as rimraf from 'rimraf'; -import * as es from 'event-stream'; -import * as rename from 'gulp-rename'; -import * as vfs from 'vinyl-fs'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import rimraf from 'rimraf'; +import es from 'event-stream'; +import rename from 'gulp-rename'; +import vfs from 'vinyl-fs'; import * as ext from './extensions'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; import { Stream } from 'stream'; export interface IExtensionDefinition { diff --git a/code/build/lib/builtInExtensionsCG.js b/code/build/lib/builtInExtensionsCG.js index 6a1e5ea539e..3dc0ae27f0a 100644 --- a/code/build/lib/builtInExtensionsCG.js +++ b/code/build/lib/builtInExtensionsCG.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const url = require("url"); -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 fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const url_1 = __importDefault(require("url")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const rootCG = path_1.default.join(root, 'extensionsCG'); +const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions || []; const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const token = process.env['GITHUB_TOKEN']; @@ -18,7 +21,7 @@ const contentBasePath = 'raw.githubusercontent.com'; const contentFileNames = ['package.json', 'package-lock.json']; async function downloadExtensionDetails(extension) { const extensionLabel = `${extension.name}@${extension.version}`; - const repository = url.parse(extension.repo).path.substr(1); + const repository = url_1.default.parse(extension.repo).path.substr(1); const repositoryContentBaseUrl = `https://${token ? `${token}@` : ''}${contentBasePath}/${repository}/v${extension.version}`; async function getContent(fileName) { try { @@ -42,16 +45,16 @@ async function downloadExtensionDetails(extension) { const results = await Promise.all(promises); for (const result of results) { if (result.body) { - const extensionFolder = path.join(rootCG, extension.name); - fs.mkdirSync(extensionFolder, { recursive: true }); - fs.writeFileSync(path.join(extensionFolder, result.fileName), result.body); - console.log(` - ${result.fileName} ${ansiColors.green('✔︎')}`); + const extensionFolder = path_1.default.join(rootCG, extension.name); + fs_1.default.mkdirSync(extensionFolder, { recursive: true }); + fs_1.default.writeFileSync(path_1.default.join(extensionFolder, result.fileName), result.body); + console.log(` - ${result.fileName} ${ansi_colors_1.default.green('✔︎')}`); } else if (result.body === undefined) { - console.log(` - ${result.fileName} ${ansiColors.yellow('⚠️')}`); + console.log(` - ${result.fileName} ${ansi_colors_1.default.yellow('⚠️')}`); } else { - console.log(` - ${result.fileName} ${ansiColors.red('🛑')}`); + console.log(` - ${result.fileName} ${ansi_colors_1.default.red('🛑')}`); } } // Validation @@ -68,10 +71,10 @@ async function main() { } } main().then(() => { - console.log(`Built-in extensions component data downloaded ${ansiColors.green('✔︎')}`); + console.log(`Built-in extensions component data downloaded ${ansi_colors_1.default.green('✔︎')}`); process.exit(0); }, err => { - console.log(`Built-in extensions component data could not be downloaded ${ansiColors.red('🛑')}`); + console.log(`Built-in extensions component data could not be downloaded ${ansi_colors_1.default.red('🛑')}`); console.error(err); process.exit(1); }); diff --git a/code/build/lib/builtInExtensionsCG.ts b/code/build/lib/builtInExtensionsCG.ts index 9d11dea3dca..4628b365a2e 100644 --- a/code/build/lib/builtInExtensionsCG.ts +++ b/code/build/lib/builtInExtensionsCG.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as url from 'url'; -import ansiColors = require('ansi-colors'); +import fs from 'fs'; +import path from 'path'; +import url from 'url'; +import ansiColors from 'ansi-colors'; import { IExtensionDefinition } from './builtInExtensions'; const root = path.dirname(path.dirname(__dirname)); diff --git a/code/build/lib/bundle.js b/code/build/lib/bundle.js index 627b9966700..f1490f4ad4b 100644 --- a/code/build/lib/bundle.js +++ b/code/build/lib/bundle.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.bundle = bundle; exports.removeAllTSBoilerplate = removeAllTSBoilerplate; -const fs = require("fs"); -const path = require("path"); -const vm = require("vm"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const vm_1 = __importDefault(require("vm")); /** * Bundle `entryPoints` given config `config`. */ @@ -30,8 +33,8 @@ function bundle(entryPoints, config, callback) { allMentionedModulesMap[excludedModule] = true; }); }); - const code = require('fs').readFileSync(path.join(__dirname, '../../src/vs/loader.js')); - const r = vm.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); + const code = require('fs').readFileSync(path_1.default.join(__dirname, '../../src/vs/loader.js')); + const r = vm_1.default.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); const loaderModule = { exports: {} }; r.call({}, require, loaderModule, loaderModule.exports); const loader = loaderModule.exports; @@ -149,7 +152,7 @@ function extractStrings(destFiles) { _path = pieces[0]; } if (/^\.\//.test(_path) || /^\.\.\//.test(_path)) { - const res = path.join(path.dirname(module), _path).replace(/\\/g, '/'); + const res = path_1.default.join(path_1.default.dirname(module), _path).replace(/\\/g, '/'); return prefix + res; } return prefix + _path; @@ -224,7 +227,7 @@ function removeAllDuplicateTSBoilerplate(destFiles) { return destFiles; } function removeAllTSBoilerplate(source) { - const seen = new Array(BOILERPLATE.length).fill(true, 0, 10); + const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); return removeDuplicateTSBoilerplate(source, seen); } // Taken from typescript compiler => emitFiles @@ -239,6 +242,8 @@ const BOILERPLATE = [ { start: /^var __createBinding/, end: /^}\)\);$/ }, { start: /^var __setModuleDefault/, end: /^}\);$/ }, { start: /^var __importStar/, end: /^};$/ }, + { start: /^var __addDisposableResource/, end: /^};$/ }, + { start: /^var __disposeResources/, end: /^}\);$/ }, ]; function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { const lines = source.split(/\r\n|\n|\r/); @@ -357,7 +362,7 @@ function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, } function readFileAndRemoveBOM(path) { const BOM_CHAR_CODE = 65279; - let contents = fs.readFileSync(path, 'utf8'); + let contents = fs_1.default.readFileSync(path, 'utf8'); // Remove BOM if (contents.charCodeAt(0) === BOM_CHAR_CODE) { contents = contents.substring(1); diff --git a/code/build/lib/bundle.ts b/code/build/lib/bundle.ts index 58995b7d5d1..68182e6b85d 100644 --- a/code/build/lib/bundle.ts +++ b/code/build/lib/bundle.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as vm from 'vm'; +import fs from 'fs'; +import path from 'path'; +import vm from 'vm'; interface IPosition { line: number; @@ -358,7 +358,7 @@ function removeAllDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[ } export function removeAllTSBoilerplate(source: string) { - const seen = new Array(BOILERPLATE.length).fill(true, 0, 10); + const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); return removeDuplicateTSBoilerplate(source, seen); } @@ -374,6 +374,8 @@ const BOILERPLATE = [ { start: /^var __createBinding/, end: /^}\)\);$/ }, { start: /^var __setModuleDefault/, end: /^}\);$/ }, { start: /^var __importStar/, end: /^};$/ }, + { start: /^var __addDisposableResource/, end: /^};$/ }, + { start: /^var __disposeResources/, end: /^}\);$/ }, ]; function removeDuplicateTSBoilerplate(source: string, SEEN_BOILERPLATE: boolean[] = []): string { diff --git a/code/build/lib/compilation.js b/code/build/lib/compilation.js index 7b9d73facbb..841dbe13ecf 100644 --- a/code/build/lib/compilation.js +++ b/code/build/lib/compilation.js @@ -3,24 +3,60 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = void 0; exports.transpileTask = transpileTask; exports.compileTask = compileTask; exports.watchTask = watchTask; -const es = require("event-stream"); -const fs = require("fs"); -const gulp = require("gulp"); -const path = require("path"); -const monacodts = require("./monaco-api"); -const nls = require("./nls"); +const event_stream_1 = __importDefault(require("event-stream")); +const fs_1 = __importDefault(require("fs")); +const gulp_1 = __importDefault(require("gulp")); +const path_1 = __importDefault(require("path")); +const monacodts = __importStar(require("./monaco-api")); +const nls = __importStar(require("./nls")); const reporter_1 = require("./reporter"); -const util = require("./util"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const os = require("os"); -const File = require("vinyl"); -const task = require("./task"); +const util = __importStar(require("./util")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const os_1 = __importDefault(require("os")); +const vinyl_1 = __importDefault(require("vinyl")); +const task = __importStar(require("./task")); const index_1 = require("./mangle/index"); const postcss_1 = require("./postcss"); const ts = require("typescript"); @@ -28,7 +64,7 @@ const watch = require('./watch'); // --- gulp-tsb: compile and transpile -------------------------------- const reporter = (0, reporter_1.createReporter)(); function getTypeScriptCompilerOptions(src) { - const rootDir = path.join(__dirname, `../../${src}`); + const rootDir = path_1.default.join(__dirname, `../../${src}`); const options = {}; options.verbose = false; options.sourceMap = true; @@ -38,13 +74,13 @@ function getTypeScriptCompilerOptions(src) { options.rootDir = rootDir; options.baseUrl = rootDir; options.sourceRoot = util.toFileUri(rootDir); - options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 0 : 1; + options.newLine = /\r\n/.test(fs_1.default.readFileSync(__filename, 'utf8')) ? 0 : 1; return options; } function createCompile(src, { build, emitError, transpileOnly, preserveEnglish }) { const tsb = require('./tsb'); const sourcemaps = require('gulp-sourcemaps'); - const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); + const projectPath = path_1.default.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; if (!build) { overrideOptions.inlineSourceMap = true; @@ -62,7 +98,7 @@ function createCompile(src, { build, emitError, transpileOnly, preserveEnglish } const isCSS = (f) => f.path.endsWith('.css') && !f.path.includes('fixtures'); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); const postcssNesting = require('postcss-nesting'); - const input = es.through(); + const input = event_stream_1.default.through(); const output = input .pipe(util.$if(isUtf8Test, bom())) // this is required to preserve BOM in test files that loose it otherwise .pipe(util.$if(!build && isRuntimeJs, util.appendOwnPathSourceURL())) @@ -80,7 +116,7 @@ function createCompile(src, { build, emitError, transpileOnly, preserveEnglish } }))) .pipe(tsFilter.restore) .pipe(reporter.end(!!emitError)); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } pipeline.tsProjectSrc = () => { return compilation.src({ base: src }); @@ -91,31 +127,31 @@ function createCompile(src, { build, emitError, transpileOnly, preserveEnglish } function transpileTask(src, out, esbuild) { const task = () => { const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { esbuild }, preserveEnglish: false }); - const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); + const srcPipe = gulp_1.default.src(`${src}/**`, { base: `${src}` }); return srcPipe .pipe(transpile()) - .pipe(gulp.dest(out)); + .pipe(gulp_1.default.dest(out)); }; - task.taskName = `transpile-${path.basename(src)}`; + task.taskName = `transpile-${path_1.default.basename(src)}`; return task; } function compileTask(src, out, build, options = {}) { const task = () => { - if (os.totalmem() < 4_000_000_000) { + if (os_1.default.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); } const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); - const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); + const srcPipe = gulp_1.default.src(`${src}/**`, { base: `${src}` }); const generator = new MonacoGenerator(false); if (src === 'src') { generator.execute(); } // mangle: TypeScript to TypeScript - let mangleStream = es.through(); + let mangleStream = event_stream_1.default.through(); if (build && !options.disableMangle) { - let ts2tsMangler = new index_1.Mangler(compile.projectPath, (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data), { mangleExports: true, manglePrivateFields: true }); + let ts2tsMangler = new index_1.Mangler(compile.projectPath, (...data) => (0, fancy_log_1.default)(ansi_colors_1.default.blue('[mangler]'), ...data), { mangleExports: true, manglePrivateFields: true }); const newContentsByFileName = ts2tsMangler.computeNewFileContents(new Set(['saveState'])); - mangleStream = es.through(async function write(data) { + mangleStream = event_stream_1.default.through(async function write(data) { const tsNormalPath = ts.normalizePath(data.path); const newContents = (await newContentsByFileName).get(tsNormalPath); if (newContents !== undefined) { @@ -134,27 +170,27 @@ function compileTask(src, out, build, options = {}) { .pipe(mangleStream) .pipe(generator.stream) .pipe(compile()) - .pipe(gulp.dest(out)); + .pipe(gulp_1.default.dest(out)); }; - task.taskName = `compile-${path.basename(src)}`; + task.taskName = `compile-${path_1.default.basename(src)}`; return task; } function watchTask(out, build, srcPath = 'src') { const task = () => { const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false }); - const src = gulp.src(`${srcPath}/**`, { base: srcPath }); + const src = gulp_1.default.src(`${srcPath}/**`, { base: srcPath }); const watchSrc = watch(`${srcPath}/**`, { base: srcPath, readDelay: 200 }); const generator = new MonacoGenerator(true); generator.execute(); return watchSrc .pipe(generator.stream) .pipe(util.incremental(compile, src, true)) - .pipe(gulp.dest(out)); + .pipe(gulp_1.default.dest(out)); }; - task.taskName = `watch-${path.basename(out)}`; + task.taskName = `watch-${path_1.default.basename(out)}`; return task; } -const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); +const REPO_SRC_FOLDER = path_1.default.join(__dirname, '../../src'); class MonacoGenerator { _isWatch; stream; @@ -163,7 +199,7 @@ class MonacoGenerator { _declarationResolver; constructor(isWatch) { this._isWatch = isWatch; - this.stream = es.through(); + this.stream = event_stream_1.default.through(); this._watchedFiles = {}; const onWillReadFile = (moduleId, filePath) => { if (!this._isWatch) { @@ -173,7 +209,7 @@ class MonacoGenerator { return; } this._watchedFiles[filePath] = true; - fs.watchFile(filePath, () => { + fs_1.default.watchFile(filePath, () => { this._declarationResolver.invalidateCache(moduleId); this._executeSoon(); }); @@ -186,7 +222,7 @@ class MonacoGenerator { }; this._declarationResolver = new monacodts.DeclarationResolver(this._fsProvider); if (this._isWatch) { - fs.watchFile(monacodts.RECIPE_PATH, () => { + fs_1.default.watchFile(monacodts.RECIPE_PATH, () => { this._executeSoon(); }); } @@ -211,7 +247,7 @@ class MonacoGenerator { return r; } _log(message, ...rest) { - fancyLog(ansiColors.cyan('[monaco.d.ts]'), message, ...rest); + (0, fancy_log_1.default)(ansi_colors_1.default.cyan('[monaco.d.ts]'), message, ...rest); } execute() { const startTime = Date.now(); @@ -223,8 +259,8 @@ class MonacoGenerator { if (result.isTheSame) { return; } - fs.writeFileSync(result.filePath, result.content); - fs.writeFileSync(path.join(REPO_SRC_FOLDER, 'vs/editor/common/standalone/standaloneEnums.ts'), result.enums); + fs_1.default.writeFileSync(result.filePath, result.content); + fs_1.default.writeFileSync(path_1.default.join(REPO_SRC_FOLDER, 'vs/editor/common/standalone/standaloneEnums.ts'), result.enums); this._log(`monaco.d.ts is changed - total time took ${Date.now() - startTime} ms`); if (!this._isWatch) { this.stream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); @@ -234,21 +270,21 @@ class MonacoGenerator { function generateApiProposalNames() { let eol; try { - const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs_1.default.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); - eol = match ? match[0] : os.EOL; + eol = match ? match[0] : os_1.default.EOL; } catch { - eol = os.EOL; + eol = os_1.default.EOL; } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; const proposals = new Map(); - const input = es.through(); + const input = event_stream_1.default.through(); const output = input .pipe(util.filter((f) => pattern.test(f.path))) - .pipe(es.through((f) => { - const name = path.basename(f.path); + .pipe(event_stream_1.default.through((f) => { + const name = path_1.default.basename(f.path); const match = pattern.exec(name); if (!match) { return; @@ -281,27 +317,27 @@ function generateApiProposalNames() { 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); - this.emit('data', new File({ + this.emit('data', new vinyl_1.default({ path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } const apiProposalNamesReporter = (0, reporter_1.createReporter)('api-proposal-names'); exports.compileApiProposalNamesTask = task.define('compile-api-proposal-names', () => { - return gulp.src('src/vscode-dts/**') + return gulp_1.default.src('src/vscode-dts/**') .pipe(generateApiProposalNames()) - .pipe(gulp.dest('src')) + .pipe(gulp_1.default.dest('src')) .pipe(apiProposalNamesReporter.end(true)); }); exports.watchApiProposalNamesTask = task.define('watch-api-proposal-names', () => { - const task = () => gulp.src('src/vscode-dts/**') + const task = () => gulp_1.default.src('src/vscode-dts/**') .pipe(generateApiProposalNames()) .pipe(apiProposalNamesReporter.end(true)); return watch('src/vscode-dts/**', { readDelay: 200 }) .pipe(util.debounce(task)) - .pipe(gulp.dest('src')); + .pipe(gulp_1.default.dest('src')); }); //# sourceMappingURL=compilation.js.map \ No newline at end of file diff --git a/code/build/lib/compilation.ts b/code/build/lib/compilation.ts index 124bcc17c17..6e1fcab5186 100644 --- a/code/build/lib/compilation.ts +++ b/code/build/lib/compilation.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fs from 'fs'; -import * as gulp from 'gulp'; -import * as path from 'path'; +import es from 'event-stream'; +import fs from 'fs'; +import gulp from 'gulp'; +import path from 'path'; import * as monacodts from './monaco-api'; import * as nls from './nls'; import { createReporter } from './reporter'; import * as util from './util'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as os from 'os'; -import * as File from 'vinyl'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import os from 'os'; +import File from 'vinyl'; import * as task from './task'; import { Mangler } from './mangle/index'; import { RawSourceMap } from 'source-map'; diff --git a/code/build/lib/date.js b/code/build/lib/date.js index 77fff0e5073..1ed884fb7ee 100644 --- a/code/build/lib/date.js +++ b/code/build/lib/date.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.writeISODate = writeISODate; exports.readISODate = readISODate; -const path = require("path"); -const fs = require("fs"); -const root = path.join(__dirname, '..', '..'); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const root = path_1.default.join(__dirname, '..', '..'); /** * Writes a `outDir/date` file with the contents of the build * so that other tasks during the build process can use it and @@ -16,17 +19,17 @@ const root = path.join(__dirname, '..', '..'); */ function writeISODate(outDir) { const result = () => new Promise((resolve, _) => { - const outDirectory = path.join(root, outDir); - fs.mkdirSync(outDirectory, { recursive: true }); + const outDirectory = path_1.default.join(root, outDir); + fs_1.default.mkdirSync(outDirectory, { recursive: true }); const date = new Date().toISOString(); - fs.writeFileSync(path.join(outDirectory, 'date'), date, 'utf8'); + fs_1.default.writeFileSync(path_1.default.join(outDirectory, 'date'), date, 'utf8'); resolve(); }); result.taskName = 'build-date-file'; return result; } function readISODate(outDir) { - const outDirectory = path.join(root, outDir); - return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); + const outDirectory = path_1.default.join(root, outDir); + return fs_1.default.readFileSync(path_1.default.join(outDirectory, 'date'), 'utf8'); } //# sourceMappingURL=date.js.map \ No newline at end of file diff --git a/code/build/lib/date.ts b/code/build/lib/date.ts index 998e89f8e6a..8a933178952 100644 --- a/code/build/lib/date.ts +++ b/code/build/lib/date.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; const root = path.join(__dirname, '..', '..'); diff --git a/code/build/lib/dependencies.js b/code/build/lib/dependencies.js index 9bcd1204eab..04a09f98708 100644 --- a/code/build/lib/dependencies.js +++ b/code/build/lib/dependencies.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getProductionDependencies = getProductionDependencies; -const fs = require("fs"); -const path = require("path"); -const cp = require("child_process"); -const root = fs.realpathSync(path.dirname(path.dirname(__dirname))); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const child_process_1 = __importDefault(require("child_process")); +const root = fs_1.default.realpathSync(path_1.default.dirname(path_1.default.dirname(__dirname))); function getNpmProductionDependencies(folder) { let raw; try { - raw = cp.execSync('npm ls --all --omit=dev --parseable', { cwd: folder, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, null] }); + raw = child_process_1.default.execSync('npm ls --all --omit=dev --parseable', { cwd: folder, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, null] }); } catch (err) { const regex = /^npm ERR! .*$/gm; @@ -34,16 +37,16 @@ function getNpmProductionDependencies(folder) { raw = err.stdout; } return raw.split(/\r?\n/).filter(line => { - return !!line.trim() && path.relative(root, line) !== path.relative(root, folder); + return !!line.trim() && path_1.default.relative(root, line) !== path_1.default.relative(root, folder); }); } function getProductionDependencies(folderPath) { const result = getNpmProductionDependencies(folderPath); // Account for distro npm dependencies - const realFolderPath = fs.realpathSync(folderPath); - const relativeFolderPath = path.relative(root, realFolderPath); + const realFolderPath = fs_1.default.realpathSync(folderPath); + const relativeFolderPath = path_1.default.relative(root, realFolderPath); const distroFolderPath = `${root}/.build/distro/npm/${relativeFolderPath}`; - if (fs.existsSync(distroFolderPath)) { + if (fs_1.default.existsSync(distroFolderPath)) { result.push(...getNpmProductionDependencies(distroFolderPath)); } return [...new Set(result)]; diff --git a/code/build/lib/dependencies.ts b/code/build/lib/dependencies.ts index 45368ffd26d..a5bc70088a7 100644 --- a/code/build/lib/dependencies.ts +++ b/code/build/lib/dependencies.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as cp from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import cp from 'child_process'; const root = fs.realpathSync(path.dirname(path.dirname(__dirname))); function getNpmProductionDependencies(folder: string): string[] { diff --git a/code/build/lib/electron.js b/code/build/lib/electron.js index 99252e4e64a..f0eb583f2cb 100644 --- a/code/build/lib/electron.js +++ b/code/build/lib/electron.js @@ -3,19 +3,55 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.config = void 0; -const fs = require("fs"); -const path = require("path"); -const vfs = require("vinyl-fs"); -const filter = require("gulp-filter"); -const util = require("./util"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const util = __importStar(require("./util")); const getVersion_1 = require("./getVersion"); function isDocumentSuffix(str) { return str === 'document' || str === 'script' || str === 'file' || str === 'source code'; } -const root = path.dirname(path.dirname(__dirname)); -const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); const commit = (0, getVersion_1.getVersion)(root); function createTemplate(input) { return (params) => { @@ -24,7 +60,7 @@ function createTemplate(input) { }); }; } -const darwinCreditsTemplate = product.darwinCredits && createTemplate(fs.readFileSync(path.join(root, product.darwinCredits), 'utf8')); +const darwinCreditsTemplate = product.darwinCredits && createTemplate(fs_1.default.readFileSync(path_1.default.join(root, product.darwinCredits), 'utf8')); /** * Generate a `DarwinDocumentType` given a list of file extensions, an icon name, and an optional suffix or file type name. * @param extensions A list of file extensions, such as `['bat', 'cmd']` @@ -183,7 +219,7 @@ exports.config = { token: process.env['GITHUB_TOKEN'], repo: product.electronRepository || undefined, validateChecksum: true, - checksumFile: path.join(root, 'build', 'checksums', 'electron.txt'), + checksumFile: path_1.default.join(root, 'build', 'checksums', 'electron.txt'), }; function getElectron(arch) { return () => { @@ -196,18 +232,18 @@ function getElectron(arch) { ffmpegChromium: false, keepDefaultApp: true }; - return vfs.src('package.json') + return vinyl_fs_1.default.src('package.json') .pipe(json({ name: product.nameShort })) .pipe(electron(electronOpts)) - .pipe(filter(['**', '!**/app/package.json'])) - .pipe(vfs.dest('.build/electron')); + .pipe((0, gulp_filter_1.default)(['**', '!**/app/package.json'])) + .pipe(vinyl_fs_1.default.dest('.build/electron')); }; } async function main(arch = process.arch) { const version = electronVersion; - const electronPath = path.join(root, '.build', 'electron'); - const versionFile = path.join(electronPath, 'version'); - const isUpToDate = fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf8') === `${version}`; + const electronPath = path_1.default.join(root, '.build', 'electron'); + const versionFile = path_1.default.join(electronPath, 'version'); + const isUpToDate = fs_1.default.existsSync(versionFile) && fs_1.default.readFileSync(versionFile, 'utf8') === `${version}`; if (!isUpToDate) { await util.rimraf(electronPath)(); await util.streamToPromise(getElectron(arch)()); diff --git a/code/build/lib/electron.ts b/code/build/lib/electron.ts index da2387f68f6..57b27022df8 100644 --- a/code/build/lib/electron.ts +++ b/code/build/lib/electron.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as vfs from 'vinyl-fs'; -import * as filter from 'gulp-filter'; +import fs from 'fs'; +import path from 'path'; +import vfs from 'vinyl-fs'; +import filter from 'gulp-filter'; import * as util from './util'; import { getVersion } from './getVersion'; diff --git a/code/build/lib/extensions.js b/code/build/lib/extensions.js index 8630c8fa061..6afa72e5bfa 100644 --- a/code/build/lib/extensions.js +++ b/code/build/lib/extensions.js @@ -3,6 +3,42 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.fromMarketplace = fromMarketplace; exports.fromGithub = fromGithub; @@ -14,35 +50,35 @@ exports.scanBuiltinExtensions = scanBuiltinExtensions; exports.translatePackageJSON = translatePackageJSON; exports.webpackExtensions = webpackExtensions; exports.buildExtensionMedia = buildExtensionMedia; -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"); -const File = require("vinyl"); +const event_stream_1 = __importDefault(require("event-stream")); +const fs_1 = __importDefault(require("fs")); +const child_process_1 = __importDefault(require("child_process")); +const glob_1 = __importDefault(require("glob")); +const gulp_1 = __importDefault(require("gulp")); +const path_1 = __importDefault(require("path")); +const vinyl_1 = __importDefault(require("vinyl")); const stats_1 = require("./stats"); -const util2 = require("./util"); +const util2 = __importStar(require("./util")); const vzip = require('gulp-vinyl-zip'); -const filter = require("gulp-filter"); -const rename = require("gulp-rename"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const buffer = require('gulp-buffer'); -const jsoncParser = require("jsonc-parser"); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const gulp_rename_1 = __importDefault(require("gulp-rename")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const gulp_buffer_1 = __importDefault(require("gulp-buffer")); +const jsoncParser = __importStar(require("jsonc-parser")); const dependencies_1 = require("./dependencies"); const builtInExtensions_1 = require("./builtInExtensions"); const getVersion_1 = require("./getVersion"); const fetch_1 = require("./fetch"); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const commit = (0, getVersion_1.getVersion)(root); const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input) { - const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); + const jsonFilter = (0, gulp_filter_1.default)(['**/*.json', '**/*.code-snippets'], { restore: true }); return input .pipe(jsonFilter) - .pipe(buffer()) - .pipe(es.mapSync((f) => { + .pipe((0, gulp_buffer_1.default)()) + .pipe(event_stream_1.default.mapSync((f) => { const errors = []; const value = jsoncParser.parse(f.contents.toString('utf8'), errors, { allowTrailingComma: true }); if (errors.length === 0) { @@ -54,11 +90,11 @@ function minifyExtensionResources(input) { .pipe(jsonFilter.restore); } function updateExtensionPackageJSON(input, update) { - const packageJsonFilter = filter('extensions/*/package.json', { restore: true }); + const packageJsonFilter = (0, gulp_filter_1.default)('extensions/*/package.json', { restore: true }); return input .pipe(packageJsonFilter) - .pipe(buffer()) - .pipe(es.mapSync((f) => { + .pipe((0, gulp_buffer_1.default)()) + .pipe(event_stream_1.default.mapSync((f) => { const data = JSON.parse(f.contents.toString('utf8')); f.contents = Buffer.from(JSON.stringify(update(data))); return f; @@ -67,7 +103,7 @@ function updateExtensionPackageJSON(input, update) { } function fromLocal(extensionPath, forWeb, disableMangle) { const webpackConfigFileName = forWeb ? 'extension-browser.webpack.config.js' : 'extension.webpack.config.js'; - const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); + const isWebPacked = fs_1.default.existsSync(path_1.default.join(extensionPath, webpackConfigFileName)); let input = isWebPacked ? fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) : fromLocalNormal(extensionPath); @@ -88,11 +124,11 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { const vsce = require('@vscode/vsce'); const webpack = require('webpack'); const webpackGulp = require('webpack-stream'); - const result = es.through(); + const result = event_stream_1.default.through(); const packagedDependencies = []; - const packageJsonConfig = require(path.join(extensionPath, 'package.json')); + const packageJsonConfig = require(path_1.default.join(extensionPath, 'package.json')); if (packageJsonConfig.dependencies) { - const webpackRootConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = require(path_1.default.join(extensionPath, webpackConfigFileName)); for (const key in webpackRootConfig.externals) { if (key in packageJsonConfig.dependencies) { packagedDependencies.push(key); @@ -106,19 +142,19 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { // as a temporary workaround. vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ + .map(fileName => path_1.default.join(extensionPath, fileName)) + .map(filePath => new vinyl_1.default({ path: filePath, - stat: fs.statSync(filePath), + stat: fs_1.default.statSync(filePath), base: extensionPath, - contents: fs.createReadStream(filePath) + contents: fs_1.default.createReadStream(filePath) })); // check for a webpack configuration files, then invoke webpack // and merge its output with the files stream. - const webpackConfigLocations = glob.sync(path.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); + const webpackConfigLocations = glob_1.default.sync(path_1.default.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { const webpackDone = (err, stats) => { - fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); + (0, fancy_log_1.default)(`Bundled extension: ${ansi_colors_1.default.yellow(path_1.default.join(path_1.default.basename(extensionPath), path_1.default.relative(extensionPath, webpackConfigPath)))}...`); if (err) { result.emit('error', err); } @@ -149,28 +185,28 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { } } } - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + const relativeOutputPath = path_1.default.relative(extensionPath, webpackConfig.output.path); return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { + .pipe(event_stream_1.default.through(function (data) { data.stat = data.stat || {}; data.base = extensionPath; this.emit('data', data); })) - .pipe(es.through(function (data) { + .pipe(event_stream_1.default.through(function (data) { // source map handling: // * rewrite sourceMappingURL // * save to disk so that upload-task picks this up - if (path.extname(data.basename) === '.js') { + if (path_1.default.extname(data.basename) === '.js') { const contents = data.contents.toString('utf8'); data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path_1.default.basename(extensionPath)}/${relativeOutputPath}/${g1}`; }), 'utf8'); } this.emit('data', data); })); }); }); - es.merge(...webpackStreams, es.readArray(files)) + event_stream_1.default.merge(...webpackStreams, event_stream_1.default.readArray(files)) // .pipe(es.through(function (data) { // // debug // console.log('out', data.path, data.contents.length); @@ -182,25 +218,25 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { console.error(packagedDependencies); result.emit('error', err); }); - return result.pipe((0, stats_1.createStatsStream)(path.basename(extensionPath))); + return result.pipe((0, stats_1.createStatsStream)(path_1.default.basename(extensionPath))); } function fromLocalNormal(extensionPath) { const vsce = require('@vscode/vsce'); - const result = es.through(); + const result = event_stream_1.default.through(); vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Npm }) .then(fileNames => { const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ + .map(fileName => path_1.default.join(extensionPath, fileName)) + .map(filePath => new vinyl_1.default({ path: filePath, - stat: fs.statSync(filePath), + stat: fs_1.default.statSync(filePath), base: extensionPath, - contents: fs.createReadStream(filePath) + contents: fs_1.default.createReadStream(filePath) })); - es.readArray(files).pipe(result); + event_stream_1.default.readArray(files).pipe(result); }) .catch(err => result.emit('error', err)); - return result.pipe((0, stats_1.createStatsStream)(path.basename(extensionPath))); + return result.pipe((0, stats_1.createStatsStream)(path_1.default.basename(extensionPath))); } const userAgent = 'VSCode Build'; const baseHeaders = { @@ -212,8 +248,8 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met const json = require('gulp-json-editor'); const [publisher, name] = extensionName.split('.'); const url = `${serviceUrl}/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`; - fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); - const packageJsonFilter = filter('package.json', { restore: true }); + (0, fancy_log_1.default)('Downloading extension:', ansi_colors_1.default.yellow(`${extensionName}@${version}`), '...'); + const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); return (0, fetch_1.fetchUrls)('', { base: url, nodeFetchOptions: { @@ -222,28 +258,28 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met checksumSha256: sha256 }) .pipe(vzip.src()) - .pipe(filter('extension/**')) - .pipe(rename(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) + .pipe((0, gulp_filter_1.default)('extension/**')) + .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) .pipe(packageJsonFilter) - .pipe(buffer()) + .pipe((0, gulp_buffer_1.default)()) .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } function fromGithub({ name, version, repo, sha256, metadata }) { const json = require('gulp-json-editor'); - fancyLog('Downloading extension from GH:', ansiColors.yellow(`${name}@${version}`), '...'); - const packageJsonFilter = filter('package.json', { restore: true }); + (0, fancy_log_1.default)('Downloading extension from GH:', ansi_colors_1.default.yellow(`${name}@${version}`), '...'); + const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); return (0, fetch_1.fetchGithub)(new URL(repo).pathname, { version, name: name => name.endsWith('.vsix'), checksumSha256: sha256 }) - .pipe(buffer()) + .pipe((0, gulp_buffer_1.default)()) .pipe(vzip.src()) - .pipe(filter('extension/**')) - .pipe(rename(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) + .pipe((0, gulp_filter_1.default)('extension/**')) + .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) .pipe(packageJsonFilter) - .pipe(buffer()) + .pipe((0, gulp_buffer_1.default)()) .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } @@ -269,7 +305,7 @@ const marketplaceWebExtensionsExclude = new Set([ 'ms-vscode.js-debug', 'ms-vscode.vscode-js-profile-table' ]); -const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const productJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productJson.builtInExtensions || []; const webBuiltInExtensions = productJson.webBuiltInExtensions || []; /** @@ -326,7 +362,7 @@ function packageNativeLocalExtensionsStream(forWeb, disableMangle) { * @returns a stream */ function packageAllLocalExtensionsStream(forWeb, disableMangle) { - return es.merge([ + return event_stream_1.default.merge([ packageNonNativeLocalExtensionsStream(forWeb, disableMangle), packageNativeLocalExtensionsStream(forWeb, disableMangle) ]); @@ -338,20 +374,20 @@ function packageAllLocalExtensionsStream(forWeb, disableMangle) { */ function doPackageLocalExtensionsStream(forWeb, disableMangle, native) { const nativeExtensionsSet = new Set(nativeExtensions); - const localExtensionsDescriptions = (glob.sync('extensions/*/package.json') + const localExtensionsDescriptions = (glob_1.default.sync('extensions/*/package.json') .map(manifestPath => { - const absoluteManifestPath = path.join(root, manifestPath); - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); + const absoluteManifestPath = path_1.default.join(root, manifestPath); + const extensionPath = path_1.default.dirname(path_1.default.join(root, manifestPath)); + const extensionName = path_1.default.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) .filter(({ name }) => native ? nativeExtensionsSet.has(name) : !nativeExtensionsSet.has(name)) .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); - const localExtensionsStream = minifyExtensionResources(es.merge(...localExtensionsDescriptions.map(extension => { + const localExtensionsStream = minifyExtensionResources(event_stream_1.default.merge(...localExtensionsDescriptions.map(extension => { return fromLocal(extension.path, forWeb, disableMangle) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); + .pipe((0, gulp_rename_1.default)(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); }))); let result; if (forWeb) { @@ -360,10 +396,10 @@ function doPackageLocalExtensionsStream(forWeb, disableMangle, native) { else { // also include shared production node modules const productionDependencies = (0, dependencies_1.getProductionDependencies)('extensions/'); - const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); - result = es.merge(localExtensionsStream, gulp.src(dependenciesSrc, { base: '.' }) - .pipe(util2.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) - .pipe(util2.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`)))); + const dependenciesSrc = productionDependencies.map(d => path_1.default.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); + result = event_stream_1.default.merge(localExtensionsStream, gulp_1.default.src(dependenciesSrc, { base: '.' }) + .pipe(util2.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) + .pipe(util2.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`)))); } return (result .pipe(util2.setExecutableBit(['**/*.sh']))); @@ -373,9 +409,9 @@ function packageMarketplaceExtensionsStream(forWeb) { ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), ...(forWeb ? webBuiltInExtensions : []) ]; - const marketplaceExtensionsStream = minifyExtensionResources(es.merge(...marketplaceExtensionsDescriptions + const marketplaceExtensionsStream = minifyExtensionResources(event_stream_1.default.merge(...marketplaceExtensionsDescriptions .map(extension => { - const src = (0, builtInExtensions_1.getExtensionStream)(extension).pipe(rename(p => p.dirname = `extensions/${p.dirname}`)); + const src = (0, builtInExtensions_1.getExtensionStream)(extension).pipe((0, gulp_rename_1.default)(p => p.dirname = `extensions/${p.dirname}`)); return updateExtensionPackageJSON(src, (data) => { delete data.scripts; delete data.dependencies; @@ -389,30 +425,30 @@ function packageMarketplaceExtensionsStream(forWeb) { function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; try { - const extensionsFolders = fs.readdirSync(extensionsRoot); + const extensionsFolders = fs_1.default.readdirSync(extensionsRoot); for (const extensionFolder of extensionsFolders) { if (exclude.indexOf(extensionFolder) >= 0) { continue; } - const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); - if (!fs.existsSync(packageJSONPath)) { + const packageJSONPath = path_1.default.join(extensionsRoot, extensionFolder, 'package.json'); + if (!fs_1.default.existsSync(packageJSONPath)) { continue; } - const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); + const packageJSON = JSON.parse(fs_1.default.readFileSync(packageJSONPath).toString('utf8')); if (!isWebExtension(packageJSON)) { continue; } - const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const children = fs_1.default.readdirSync(path_1.default.join(extensionsRoot, extensionFolder)); const packageNLSPath = children.filter(child => child === 'package.nls.json')[0]; - const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; + const packageNLS = packageNLSPath ? JSON.parse(fs_1.default.readFileSync(path_1.default.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; scannedExtensions.push({ extensionPath: extensionFolder, packageJSON, packageNLS, - readmePath: readme ? path.join(extensionFolder, readme) : undefined, - changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, + readmePath: readme ? path_1.default.join(extensionFolder, readme) : undefined, + changelogPath: changelog ? path_1.default.join(extensionFolder, changelog) : undefined, }); } return scannedExtensions; @@ -423,7 +459,7 @@ function scanBuiltinExtensions(extensionsRoot, exclude = []) { } function translatePackageJSON(packageJSON, packageNLSPath) { const CharCode_PC = '%'.charCodeAt(0); - const packageNls = JSON.parse(fs.readFileSync(packageNLSPath).toString()); + const packageNls = JSON.parse(fs_1.default.readFileSync(packageNLSPath).toString()); const translate = (obj) => { for (const key in obj) { const val = obj[key]; @@ -444,7 +480,7 @@ function translatePackageJSON(packageJSON, packageNLSPath) { translate(packageJSON); return packageJSON; } -const extensionsPath = path.join(root, 'extensions'); +const extensionsPath = path_1.default.join(root, 'extensions'); // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ 'markdown-language-features/esbuild-notebook.js', @@ -463,7 +499,7 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; if (outputRoot) { - config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + config.output.path = path_1.default.join(outputRoot, path_1.default.relative(path_1.default.dirname(configPath), config.output.path)); } webpackConfigs.push(config); } @@ -475,18 +511,18 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { for (const stats of fullStats.children) { const outputPath = stats.outputPath; if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const relativePath = path_1.default.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.`); + (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green(taskName)} ${ansi_colors_1.default.cyan(match[0])} with ${stats.errors.length} errors.`); } if (Array.isArray(stats.errors)) { stats.errors.forEach((error) => { - fancyLog.error(error); + fancy_log_1.default.error(error); }); } if (Array.isArray(stats.warnings)) { stats.warnings.forEach((warning) => { - fancyLog.warn(warning); + fancy_log_1.default.warn(warning); }); } } @@ -506,7 +542,7 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { else { webpack(webpackConfigs).run((err, stats) => { if (err) { - fancyLog.error(err); + fancy_log_1.default.error(err); reject(); } else { @@ -520,9 +556,9 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { 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.`); + (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); for (const match of matches || []) { - fancyLog.error(match); + fancy_log_1.default.error(match); } } const tasks = scripts.map(({ script, outputRoot }) => { @@ -534,7 +570,7 @@ async function esbuildExtensions(taskName, isWatch, scripts) { if (outputRoot) { args.push('--outputRoot', outputRoot); } - const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { + const proc = child_process_1.default.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { if (error) { return reject(error); } @@ -542,7 +578,7 @@ async function esbuildExtensions(taskName, isWatch, scripts) { return resolve(); }); proc.stdout.on('data', (data) => { - fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); + (0, fancy_log_1.default)(`${ansi_colors_1.default.green(taskName)}: ${data.toString('utf8')}`); }); }); }); @@ -550,8 +586,8 @@ async function esbuildExtensions(taskName, isWatch, scripts) { } async function buildExtensionMedia(isWatch, outputRoot) { return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + script: path_1.default.join(extensionsPath, p), + outputRoot: outputRoot ? path_1.default.join(root, outputRoot, path_1.default.dirname(p)) : undefined }))); } //# sourceMappingURL=extensions.js.map \ No newline at end of file diff --git a/code/build/lib/extensions.ts b/code/build/lib/extensions.ts index a881d3153da..7ddfbb03587 100644 --- a/code/build/lib/extensions.ts +++ b/code/build/lib/extensions.ts @@ -3,24 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -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'; +import es from 'event-stream'; +import fs from 'fs'; +import cp from 'child_process'; +import glob from 'glob'; +import gulp from 'gulp'; +import path from 'path'; import { Stream } from 'stream'; -import * as File from 'vinyl'; +import File from 'vinyl'; import { createStatsStream } from './stats'; import * as util2 from './util'; const vzip = require('gulp-vinyl-zip'); -import filter = require('gulp-filter'); -import rename = require('gulp-rename'); -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -const buffer = require('gulp-buffer'); +import filter from 'gulp-filter'; +import rename from 'gulp-rename'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; -import webpack = require('webpack'); +import webpack from 'webpack'; import { getProductionDependencies } from './dependencies'; import { IExtensionDefinition, getExtensionStream } from './builtInExtensions'; import { getVersion } from './getVersion'; diff --git a/code/build/lib/fetch.js b/code/build/lib/fetch.js index b7da65f4af2..078706cdd00 100644 --- a/code/build/lib/fetch.js +++ b/code/build/lib/fetch.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.fetchUrls = fetchUrls; exports.fetchUrl = fetchUrl; exports.fetchGithub = fetchGithub; -const es = require("event-stream"); -const VinylFile = require("vinyl"); -const log = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const crypto = require("crypto"); -const through2 = require("through2"); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_1 = __importDefault(require("vinyl")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const crypto_1 = __importDefault(require("crypto")); +const through2_1 = __importDefault(require("through2")); function fetchUrls(urls, options) { if (options === undefined) { options = {}; @@ -23,7 +26,7 @@ function fetchUrls(urls, options) { if (!Array.isArray(urls)) { urls = [urls]; } - return es.readArray(urls).pipe(es.map((data, cb) => { + return event_stream_1.default.readArray(urls).pipe(event_stream_1.default.map((data, cb) => { const url = [options.base, data].join(''); fetchUrl(url, options).then(file => { cb(undefined, file); @@ -37,7 +40,7 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { try { let startTime = 0; if (verbose) { - log(`Start fetching ${ansiColors.magenta(url)}${retries !== 10 ? ` (${10 - retries} retry)` : ''}`); + (0, fancy_log_1.default)(`Start fetching ${ansi_colors_1.default.magenta(url)}${retries !== 10 ? ` (${10 - retries} retry)` : ''}`); startTime = new Date().getTime(); } const controller = new AbortController(); @@ -48,33 +51,33 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { signal: controller.signal /* Typings issue with lib.dom.d.ts */ }); if (verbose) { - log(`Fetch completed: Status ${response.status}. Took ${ansiColors.magenta(`${new Date().getTime() - startTime} ms`)}`); + (0, fancy_log_1.default)(`Fetch completed: Status ${response.status}. Took ${ansi_colors_1.default.magenta(`${new Date().getTime() - startTime} ms`)}`); } if (response.ok && (response.status >= 200 && response.status < 300)) { const contents = Buffer.from(await response.arrayBuffer()); if (options.checksumSha256) { - const actualSHA256Checksum = crypto.createHash('sha256').update(contents).digest('hex'); + const actualSHA256Checksum = crypto_1.default.createHash('sha256').update(contents).digest('hex'); if (actualSHA256Checksum !== options.checksumSha256) { - throw new Error(`Checksum mismatch for ${ansiColors.cyan(url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); + throw new Error(`Checksum mismatch for ${ansi_colors_1.default.cyan(url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); } else if (verbose) { - log(`Verified SHA256 checksums match for ${ansiColors.cyan(url)}`); + (0, fancy_log_1.default)(`Verified SHA256 checksums match for ${ansi_colors_1.default.cyan(url)}`); } } else if (verbose) { - log(`Skipping checksum verification for ${ansiColors.cyan(url)} because no expected checksum was provided`); + (0, fancy_log_1.default)(`Skipping checksum verification for ${ansi_colors_1.default.cyan(url)} because no expected checksum was provided`); } if (verbose) { - log(`Fetched response body buffer: ${ansiColors.magenta(`${contents.byteLength} bytes`)}`); + (0, fancy_log_1.default)(`Fetched response body buffer: ${ansi_colors_1.default.magenta(`${contents.byteLength} bytes`)}`); } - return new VinylFile({ + return new vinyl_1.default({ cwd: '/', base: options.base, path: url, contents }); } - let err = `Request ${ansiColors.magenta(url)} failed with status code: ${response.status}`; + let err = `Request ${ansi_colors_1.default.magenta(url)} failed with status code: ${response.status}`; if (response.status === 403) { err += ' (you may be rate limited)'; } @@ -86,7 +89,7 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { } catch (e) { if (verbose) { - log(`Fetching ${ansiColors.cyan(url)} failed: ${e}`); + (0, fancy_log_1.default)(`Fetching ${ansi_colors_1.default.cyan(url)} failed: ${e}`); } if (retries > 0) { await new Promise(resolve => setTimeout(resolve, retryDelay)); @@ -117,7 +120,7 @@ function fetchGithub(repo, options) { base: 'https://api.github.com', verbose: options.verbose, nodeFetchOptions: { headers: ghApiHeaders } - }).pipe(through2.obj(async function (file, _enc, callback) { + }).pipe(through2_1.default.obj(async function (file, _enc, callback) { const assetFilter = typeof options.name === 'string' ? (name) => name === options.name : options.name; const asset = JSON.parse(file.contents.toString()).assets.find((a) => assetFilter(a.name)); if (!asset) { diff --git a/code/build/lib/fetch.ts b/code/build/lib/fetch.ts index 0c44b8e567f..47a65b88fb5 100644 --- a/code/build/lib/fetch.ts +++ b/code/build/lib/fetch.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as VinylFile from 'vinyl'; -import * as log from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as crypto from 'crypto'; -import * as through2 from 'through2'; +import es from 'event-stream'; +import VinylFile from 'vinyl'; +import log from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import crypto from 'crypto'; +import through2 from 'through2'; import { Stream } from 'stream'; export interface IFetchOptions { diff --git a/code/build/lib/formatter.js b/code/build/lib/formatter.js index 29f265c8289..1085ea8f488 100644 --- a/code/build/lib/formatter.js +++ b/code/build/lib/formatter.js @@ -1,17 +1,20 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.format = format; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const fs = require("fs"); -const path = require("path"); -const ts = require("typescript"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const typescript_1 = __importDefault(require("typescript")); class LanguageServiceHost { files = {}; addFile(fileName, text) { - this.files[fileName] = ts.ScriptSnapshot.fromString(text); + this.files[fileName] = typescript_1.default.ScriptSnapshot.fromString(text); } fileExists(path) { return !!this.files[path]; @@ -20,18 +23,18 @@ class LanguageServiceHost { return this.files[path]?.getText(0, this.files[path].getLength()); } // for ts.LanguageServiceHost - getCompilationSettings = () => ts.getDefaultCompilerOptions(); + getCompilationSettings = () => typescript_1.default.getDefaultCompilerOptions(); getScriptFileNames = () => Object.keys(this.files); getScriptVersion = (_fileName) => '0'; getScriptSnapshot = (fileName) => this.files[fileName]; getCurrentDirectory = () => process.cwd(); - getDefaultLibFileName = (options) => ts.getDefaultLibFilePath(options); + getDefaultLibFileName = (options) => typescript_1.default.getDefaultLibFilePath(options); } const defaults = { baseIndentSize: 0, indentSize: 4, tabSize: 4, - indentStyle: ts.IndentStyle.Smart, + indentStyle: typescript_1.default.IndentStyle.Smart, newLineCharacter: '\r\n', convertTabsToSpaces: false, insertSpaceAfterCommaDelimiter: true, @@ -54,14 +57,14 @@ const defaults = { const getOverrides = (() => { let value; return () => { - value ??= JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); + value ??= JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); return value; }; })(); function format(fileName, text) { const host = new LanguageServiceHost(); host.addFile(fileName, text); - const languageService = ts.createLanguageService(host); + const languageService = typescript_1.default.createLanguageService(host); const edits = languageService.getFormattingEditsForDocument(fileName, { ...defaults, ...getOverrides() }); edits .sort((a, b) => a.span.start - b.span.start) diff --git a/code/build/lib/formatter.ts b/code/build/lib/formatter.ts index 0d9035b3d87..993722e5f92 100644 --- a/code/build/lib/formatter.ts +++ b/code/build/lib/formatter.ts @@ -2,9 +2,9 @@ * 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 path from 'path'; -import * as ts from 'typescript'; +import fs from 'fs'; +import path from 'path'; +import ts from 'typescript'; class LanguageServiceHost implements ts.LanguageServiceHost { diff --git a/code/build/lib/getVersion.js b/code/build/lib/getVersion.js index b50ead538a2..7606c17ab14 100644 --- a/code/build/lib/getVersion.js +++ b/code/build/lib/getVersion.js @@ -3,9 +3,42 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getVersion = getVersion; -const git = require("./git"); +const git = __importStar(require("./git")); function getVersion(root) { let version = process.env['BUILD_SOURCEVERSION']; if (!version || !/^[0-9a-f]{40}$/i.test(version.trim())) { diff --git a/code/build/lib/git.js b/code/build/lib/git.js index 798a408bdb9..30de97ed6e3 100644 --- a/code/build/lib/git.js +++ b/code/build/lib/git.js @@ -1,21 +1,24 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getVersion = getVersion; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const path = require("path"); -const fs = require("fs"); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); /** * Returns the sha1 commit version of a repository or undefined in case of failure. */ function getVersion(repo) { - const git = path.join(repo, '.git'); - const headPath = path.join(git, 'HEAD'); + const git = path_1.default.join(repo, '.git'); + const headPath = path_1.default.join(git, 'HEAD'); let head; try { - head = fs.readFileSync(headPath, 'utf8').trim(); + head = fs_1.default.readFileSync(headPath, 'utf8').trim(); } catch (e) { return undefined; @@ -28,17 +31,17 @@ function getVersion(repo) { return undefined; } const ref = refMatch[1]; - const refPath = path.join(git, ref); + const refPath = path_1.default.join(git, ref); try { - return fs.readFileSync(refPath, 'utf8').trim(); + return fs_1.default.readFileSync(refPath, 'utf8').trim(); } catch (e) { // noop } - const packedRefsPath = path.join(git, 'packed-refs'); + const packedRefsPath = path_1.default.join(git, 'packed-refs'); let refsRaw; try { - refsRaw = fs.readFileSync(packedRefsPath, 'utf8').trim(); + refsRaw = fs_1.default.readFileSync(packedRefsPath, 'utf8').trim(); } catch (e) { return undefined; diff --git a/code/build/lib/git.ts b/code/build/lib/git.ts index dbb424f21df..a3c23d8c29b 100644 --- a/code/build/lib/git.ts +++ b/code/build/lib/git.ts @@ -2,8 +2,8 @@ * 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 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; /** * Returns the sha1 commit version of a repository or undefined in case of failure. diff --git a/code/build/lib/i18n.js b/code/build/lib/i18n.js index 93864eedf09..f2dd063cacd 100644 --- a/code/build/lib/i18n.js +++ b/code/build/lib/i18n.js @@ -3,6 +3,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.EXTERNAL_EXTENSIONS = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; exports.processNlsFiles = processNlsFiles; @@ -12,20 +15,20 @@ exports.createXlfFilesForExtensions = createXlfFilesForExtensions; exports.createXlfFilesForIsl = createXlfFilesForIsl; exports.prepareI18nPackFiles = prepareI18nPackFiles; exports.prepareIslFiles = prepareIslFiles; -const path = require("path"); -const fs = require("fs"); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); const event_stream_1 = require("event-stream"); -const jsonMerge = require("gulp-merge-json"); -const File = require("vinyl"); -const xml2js = require("xml2js"); -const gulp = require("gulp"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const iconv = require("@vscode/iconv-lite-umd"); +const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); +const vinyl_1 = __importDefault(require("vinyl")); +const xml2js_1 = __importDefault(require("xml2js")); +const gulp_1 = __importDefault(require("gulp")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const iconv_lite_umd_1 = __importDefault(require("@vscode/iconv-lite-umd")); const l10n_dev_1 = require("@vscode/l10n-dev"); -const REPO_ROOT_PATH = path.join(__dirname, '../..'); +const REPO_ROOT_PATH = path_1.default.join(__dirname, '../..'); function log(message, ...rest) { - fancyLog(ansiColors.green('[i18n]'), message, ...rest); + (0, fancy_log_1.default)(ansi_colors_1.default.green('[i18n]'), message, ...rest); } exports.defaultLanguages = [ { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, @@ -188,7 +191,7 @@ class XLF { } static parse = function (xlfString) { return new Promise((resolve, reject) => { - const parser = new xml2js.Parser(); + const parser = new xml2js_1.default.Parser(); const files = []; parser.parseString(xlfString, function (err, result) { if (err) { @@ -278,8 +281,8 @@ function stripComments(content) { return result; } function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { - const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); - if (!fs.existsSync(languageDirectory)) { + const languageDirectory = path_1.default.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); + if (!fs_1.default.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.`); } @@ -289,10 +292,10 @@ function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { log(`Generating nls bundles for: ${language.id}`); } const languageFolderName = language.translationId || language.id; - const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); + const i18nFile = path_1.default.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); let allMessages; - if (fs.existsSync(i18nFile)) { - const content = stripComments(fs.readFileSync(i18nFile, 'utf8')); + if (fs_1.default.existsSync(i18nFile)) { + const content = stripComments(fs_1.default.readFileSync(i18nFile, 'utf8')); allMessages = JSON.parse(content); } let nlsIndex = 0; @@ -304,7 +307,7 @@ function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { nlsIndex++; } } - emitter.queue(new File({ + emitter.queue(new vinyl_1.default({ contents: Buffer.from(`${fileHeader} globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), @@ -315,10 +318,10 @@ globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), } function processNlsFiles(opts) { return (0, event_stream_1.through)(function (file) { - const fileName = path.basename(file.path); + const fileName = path_1.default.basename(file.path); if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles (TODO@esm this file is not created anymore, pick another) try { - const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + const json = JSON.parse(fs_1.default.readFileSync(path_1.default.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); if (NLSKeysFormat.is(json)) { processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); } @@ -366,7 +369,7 @@ function getResource(sourceFile) { } function createXlfFilesForCoreBundle() { return (0, event_stream_1.through)(function (file) { - const basename = path.basename(file.path); + const basename = path_1.default.basename(file.path); if (basename === 'nls.metadata.json') { if (file.isBuffer()) { const xlfs = Object.create(null); @@ -393,7 +396,7 @@ function createXlfFilesForCoreBundle() { for (const resource in xlfs) { const xlf = xlfs[resource]; const filePath = `${xlf.project}/${resource.replace(/\//g, '_')}.xlf`; - const xlfFile = new File({ + const xlfFile = new vinyl_1.default({ path: filePath, contents: Buffer.from(xlf.toString(), 'utf8') }); @@ -413,7 +416,7 @@ function createXlfFilesForCoreBundle() { } function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder) { const prefix = prefixWithBuildFolder ? '.build/' : ''; - return gulp + return gulp_1.default .src([ // For source code of extensions `${prefix}extensions/${extensionFolderName}/{src,client,server}/**/*.{ts,tsx}`, @@ -429,12 +432,12 @@ function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder callback(); return; } - const extension = path.extname(file.relative); + const extension = path_1.default.extname(file.relative); if (extension !== '.json') { const contents = file.contents.toString('utf8'); try { const json = (0, l10n_dev_1.getL10nJson)([{ contents, extension }]); - callback(undefined, new File({ + callback(undefined, new vinyl_1.default({ path: `extensions/${extensionFolderName}/bundle.l10n.json`, contents: Buffer.from(JSON.stringify(json), 'utf8') })); @@ -464,7 +467,7 @@ function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder } callback(undefined, file); })) - .pipe(jsonMerge({ + .pipe((0, gulp_merge_json_1.default)({ fileName: `extensions/${extensionFolderName}/bundle.l10n.json`, jsonSpace: '', concatArrays: true @@ -481,16 +484,16 @@ function createXlfFilesForExtensions() { let folderStreamEndEmitted = false; return (0, event_stream_1.through)(function (extensionFolder) { const folderStream = this; - const stat = fs.statSync(extensionFolder.path); + const stat = fs_1.default.statSync(extensionFolder.path); if (!stat.isDirectory()) { return; } - const extensionFolderName = path.basename(extensionFolder.path); + const extensionFolderName = path_1.default.basename(extensionFolder.path); if (extensionFolderName === 'node_modules') { return; } // Get extension id and use that as the id - const manifest = fs.readFileSync(path.join(extensionFolder.path, 'package.json'), 'utf-8'); + const manifest = fs_1.default.readFileSync(path_1.default.join(extensionFolder.path, 'package.json'), 'utf-8'); const manifestJson = JSON.parse(manifest); const extensionId = manifestJson.publisher + '.' + manifestJson.name; counter++; @@ -501,17 +504,17 @@ function createXlfFilesForExtensions() { } return _l10nMap; } - (0, event_stream_1.merge)(gulp.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }), createL10nBundleForExtension(extensionFolderName, exports.EXTERNAL_EXTENSIONS.includes(extensionId))).pipe((0, event_stream_1.through)(function (file) { + (0, event_stream_1.merge)(gulp_1.default.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }), createL10nBundleForExtension(extensionFolderName, exports.EXTERNAL_EXTENSIONS.includes(extensionId))).pipe((0, event_stream_1.through)(function (file) { if (file.isBuffer()) { const buffer = file.contents; - const basename = path.basename(file.path); + const basename = path_1.default.basename(file.path); if (basename === 'package.nls.json') { const json = JSON.parse(buffer.toString('utf8')); getL10nMap().set(`extensions/${extensionId}/package`, json); } else if (basename === 'nls.metadata.json') { const json = JSON.parse(buffer.toString('utf8')); - const relPath = path.relative(`.build/extensions/${extensionFolderName}`, path.dirname(file.path)); + const relPath = path_1.default.relative(`.build/extensions/${extensionFolderName}`, path_1.default.dirname(file.path)); for (const file in json) { const fileContent = json[file]; const info = Object.create(null); @@ -536,8 +539,8 @@ function createXlfFilesForExtensions() { } }, function () { if (_l10nMap?.size > 0) { - const xlfFile = new File({ - path: path.join(extensionsProject, extensionId + '.xlf'), + const xlfFile = new vinyl_1.default({ + path: path_1.default.join(extensionsProject, extensionId + '.xlf'), contents: Buffer.from((0, l10n_dev_1.getL10nXlf)(_l10nMap), 'utf8') }); folderStream.queue(xlfFile); @@ -560,7 +563,7 @@ function createXlfFilesForExtensions() { function createXlfFilesForIsl() { return (0, event_stream_1.through)(function (file) { let projectName, resourceFile; - if (path.basename(file.path) === 'messages.en.isl') { + if (path_1.default.basename(file.path) === 'messages.en.isl') { projectName = setupProject; resourceFile = 'messages.xlf'; } @@ -602,8 +605,8 @@ function createXlfFilesForIsl() { 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') }); + const newFilePath = path_1.default.join(projectName, resourceFile); + const xlfFile = new vinyl_1.default({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') }); this.queue(xlfFile); }); } @@ -623,8 +626,8 @@ function createI18nFile(name, messages) { if (process.platform === 'win32') { content = content.replace(/\n/g, '\r\n'); } - return new File({ - path: path.join(name + '.i18n.json'), + return new vinyl_1.default({ + path: path_1.default.join(name + '.i18n.json'), contents: Buffer.from(content, 'utf8') }); } @@ -643,9 +646,9 @@ function prepareI18nPackFiles(resultingTranslationPaths) { const extensionsPacks = {}; const errors = []; return (0, event_stream_1.through)(function (xlf) { - let project = path.basename(path.dirname(path.dirname(xlf.relative))); + let project = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(xlf.relative))); // strip `-new` since vscode-extensions-loc uses the `-new` suffix to indicate that it's from the new loc pipeline - const resource = path.basename(path.basename(xlf.relative, '.xlf'), '-new'); + const resource = path_1.default.basename(path_1.default.basename(xlf.relative, '.xlf'), '-new'); if (exports.EXTERNAL_EXTENSIONS.find(e => e === resource)) { project = extensionsProject; } @@ -720,11 +723,11 @@ function prepareIslFiles(language, innoSetupConfig) { function createIslFile(name, messages, language, innoSetup) { const content = []; let originalContent; - if (path.basename(name) === 'Default') { - originalContent = new TextModel(fs.readFileSync(name + '.isl', 'utf8')); + if (path_1.default.basename(name) === 'Default') { + originalContent = new TextModel(fs_1.default.readFileSync(name + '.isl', 'utf8')); } else { - originalContent = new TextModel(fs.readFileSync(name + '.en.isl', 'utf8')); + originalContent = new TextModel(fs_1.default.readFileSync(name + '.en.isl', 'utf8')); } originalContent.lines.forEach(line => { if (line.length > 0) { @@ -746,10 +749,10 @@ function createIslFile(name, messages, language, innoSetup) { } } }); - const basename = path.basename(name); + const basename = path_1.default.basename(name); const filePath = `${basename}.${language.id}.isl`; - const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); - return new File({ + const encoded = iconv_lite_umd_1.default.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); + return new vinyl_1.default({ path: filePath, contents: Buffer.from(encoded), }); diff --git a/code/build/lib/i18n.resources.json b/code/build/lib/i18n.resources.json index fb732ae1eaf..2b510757855 100644 --- a/code/build/lib/i18n.resources.json +++ b/code/build/lib/i18n.resources.json @@ -58,10 +58,6 @@ "name": "vs/workbench/contrib/commands", "project": "vscode-workbench" }, - { - "name": "vs/workbench/contrib/mappedEdits", - "project": "vscode-workbench" - }, { "name": "vs/workbench/contrib/markdown", "project": "vscode-workbench" diff --git a/code/build/lib/i18n.ts b/code/build/lib/i18n.ts index c493ad40eaf..a54a8af9523 100644 --- a/code/build/lib/i18n.ts +++ b/code/build/lib/i18n.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; import { map, merge, through, ThroughStream } from 'event-stream'; -import * as jsonMerge from 'gulp-merge-json'; -import * as File from 'vinyl'; -import * as xml2js from 'xml2js'; -import * as gulp from 'gulp'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as iconv from '@vscode/iconv-lite-umd'; +import jsonMerge from 'gulp-merge-json'; +import File from 'vinyl'; +import xml2js from 'xml2js'; +import gulp from 'gulp'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import iconv from '@vscode/iconv-lite-umd'; import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); diff --git a/code/build/lib/inlineMeta.js b/code/build/lib/inlineMeta.js index 5ec7e9e9c07..3b473ae091e 100644 --- a/code/build/lib/inlineMeta.js +++ b/code/build/lib/inlineMeta.js @@ -3,9 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.inlineMeta = inlineMeta; -const es = require("event-stream"); +const event_stream_1 = __importDefault(require("event-stream")); const path_1 = require("path"); const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; // TODO in order to inline `product.json`, more work is @@ -16,7 +19,7 @@ const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; // - a `target` is added in `gulpfile.vscode.win32.js` // const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; function inlineMeta(result, ctx) { - return result.pipe(es.through(function (file) { + return result.pipe(event_stream_1.default.through(function (file) { if (matchesFile(file, ctx)) { let content = file.contents.toString(); let markerFound = false; diff --git a/code/build/lib/inlineMeta.ts b/code/build/lib/inlineMeta.ts index dc061aca8d1..2a0db13d06e 100644 --- a/code/build/lib/inlineMeta.ts +++ b/code/build/lib/inlineMeta.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; +import es from 'event-stream'; import { basename } from 'path'; -import * as File from 'vinyl'; +import File from 'vinyl'; export interface IInlineMetaContext { readonly targetPaths: string[]; diff --git a/code/build/lib/layersChecker.js b/code/build/lib/layersChecker.js index 978ce2625b5..5cf5c58402c 100644 --- a/code/build/lib/layersChecker.js +++ b/code/build/lib/layersChecker.js @@ -3,8 +3,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); +const typescript_1 = __importDefault(require("typescript")); const fs_1 = require("fs"); const path_1 = require("path"); const minimatch_1 = require("minimatch"); @@ -72,6 +75,7 @@ const CORE_TYPES = [ 'Body', '__type', '__global', + 'Performance', 'PerformanceMark', 'PerformanceObserver', 'ImportMeta', @@ -294,8 +298,8 @@ let hasErrors = false; function checkFile(program, sourceFile, rule) { checkNode(sourceFile); function checkNode(node) { - if (node.kind !== ts.SyntaxKind.Identifier) { - return ts.forEachChild(node, checkNode); // recurse down + if (node.kind !== typescript_1.default.SyntaxKind.Identifier) { + return typescript_1.default.forEachChild(node, checkNode); // recurse down } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(node); @@ -351,11 +355,11 @@ function checkFile(program, sourceFile, rule) { } } function createProgram(tsconfigPath) { - const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); - const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; - const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); - const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); - return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); + const tsConfig = typescript_1.default.readConfigFile(tsconfigPath, typescript_1.default.sys.readFile); + const configHostParser = { fileExists: fs_1.existsSync, readDirectory: typescript_1.default.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = typescript_1.default.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); + const compilerHost = typescript_1.default.createCompilerHost(tsConfigParsed.options, true); + return typescript_1.default.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); } // // Create program and start checking diff --git a/code/build/lib/layersChecker.ts b/code/build/lib/layersChecker.ts index 454f8874e3d..63377328928 100644 --- a/code/build/lib/layersChecker.ts +++ b/code/build/lib/layersChecker.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; +import ts from 'typescript'; import { readFileSync, existsSync } from 'fs'; import { resolve, dirname, join } from 'path'; import { match } from 'minimatch'; @@ -73,6 +73,7 @@ const CORE_TYPES = [ 'Body', '__type', '__global', + 'Performance', 'PerformanceMark', 'PerformanceObserver', 'ImportMeta', diff --git a/code/build/lib/mangle/index.js b/code/build/lib/mangle/index.js index 0d90e088579..e763b0f83a8 100644 --- a/code/build/lib/mangle/index.js +++ b/code/build/lib/mangle/index.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.Mangler = void 0; -const v8 = require("node:v8"); -const fs = require("fs"); -const path = require("path"); +const node_v8_1 = __importDefault(require("node:v8")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const process_1 = require("process"); const source_map_1 = require("source-map"); -const ts = require("typescript"); +const typescript_1 = __importDefault(require("typescript")); const url_1 = require("url"); -const workerpool = require("workerpool"); +const workerpool_1 = __importDefault(require("workerpool")); const staticLanguageServiceHost_1 = require("./staticLanguageServiceHost"); const buildfile = require('../../buildfile'); class ShortIdent { @@ -66,29 +69,29 @@ class ClassData { this.node = node; const candidates = []; for (const member of node.members) { - if (ts.isMethodDeclaration(member)) { + if (typescript_1.default.isMethodDeclaration(member)) { // method `foo() {}` candidates.push(member); } - else if (ts.isPropertyDeclaration(member)) { + else if (typescript_1.default.isPropertyDeclaration(member)) { // property `foo = 234` candidates.push(member); } - else if (ts.isGetAccessor(member)) { + else if (typescript_1.default.isGetAccessor(member)) { // getter: `get foo() { ... }` candidates.push(member); } - else if (ts.isSetAccessor(member)) { + else if (typescript_1.default.isSetAccessor(member)) { // setter: `set foo() { ... }` candidates.push(member); } - else if (ts.isConstructorDeclaration(member)) { + else if (typescript_1.default.isConstructorDeclaration(member)) { // constructor-prop:`constructor(private foo) {}` for (const param of member.parameters) { - if (hasModifier(param, ts.SyntaxKind.PrivateKeyword) - || hasModifier(param, ts.SyntaxKind.ProtectedKeyword) - || hasModifier(param, ts.SyntaxKind.PublicKeyword) - || hasModifier(param, ts.SyntaxKind.ReadonlyKeyword)) { + if (hasModifier(param, typescript_1.default.SyntaxKind.PrivateKeyword) + || hasModifier(param, typescript_1.default.SyntaxKind.ProtectedKeyword) + || hasModifier(param, typescript_1.default.SyntaxKind.PublicKeyword) + || hasModifier(param, typescript_1.default.SyntaxKind.ReadonlyKeyword)) { candidates.push(param); } } @@ -109,8 +112,8 @@ class ClassData { } const { name } = node; let ident = name.getText(); - if (name.kind === ts.SyntaxKind.ComputedPropertyName) { - if (name.expression.kind !== ts.SyntaxKind.StringLiteral) { + if (name.kind === typescript_1.default.SyntaxKind.ComputedPropertyName) { + if (name.expression.kind !== typescript_1.default.SyntaxKind.StringLiteral) { // unsupported: [Symbol.foo] or [abc + 'field'] return; } @@ -120,10 +123,10 @@ class ClassData { return ident; } static _getFieldType(node) { - if (hasModifier(node, ts.SyntaxKind.PrivateKeyword)) { + if (hasModifier(node, typescript_1.default.SyntaxKind.PrivateKeyword)) { return 2 /* FieldType.Private */; } - else if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword)) { + else if (hasModifier(node, typescript_1.default.SyntaxKind.ProtectedKeyword)) { return 1 /* FieldType.Protected */; } else { @@ -307,7 +310,7 @@ class DeclarationData { this.replacementName = fileIdents.next(); } getLocations(service) { - if (ts.isVariableDeclaration(this.node)) { + if (typescript_1.default.isVariableDeclaration(this.node)) { // If the const aliases any types, we need to rename those too const definitionResult = service.getDefinitionAndBoundSpan(this.fileName, this.node.name.getStart()); if (definitionResult?.definitions && definitionResult.definitions.length > 1) { @@ -355,20 +358,20 @@ class Mangler { this.projectPath = projectPath; this.log = log; this.config = config; - this.renameWorkerPool = workerpool.pool(path.join(__dirname, 'renameWorker.js'), { + this.renameWorkerPool = workerpool_1.default.pool(path_1.default.join(__dirname, 'renameWorker.js'), { maxWorkers: 1, minWorkers: 'max' }); } async computeNewFileContents(strictImplicitPublicHandling) { - const service = ts.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(this.projectPath)); + const service = typescript_1.default.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(this.projectPath)); // STEP: // - Find all classes and their field info. // - Find exported symbols. const fileIdents = new ShortIdent('$'); const visit = (node) => { if (this.config.manglePrivateFields) { - if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + if (typescript_1.default.isClassDeclaration(node) || typescript_1.default.isClassExpression(node)) { const anchor = node.name ?? node; const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`; if (this.allClassDataByKey.has(key)) { @@ -381,19 +384,19 @@ class Mangler { // Find exported classes, functions, and vars if (( // Exported class - ts.isClassDeclaration(node) - && hasModifier(node, ts.SyntaxKind.ExportKeyword) + typescript_1.default.isClassDeclaration(node) + && hasModifier(node, typescript_1.default.SyntaxKind.ExportKeyword) && node.name) || ( // Exported function - ts.isFunctionDeclaration(node) - && ts.isSourceFile(node.parent) - && hasModifier(node, ts.SyntaxKind.ExportKeyword) + typescript_1.default.isFunctionDeclaration(node) + && typescript_1.default.isSourceFile(node.parent) + && hasModifier(node, typescript_1.default.SyntaxKind.ExportKeyword) && node.name && node.body // On named function and not on the overload ) || ( // Exported variable - ts.isVariableDeclaration(node) - && hasModifier(node.parent.parent, ts.SyntaxKind.ExportKeyword) // Variable statement is exported - && ts.isSourceFile(node.parent.parent.parent)) + typescript_1.default.isVariableDeclaration(node) + && hasModifier(node.parent.parent, typescript_1.default.SyntaxKind.ExportKeyword) // Variable statement is exported + && typescript_1.default.isSourceFile(node.parent.parent.parent)) // Disabled for now because we need to figure out how to handle // enums that are used in monaco or extHost interfaces. /* || ( @@ -411,17 +414,17 @@ class Mangler { this.allExportedSymbols.add(new DeclarationData(node.getSourceFile().fileName, node, fileIdents)); } } - ts.forEachChild(node, visit); + typescript_1.default.forEachChild(node, visit); }; for (const file of service.getProgram().getSourceFiles()) { if (!file.isDeclarationFile) { - ts.forEachChild(file, visit); + typescript_1.default.forEachChild(file, visit); } } this.log(`Done collecting. Classes: ${this.allClassDataByKey.size}. Exported symbols: ${this.allExportedSymbols.size}`); // STEP: connect sub and super-types const setupParents = (data) => { - const extendsClause = data.node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword); + const extendsClause = data.node.heritageClauses?.find(h => h.token === typescript_1.default.SyntaxKind.ExtendsKeyword); if (!extendsClause) { // no EXTENDS-clause return; @@ -502,7 +505,7 @@ class Mangler { .then((locations) => ({ newName, locations }))); }; for (const data of this.allClassDataByKey.values()) { - if (hasModifier(data.node, ts.SyntaxKind.DeclareKeyword)) { + if (hasModifier(data.node, typescript_1.default.SyntaxKind.DeclareKeyword)) { continue; } fields: for (const [name, info] of data.fields) { @@ -550,7 +553,7 @@ class Mangler { let savedBytes = 0; for (const item of service.getProgram().getSourceFiles()) { const { mapRoot, sourceRoot } = service.getProgram().getCompilerOptions(); - const projectDir = path.dirname(this.projectPath); + const projectDir = path_1.default.dirname(this.projectPath); const sourceMapRoot = mapRoot ?? (0, url_1.pathToFileURL)(sourceRoot ?? projectDir).toString(); // source maps let generator; @@ -562,7 +565,7 @@ class Mangler { } else { // source map generator - const relativeFileName = normalize(path.relative(projectDir, item.fileName)); + const relativeFileName = normalize(path_1.default.relative(projectDir, item.fileName)); const mappingsByLine = new Map(); // apply renames edits.sort((a, b) => b.offset - a.offset); @@ -601,7 +604,7 @@ class Mangler { }); } // source map generation, make sure to get mappings per line correct - generator = new source_map_1.SourceMapGenerator({ file: path.basename(item.fileName), sourceRoot: sourceMapRoot }); + generator = new source_map_1.SourceMapGenerator({ file: path_1.default.basename(item.fileName), sourceRoot: sourceMapRoot }); generator.setSourceContent(relativeFileName, item.getFullText()); for (const [, mappings] of mappingsByLine) { let lineDelta = 0; @@ -619,19 +622,19 @@ class Mangler { } service.dispose(); this.renameWorkerPool.terminate(); - this.log(`Done: ${savedBytes / 1000}kb saved, memory-usage: ${JSON.stringify(v8.getHeapStatistics())}`); + this.log(`Done: ${savedBytes / 1000}kb saved, memory-usage: ${JSON.stringify(node_v8_1.default.getHeapStatistics())}`); return result; } } exports.Mangler = Mangler; // --- ast utils function hasModifier(node, kind) { - const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; + const modifiers = typescript_1.default.canHaveModifiers(node) ? typescript_1.default.getModifiers(node) : undefined; return Boolean(modifiers?.find(mode => mode.kind === kind)); } function isInAmbientContext(node) { for (let p = node.parent; p; p = p.parent) { - if (ts.isModuleDeclaration(p)) { + if (typescript_1.default.isModuleDeclaration(p)) { return true; } } @@ -641,21 +644,21 @@ function normalize(path) { return path.replace(/\\/g, '/'); } async function _run() { - const root = path.join(__dirname, '..', '..', '..'); - const projectBase = path.join(root, 'src'); - const projectPath = path.join(projectBase, 'tsconfig.json'); - const newProjectBase = path.join(path.dirname(projectBase), path.basename(projectBase) + '2'); - fs.cpSync(projectBase, newProjectBase, { recursive: true }); + const root = path_1.default.join(__dirname, '..', '..', '..'); + const projectBase = path_1.default.join(root, 'src'); + const projectPath = path_1.default.join(projectBase, 'tsconfig.json'); + const newProjectBase = path_1.default.join(path_1.default.dirname(projectBase), path_1.default.basename(projectBase) + '2'); + fs_1.default.cpSync(projectBase, newProjectBase, { recursive: true }); const mangler = new Mangler(projectPath, console.log, { mangleExports: true, manglePrivateFields: true, }); for (const [fileName, contents] of await mangler.computeNewFileContents(new Set(['saveState']))) { - const newFilePath = path.join(newProjectBase, path.relative(projectBase, fileName)); - await fs.promises.mkdir(path.dirname(newFilePath), { recursive: true }); - await fs.promises.writeFile(newFilePath, contents.out); + const newFilePath = path_1.default.join(newProjectBase, path_1.default.relative(projectBase, fileName)); + await fs_1.default.promises.mkdir(path_1.default.dirname(newFilePath), { recursive: true }); + await fs_1.default.promises.writeFile(newFilePath, contents.out); if (contents.sourceMap) { - await fs.promises.writeFile(newFilePath + '.map', contents.sourceMap); + await fs_1.default.promises.writeFile(newFilePath + '.map', contents.sourceMap); } } } diff --git a/code/build/lib/mangle/index.ts b/code/build/lib/mangle/index.ts index 15975c2385d..122b9f7bc6f 100644 --- a/code/build/lib/mangle/index.ts +++ b/code/build/lib/mangle/index.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as v8 from 'node:v8'; -import * as fs from 'fs'; -import * as path from 'path'; +import v8 from 'node:v8'; +import fs from 'fs'; +import path from 'path'; import { argv } from 'process'; import { Mapping, SourceMapGenerator } from 'source-map'; -import * as ts from 'typescript'; +import ts from 'typescript'; import { pathToFileURL } from 'url'; -import * as workerpool from 'workerpool'; +import workerpool from 'workerpool'; import { StaticLanguageServiceHost } from './staticLanguageServiceHost'; const buildfile = require('../../buildfile'); diff --git a/code/build/lib/mangle/renameWorker.js b/code/build/lib/mangle/renameWorker.js index 6cd429b8c9a..8bd59a4e2d5 100644 --- a/code/build/lib/mangle/renameWorker.js +++ b/code/build/lib/mangle/renameWorker.js @@ -3,20 +3,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const workerpool = require("workerpool"); +const typescript_1 = __importDefault(require("typescript")); +const workerpool_1 = __importDefault(require("workerpool")); const staticLanguageServiceHost_1 = require("./staticLanguageServiceHost"); let service; function findRenameLocations(projectPath, fileName, position) { if (!service) { - service = ts.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(projectPath)); + service = typescript_1.default.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(projectPath)); } return service.findRenameLocations(fileName, position, false, false, { providePrefixAndSuffixTextForRename: true, }) ?? []; } -workerpool.worker({ +workerpool_1.default.worker({ findRenameLocations }); //# sourceMappingURL=renameWorker.js.map \ No newline at end of file diff --git a/code/build/lib/mangle/renameWorker.ts b/code/build/lib/mangle/renameWorker.ts index 29b34e8c514..0cce5677593 100644 --- a/code/build/lib/mangle/renameWorker.ts +++ b/code/build/lib/mangle/renameWorker.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; -import * as workerpool from 'workerpool'; +import ts from 'typescript'; +import workerpool from 'workerpool'; import { StaticLanguageServiceHost } from './staticLanguageServiceHost'; let service: ts.LanguageService | undefined; diff --git a/code/build/lib/mangle/staticLanguageServiceHost.js b/code/build/lib/mangle/staticLanguageServiceHost.js index 1f338f0e61c..7777888dd06 100644 --- a/code/build/lib/mangle/staticLanguageServiceHost.js +++ b/code/build/lib/mangle/staticLanguageServiceHost.js @@ -3,10 +3,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.StaticLanguageServiceHost = void 0; -const ts = require("typescript"); -const path = require("path"); +const typescript_1 = __importDefault(require("typescript")); +const path_1 = __importDefault(require("path")); class StaticLanguageServiceHost { projectPath; _cmdLine; @@ -14,11 +17,11 @@ class StaticLanguageServiceHost { constructor(projectPath) { this.projectPath = projectPath; const existingOptions = {}; - const parsed = ts.readConfigFile(projectPath, ts.sys.readFile); + const parsed = typescript_1.default.readConfigFile(projectPath, typescript_1.default.sys.readFile); if (parsed.error) { throw parsed.error; } - this._cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(projectPath), existingOptions); + this._cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, path_1.default.dirname(projectPath), existingOptions); if (this._cmdLine.errors.length > 0) { throw parsed.error; } @@ -38,28 +41,28 @@ class StaticLanguageServiceHost { getScriptSnapshot(fileName) { let result = this._scriptSnapshots.get(fileName); if (result === undefined) { - const content = ts.sys.readFile(fileName); + const content = typescript_1.default.sys.readFile(fileName); if (content === undefined) { return undefined; } - result = ts.ScriptSnapshot.fromString(content); + result = typescript_1.default.ScriptSnapshot.fromString(content); this._scriptSnapshots.set(fileName, result); } return result; } getCurrentDirectory() { - return path.dirname(this.projectPath); + return path_1.default.dirname(this.projectPath); } getDefaultLibFileName(options) { - return ts.getDefaultLibFilePath(options); + return typescript_1.default.getDefaultLibFilePath(options); } - directoryExists = ts.sys.directoryExists; - getDirectories = ts.sys.getDirectories; - fileExists = ts.sys.fileExists; - readFile = ts.sys.readFile; - readDirectory = ts.sys.readDirectory; + directoryExists = typescript_1.default.sys.directoryExists; + getDirectories = typescript_1.default.sys.getDirectories; + fileExists = typescript_1.default.sys.fileExists; + readFile = typescript_1.default.sys.readFile; + readDirectory = typescript_1.default.sys.readDirectory; // this is necessary to make source references work. - realpath = ts.sys.realpath; + realpath = typescript_1.default.sys.realpath; } exports.StaticLanguageServiceHost = StaticLanguageServiceHost; //# sourceMappingURL=staticLanguageServiceHost.js.map \ No newline at end of file diff --git a/code/build/lib/mangle/staticLanguageServiceHost.ts b/code/build/lib/mangle/staticLanguageServiceHost.ts index c2793342ce3..b41b4e52133 100644 --- a/code/build/lib/mangle/staticLanguageServiceHost.ts +++ b/code/build/lib/mangle/staticLanguageServiceHost.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; -import * as path from 'path'; +import ts from 'typescript'; +import path from 'path'; export class StaticLanguageServiceHost implements ts.LanguageServiceHost { diff --git a/code/build/lib/monaco-api.js b/code/build/lib/monaco-api.js index 2052806c46b..84cc556cb62 100644 --- a/code/build/lib/monaco-api.js +++ b/code/build/lib/monaco-api.js @@ -3,21 +3,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; exports.run3 = run3; exports.execute = execute; -const fs = require("fs"); -const path = require("path"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); const dtsv = '3'; const tsfmt = require('../../tsfmt.json'); -const SRC = path.join(__dirname, '../../src'); -exports.RECIPE_PATH = path.join(__dirname, '../monaco/monaco.d.ts.recipe'); -const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts'); +const SRC = path_1.default.join(__dirname, '../../src'); +exports.RECIPE_PATH = path_1.default.join(__dirname, '../monaco/monaco.d.ts.recipe'); +const DECLARATION_PATH = path_1.default.join(__dirname, '../../src/vs/monaco.d.ts'); function logErr(message, ...rest) { - fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest); + (0, fancy_log_1.default)(ansi_colors_1.default.yellow(`[monaco.d.ts]`), message, ...rest); } function isDeclaration(ts, a) { return (a.kind === ts.SyntaxKind.InterfaceDeclaration @@ -464,7 +467,7 @@ function generateDeclarationFile(ts, recipe, sourceFileGetter) { }; } function _run(ts, sourceFileGetter) { - const recipe = fs.readFileSync(exports.RECIPE_PATH).toString(); + const recipe = fs_1.default.readFileSync(exports.RECIPE_PATH).toString(); const t = generateDeclarationFile(ts, recipe, sourceFileGetter); if (!t) { return null; @@ -472,7 +475,7 @@ function _run(ts, sourceFileGetter) { const result = t.result; const usageContent = t.usageContent; const enums = t.enums; - const currentContent = fs.readFileSync(DECLARATION_PATH).toString(); + const currentContent = fs_1.default.readFileSync(DECLARATION_PATH).toString(); const one = currentContent.replace(/\r\n/gm, '\n'); const other = result.replace(/\r\n/gm, '\n'); const isTheSame = (one === other); @@ -486,13 +489,13 @@ function _run(ts, sourceFileGetter) { } class FSProvider { existsSync(filePath) { - return fs.existsSync(filePath); + return fs_1.default.existsSync(filePath); } statSync(filePath) { - return fs.statSync(filePath); + return fs_1.default.statSync(filePath); } readFileSync(_moduleId, filePath) { - return fs.readFileSync(filePath); + return fs_1.default.readFileSync(filePath); } } exports.FSProvider = FSProvider; @@ -532,9 +535,9 @@ class DeclarationResolver { } _getFileName(moduleId) { if (/\.d\.ts$/.test(moduleId)) { - return path.join(SRC, moduleId); + return path_1.default.join(SRC, moduleId); } - return path.join(SRC, `${moduleId}.ts`); + return path_1.default.join(SRC, `${moduleId}.ts`); } _getDeclarationSourceFile(moduleId) { const fileName = this._getFileName(moduleId); diff --git a/code/build/lib/monaco-api.ts b/code/build/lib/monaco-api.ts index 288bec0f858..5dc9a04266c 100644 --- a/code/build/lib/monaco-api.ts +++ b/code/build/lib/monaco-api.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import fs from 'fs'; import type * as ts from 'typescript'; -import * as path from 'path'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import path from 'path'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; const dtsv = '3'; diff --git a/code/build/lib/nls.js b/code/build/lib/nls.js index 6ddcd46167a..12e60a36ec9 100644 --- a/code/build/lib/nls.js +++ b/code/build/lib/nls.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.nls = nls; -const lazy = require("lazy.js"); +const lazy_js_1 = __importDefault(require("lazy.js")); const event_stream_1 = require("event-stream"); -const File = require("vinyl"); -const sm = require("source-map"); -const path = require("path"); -const sort = require("gulp-sort"); +const vinyl_1 = __importDefault(require("vinyl")); +const source_map_1 = __importDefault(require("source-map")); +const path_1 = __importDefault(require("path")); +const gulp_sort_1 = __importDefault(require("gulp-sort")); var CollectStepResult; (function (CollectStepResult) { CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; @@ -46,7 +49,7 @@ function nls(options) { let base; const input = (0, event_stream_1.through)(); const output = input - .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files + .pipe((0, gulp_sort_1.default)()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files .pipe((0, event_stream_1.through)(function (f) { if (!f.sourceMap) { return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); @@ -57,7 +60,7 @@ function nls(options) { } const root = f.sourceMap.sourceRoot; if (root) { - source = path.join(root, source); + source = path_1.default.join(root, source); } const typescript = f.sourceMap.sourcesContent[0]; if (!typescript) { @@ -67,7 +70,7 @@ function nls(options) { this.emit('data', _nls.patchFile(f, typescript, options)); }, function () { for (const file of [ - new File({ + new vinyl_1.default({ contents: Buffer.from(JSON.stringify({ keys: _nls.moduleToNLSKeys, messages: _nls.moduleToNLSMessages, @@ -75,17 +78,17 @@ function nls(options) { base, path: `${base}/nls.metadata.json` }), - new File({ + new vinyl_1.default({ contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), base, path: `${base}/nls.messages.json` }), - new File({ + new vinyl_1.default({ contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), base, path: `${base}/nls.keys.json` }), - new File({ + new vinyl_1.default({ contents: Buffer.from(`/*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ @@ -111,7 +114,7 @@ var _nls; _nls.allNLSModulesAndKeys = []; let allNLSMessagesIndex = 0; function fileFrom(file, contents, path = file.path) { - return new File({ + return new vinyl_1.default({ contents: Buffer.from(contents), base: file.base, cwd: file.cwd, @@ -163,7 +166,7 @@ var _nls; const service = ts.createLanguageService(serviceHost); const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); // all imports - const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); + const imports = (0, lazy_js_1.default)(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); // import nls = require('vs/nls'); const importEqualsDeclarations = imports .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) @@ -188,7 +191,7 @@ var _nls; .filter(r => !r.isWriteAccess) // find the deepest call expressions AST nodes that contain those references .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => lazy(a).last()) + .map(a => (0, lazy_js_1.default)(a).last()) .filter(n => !!n) .map(n => n) // only `localize` calls @@ -214,7 +217,7 @@ var _nls; const localizeCallExpressions = localizeReferences .concat(namedLocalizeReferences) .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => lazy(a).last()) + .map(a => (0, lazy_js_1.default)(a).last()) .filter(n => !!n) .map(n => n); // collect everything @@ -281,18 +284,18 @@ var _nls; } } toString() { - return lazy(this.lines).zip(this.lineEndings) + return (0, lazy_js_1.default)(this.lines).zip(this.lineEndings) .flatten().toArray().join(''); } } function patchJavascript(patches, contents) { const model = new TextModel(contents); // patch the localize calls - lazy(patches).reverse().each(p => model.apply(p)); + (0, lazy_js_1.default)(patches).reverse().each(p => model.apply(p)); return model.toString(); } function patchSourcemap(patches, rsm, smc) { - const smg = new sm.SourceMapGenerator({ + const smg = new source_map_1.default.SourceMapGenerator({ file: rsm.file, sourceRoot: rsm.sourceRoot }); @@ -317,10 +320,10 @@ var _nls; generated.column += lengthDiff; patches.pop(); } - source = rsm.sourceRoot ? path.relative(rsm.sourceRoot, m.source) : m.source; + source = rsm.sourceRoot ? path_1.default.relative(rsm.sourceRoot, m.source) : m.source; source = source.replace(/\\/g, '/'); smg.addMapping({ source, name: m.name, original, generated }); - }, null, sm.SourceMapConsumer.GENERATED_ORDER); + }, null, source_map_1.default.SourceMapConsumer.GENERATED_ORDER); if (source) { smg.setSourceContent(source, smc.sourceContentFor(source)); } @@ -341,7 +344,7 @@ var _nls; } const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); - const smc = new sm.SourceMapConsumer(sourcemap); + const smc = new source_map_1.default.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); // build patches const toPatch = (c) => { @@ -349,7 +352,7 @@ var _nls; const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end))); return { span: { start, end }, content: c.content }; }; - const localizePatches = lazy(localizeCalls) + const localizePatches = (0, lazy_js_1.default)(localizeCalls) .map(lc => (options.preserveEnglish ? [ { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") ] : [ @@ -358,7 +361,7 @@ var _nls; ])) .flatten() .map(toPatch); - const localize2Patches = lazy(localize2Calls) + const localize2Patches = (0, lazy_js_1.default)(localize2Calls) .map(lc => ({ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") )) .map(toPatch); diff --git a/code/build/lib/nls.ts b/code/build/lib/nls.ts index cac832903a3..ef2afc5d7c8 100644 --- a/code/build/lib/nls.ts +++ b/code/build/lib/nls.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import type * as ts from 'typescript'; -import * as lazy from 'lazy.js'; +import lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; -import * as File from 'vinyl'; -import * as sm from 'source-map'; -import * as path from 'path'; -import * as sort from 'gulp-sort'; +import File from 'vinyl'; +import sm from 'source-map'; +import path from 'path'; +import sort from 'gulp-sort'; declare class FileSourceMap extends File { public sourceMap: sm.RawSourceMap; diff --git a/code/build/lib/node.js b/code/build/lib/node.js index 74a54a3c170..01a381183ff 100644 --- a/code/build/lib/node.js +++ b/code/build/lib/node.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const fs = require("fs"); -const root = path.dirname(path.dirname(__dirname)); -const npmrcPath = path.join(root, 'remote', '.npmrc'); -const npmrc = fs.readFileSync(npmrcPath, 'utf8'); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const npmrcPath = path_1.default.join(root, 'remote', '.npmrc'); +const npmrc = fs_1.default.readFileSync(npmrcPath, 'utf8'); const version = /^target="(.*)"$/m.exec(npmrc)[1]; const platform = process.platform; const arch = process.arch; const node = platform === 'win32' ? 'node.exe' : 'node'; -const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); +const nodePath = path_1.default.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); console.log(nodePath); //# sourceMappingURL=node.js.map \ No newline at end of file diff --git a/code/build/lib/node.ts b/code/build/lib/node.ts index 4beb13ae91b..a2fdc361aa1 100644 --- a/code/build/lib/node.ts +++ b/code/build/lib/node.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; const root = path.dirname(path.dirname(__dirname)); const npmrcPath = path.join(root, 'remote', '.npmrc'); diff --git a/code/build/lib/optimize.js b/code/build/lib/optimize.js index 83f34dc0745..d75a2978697 100644 --- a/code/build/lib/optimize.js +++ b/code/build/lib/optimize.js @@ -3,34 +3,70 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.bundleTask = bundleTask; exports.minifyTask = minifyTask; -const es = require("event-stream"); -const gulp = require("gulp"); -const filter = require("gulp-filter"); -const path = require("path"); -const fs = require("fs"); -const pump = require("pump"); -const VinylFile = require("vinyl"); -const bundle = require("./bundle"); +const event_stream_1 = __importDefault(require("event-stream")); +const gulp_1 = __importDefault(require("gulp")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const pump_1 = __importDefault(require("pump")); +const vinyl_1 = __importDefault(require("vinyl")); +const bundle = __importStar(require("./bundle")); const postcss_1 = require("./postcss"); -const esbuild = require("esbuild"); -const sourcemaps = require("gulp-sourcemaps"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const REPO_ROOT_PATH = path.join(__dirname, '../..'); +const esbuild_1 = __importDefault(require("esbuild")); +const gulp_sourcemaps_1 = __importDefault(require("gulp-sourcemaps")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const REPO_ROOT_PATH = path_1.default.join(__dirname, '../..'); const DEFAULT_FILE_HEADER = [ '/*!--------------------------------------------------------', ' * Copyright (C) Microsoft Corporation. All rights reserved.', ' *--------------------------------------------------------*/' ].join('\n'); function bundleESMTask(opts) { - const resourcesStream = es.through(); // this stream will contain the resources - const bundlesStream = es.through(); // this stream will contain the bundled files + const resourcesStream = event_stream_1.default.through(); // this stream will contain the resources + const bundlesStream = event_stream_1.default.through(); // this stream will contain the bundled files const entryPoints = opts.entryPoints.map(entryPoint => { if (typeof entryPoint === 'string') { - return { name: path.parse(entryPoint).name }; + return { name: path_1.default.parse(entryPoint).name }; } return entryPoint; }); @@ -44,7 +80,7 @@ function bundleESMTask(opts) { const files = []; const tasks = []; for (const entryPoint of entryPoints) { - fancyLog(`Bundled entry point: ${ansiColors.yellow(entryPoint.name)}...`); + (0, fancy_log_1.default)(`Bundled entry point: ${ansi_colors_1.default.yellow(entryPoint.name)}...`); // support for 'dest' via esbuild#in/out const dest = entryPoint.dest?.replace(/\.[^/.]+$/, '') ?? entryPoint.name; // banner contents @@ -54,14 +90,14 @@ function bundleESMTask(opts) { }; // TS Boilerplate if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) { - const tslibPath = path.join(require.resolve('tslib'), '../tslib.es6.js'); - banner.js += await fs.promises.readFile(tslibPath, 'utf-8'); + const tslibPath = path_1.default.join(require.resolve('tslib'), '../tslib.es6.js'); + banner.js += await fs_1.default.promises.readFile(tslibPath, 'utf-8'); } const contentsMapper = { name: 'contents-mapper', setup(build) { build.onLoad({ filter: /\.js$/ }, async ({ path }) => { - const contents = await fs.promises.readFile(path, 'utf-8'); + const contents = await fs_1.default.promises.readFile(path, 'utf-8'); // TS Boilerplate let newContents; if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) { @@ -85,11 +121,11 @@ function bundleESMTask(opts) { // We inline selected modules that are we depend on on startup without // a conditional `await import(...)` by hooking into the resolution. build.onResolve({ filter: /^minimist$/ }, () => { - return { path: path.join(REPO_ROOT_PATH, 'node_modules', 'minimist', 'index.js'), external: false }; + return { path: path_1.default.join(REPO_ROOT_PATH, 'node_modules', 'minimist', 'index.js'), external: false }; }); }, }; - const task = esbuild.build({ + const task = esbuild_1.default.build({ bundle: true, external: entryPoint.exclude, packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages @@ -108,11 +144,11 @@ function bundleESMTask(opts) { banner: entryPoint.name === 'vs/workbench/workbench.web.main' ? undefined : banner, // TODO@esm remove line when we stop supporting web-amd-esm-bridge entryPoints: [ { - in: path.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`), + in: path_1.default.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`), out: dest, } ], - outdir: path.join(REPO_ROOT_PATH, opts.src), + outdir: path_1.default.join(REPO_ROOT_PATH, opts.src), write: false, // enables res.outputFiles metafile: true, // enables res.metafile // minify: NOT enabled because we have a separate minify task that takes care of the TSLib banner as well @@ -126,9 +162,9 @@ function bundleESMTask(opts) { contents: Buffer.from(file.contents), sourceMap: sourceMapFile ? JSON.parse(sourceMapFile.text) : undefined, // support gulp-sourcemaps path: file.path, - base: path.join(REPO_ROOT_PATH, opts.src) + base: path_1.default.join(REPO_ROOT_PATH, opts.src) }; - files.push(new VinylFile(fileProps)); + files.push(new vinyl_1.default(fileProps)); } }); tasks.push(task); @@ -138,13 +174,13 @@ function bundleESMTask(opts) { }; bundleAsync().then((output) => { // bundle output (JS, CSS, SVG...) - es.readArray(output.files).pipe(bundlesStream); + event_stream_1.default.readArray(output.files).pipe(bundlesStream); // forward all resources - gulp.src(opts.resources ?? [], { base: `${opts.src}`, allowEmpty: true }).pipe(resourcesStream); + gulp_1.default.src(opts.resources ?? [], { base: `${opts.src}`, allowEmpty: true }).pipe(resourcesStream); }); - const result = es.merge(bundlesStream, resourcesStream); + const result = event_stream_1.default.merge(bundlesStream, resourcesStream); return result - .pipe(sourcemaps.write('./', { + .pipe(gulp_sourcemaps_1.default.write('./', { sourceRoot: undefined, addComment: true, includeContent: true @@ -152,7 +188,7 @@ function bundleESMTask(opts) { } function bundleTask(opts) { return function () { - return bundleESMTask(opts.esm).pipe(gulp.dest(opts.out)); + return bundleESMTask(opts.esm).pipe(gulp_1.default.dest(opts.out)); }; } function minifyTask(src, sourceMapBaseUrl) { @@ -160,11 +196,11 @@ function minifyTask(src, sourceMapBaseUrl) { return cb => { const cssnano = require('cssnano'); const svgmin = require('gulp-svgmin'); - const jsFilter = filter('**/*.js', { restore: true }); - const cssFilter = filter('**/*.css', { restore: true }); - const svgFilter = filter('**/*.svg', { restore: true }); - pump(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), es.map((f, cb) => { - esbuild.build({ + const jsFilter = (0, gulp_filter_1.default)('**/*.js', { restore: true }); + const cssFilter = (0, gulp_filter_1.default)('**/*.css', { restore: true }); + const svgFilter = (0, gulp_filter_1.default)('**/*.svg', { restore: true }); + (0, pump_1.default)(gulp_1.default.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, gulp_sourcemaps_1.default.init({ loadMaps: true }), event_stream_1.default.map((f, cb) => { + esbuild_1.default.build({ entryPoints: [f.path], minify: true, sourcemap: 'external', @@ -187,12 +223,12 @@ function minifyTask(src, sourceMapBaseUrl) { cb(undefined, f); } }, cb); - }), jsFilter.restore, cssFilter, (0, postcss_1.gulpPostcss)([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), svgFilter.restore, sourcemaps.write('./', { + }), jsFilter.restore, cssFilter, (0, postcss_1.gulpPostcss)([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), svgFilter.restore, gulp_sourcemaps_1.default.write('./', { sourceMappingURL, sourceRoot: undefined, includeContent: true, addComment: true - }), gulp.dest(src + '-min'), (err) => cb(err)); + }), gulp_1.default.dest(src + '-min'), (err) => cb(err)); }; } //# sourceMappingURL=optimize.js.map \ No newline at end of file diff --git a/code/build/lib/optimize.ts b/code/build/lib/optimize.ts index 8c49fa81888..e12c85c3fa0 100644 --- a/code/build/lib/optimize.ts +++ b/code/build/lib/optimize.ts @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as gulp from 'gulp'; -import * as filter from 'gulp-filter'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as pump from 'pump'; -import * as VinylFile from 'vinyl'; +import es from 'event-stream'; +import gulp from 'gulp'; +import filter from 'gulp-filter'; +import path from 'path'; +import fs from 'fs'; +import pump from 'pump'; +import VinylFile from 'vinyl'; import * as bundle from './bundle'; import { gulpPostcss } from './postcss'; -import * as esbuild from 'esbuild'; -import * as sourcemaps from 'gulp-sourcemaps'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import esbuild from 'esbuild'; +import sourcemaps from 'gulp-sourcemaps'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); diff --git a/code/build/lib/policies.js b/code/build/lib/policies.js index 1560dc7415d..d52015c550b 100644 --- a/code/build/lib/policies.js +++ b/code/build/lib/policies.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const child_process_1 = require("child_process"); const fs_1 = require("fs"); -const path = require("path"); -const byline = require("byline"); +const path_1 = __importDefault(require("path")); +const byline_1 = __importDefault(require("byline")); const ripgrep_1 = require("@vscode/ripgrep"); -const Parser = require("tree-sitter"); +const tree_sitter_1 = __importDefault(require("tree-sitter")); const { typescript } = require('tree-sitter-typescript'); const product = require('../../product.json'); const packageJson = require('../../package.json'); @@ -258,7 +261,7 @@ const StringArrayQ = { } }; function getProperty(qtype, node, key) { - const query = new Parser.Query(typescript, `( + const query = new tree_sitter_1.default.Query(typescript, `( (pair key: [(property_identifier)(string)] @key value: ${qtype.Q} @@ -331,7 +334,7 @@ function getPolicy(moduleName, configurationNode, settingNode, policyNode, categ return result; } function getPolicies(moduleName, node) { - const query = new Parser.Query(typescript, ` + const query = new tree_sitter_1.default.Query(typescript, ` ( (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) @@ -360,7 +363,7 @@ async function getFiles(root) { return new Promise((c, e) => { const result = []; const rg = (0, child_process_1.spawn)(ripgrep_1.rgPath, ['-l', 'registerConfiguration\\(', '-g', 'src/**/*.ts', '-g', '!src/**/test/**', root]); - const stream = byline(rg.stdout.setEncoding('utf8')); + const stream = (0, byline_1.default)(rg.stdout.setEncoding('utf8')); stream.on('data', path => result.push(path)); stream.on('error', err => e(err)); stream.on('end', () => c(result)); @@ -494,13 +497,13 @@ async function getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageI return await getSpecificNLS(resourceUrlTemplate, languageId, latestCompatibleVersion); } async function parsePolicies() { - const parser = new Parser(); + const parser = new tree_sitter_1.default(); parser.setLanguage(typescript); const files = await getFiles(process.cwd()); - const base = path.join(process.cwd(), 'src'); + const base = path_1.default.join(process.cwd(), 'src'); const policies = []; for (const file of files) { - const moduleName = path.relative(base, file).replace(/\.ts$/i, '').replace(/\\/g, '/'); + const moduleName = path_1.default.relative(base, file).replace(/\.ts$/i, '').replace(/\\/g, '/'); const contents = await fs_1.promises.readFile(file, { encoding: 'utf8' }); const tree = parser.parse(contents); policies.push(...getPolicies(moduleName, tree.rootNode)); @@ -529,11 +532,11 @@ async function main() { const root = '.build/policies/win32'; await fs_1.promises.rm(root, { recursive: true, force: true }); await fs_1.promises.mkdir(root, { recursive: true }); - await fs_1.promises.writeFile(path.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); + await fs_1.promises.writeFile(path_1.default.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); for (const { languageId, contents } of adml) { - const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); + const languagePath = path_1.default.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); await fs_1.promises.mkdir(languagePath, { recursive: true }); - await fs_1.promises.writeFile(path.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); + await fs_1.promises.writeFile(path_1.default.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); } } if (require.main === module) { diff --git a/code/build/lib/policies.ts b/code/build/lib/policies.ts index f602c8a0d6e..57941d8e967 100644 --- a/code/build/lib/policies.ts +++ b/code/build/lib/policies.ts @@ -5,10 +5,10 @@ import { spawn } from 'child_process'; import { promises as fs } from 'fs'; -import * as path from 'path'; -import * as byline from 'byline'; +import path from 'path'; +import byline from 'byline'; import { rgPath } from '@vscode/ripgrep'; -import * as Parser from 'tree-sitter'; +import Parser from 'tree-sitter'; const { typescript } = require('tree-sitter-typescript'); const product = require('../../product.json'); const packageJson = require('../../package.json'); diff --git a/code/build/lib/postcss.js b/code/build/lib/postcss.js index 356015ab159..210a184e5f5 100644 --- a/code/build/lib/postcss.js +++ b/code/build/lib/postcss.js @@ -1,15 +1,18 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.gulpPostcss = gulpPostcss; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const postcss = require("postcss"); -const es = require("event-stream"); +const postcss_1 = __importDefault(require("postcss")); +const event_stream_1 = __importDefault(require("event-stream")); function gulpPostcss(plugins, handleError) { - const instance = postcss(plugins); - return es.map((file, callback) => { + const instance = (0, postcss_1.default)(plugins); + return event_stream_1.default.map((file, callback) => { if (file.isNull()) { return callback(null, file); } diff --git a/code/build/lib/postcss.ts b/code/build/lib/postcss.ts index cf3121e221e..9ec2188d13a 100644 --- a/code/build/lib/postcss.ts +++ b/code/build/lib/postcss.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as postcss from 'postcss'; -import * as File from 'vinyl'; -import * as es from 'event-stream'; +import postcss from 'postcss'; +import File from 'vinyl'; +import es from 'event-stream'; export function gulpPostcss(plugins: postcss.AcceptedPlugin[], handleError?: (err: Error) => void) { const instance = postcss(plugins); diff --git a/code/build/lib/preLaunch.js b/code/build/lib/preLaunch.js index 4791514fdfe..75207fe50c0 100644 --- a/code/build/lib/preLaunch.js +++ b/code/build/lib/preLaunch.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); // @ts-check -const path = require("path"); +const path_1 = __importDefault(require("path")); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const rootDir = path.resolve(__dirname, '..', '..'); +const rootDir = path_1.default.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); @@ -19,7 +22,7 @@ function runProcess(command, args = []) { } async function exists(subdir) { try { - await fs_1.promises.stat(path.join(rootDir, subdir)); + await fs_1.promises.stat(path_1.default.join(rootDir, subdir)); return true; } catch { diff --git a/code/build/lib/preLaunch.ts b/code/build/lib/preLaunch.ts index e0ea274458a..0c178afcb59 100644 --- a/code/build/lib/preLaunch.ts +++ b/code/build/lib/preLaunch.ts @@ -5,7 +5,7 @@ // @ts-check -import * as path from 'path'; +import path from 'path'; import { spawn } from 'child_process'; import { promises as fs } from 'fs'; diff --git a/code/build/lib/reporter.js b/code/build/lib/reporter.js index 9d4a1b4fd79..16bb44ec539 100644 --- a/code/build/lib/reporter.js +++ b/code/build/lib/reporter.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.createReporter = createReporter; -const es = require("event-stream"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const fs = require("fs"); -const path = require("path"); +const event_stream_1 = __importDefault(require("event-stream")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); class ErrorLog { id; constructor(id) { @@ -23,7 +26,7 @@ class ErrorLog { return; } this.startTime = new Date().getTime(); - fancyLog(`Starting ${ansiColors.green('compilation')}${this.id ? ansiColors.blue(` ${this.id}`) : ''}...`); + (0, fancy_log_1.default)(`Starting ${ansi_colors_1.default.green('compilation')}${this.id ? ansi_colors_1.default.blue(` ${this.id}`) : ''}...`); } onEnd() { if (--this.count > 0) { @@ -37,10 +40,10 @@ class ErrorLog { errors.map(err => { if (!seen.has(err)) { seen.add(err); - fancyLog(`${ansiColors.red('Error')}: ${err}`); + (0, fancy_log_1.default)(`${ansi_colors_1.default.red('Error')}: ${err}`); } }); - fancyLog(`Finished ${ansiColors.green('compilation')}${this.id ? ansiColors.blue(` ${this.id}`) : ''} with ${errors.length} errors after ${ansiColors.magenta((new Date().getTime() - this.startTime) + ' ms')}`); + (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green('compilation')}${this.id ? ansi_colors_1.default.blue(` ${this.id}`) : ''} with ${errors.length} errors after ${ansi_colors_1.default.magenta((new Date().getTime() - this.startTime) + ' ms')}`); const regex = /^([^(]+)\((\d+),(\d+)\): (.*)$/s; const messages = errors .map(err => regex.exec(err)) @@ -49,7 +52,7 @@ class ErrorLog { .map(([, path, line, column, message]) => ({ path, line: parseInt(line), column: parseInt(column), message })); try { const logFileName = 'log' + (this.id ? `_${this.id}` : ''); - fs.writeFileSync(path.join(buildLogFolder, logFileName), JSON.stringify(messages)); + fs_1.default.writeFileSync(path_1.default.join(buildLogFolder, logFileName), JSON.stringify(messages)); } catch (err) { //noop @@ -65,9 +68,9 @@ function getErrorLog(id = '') { } return errorLog; } -const buildLogFolder = path.join(path.dirname(path.dirname(__dirname)), '.build'); +const buildLogFolder = path_1.default.join(path_1.default.dirname(path_1.default.dirname(__dirname)), '.build'); try { - fs.mkdirSync(buildLogFolder); + fs_1.default.mkdirSync(buildLogFolder); } catch (err) { // ignore @@ -81,7 +84,7 @@ function createReporter(id) { result.end = (emitError) => { errors.length = 0; errorLog.onStart(); - return es.through(undefined, function () { + return event_stream_1.default.through(undefined, function () { errorLog.onEnd(); if (emitError && errors.length > 0) { if (!errors.__logged__) { diff --git a/code/build/lib/reporter.ts b/code/build/lib/reporter.ts index 382e0c78546..c21fd841c0d 100644 --- a/code/build/lib/reporter.ts +++ b/code/build/lib/reporter.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as fs from 'fs'; -import * as path from 'path'; +import es from 'event-stream'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import fs from 'fs'; +import path from 'path'; class ErrorLog { constructor(public id: string) { diff --git a/code/build/lib/snapshotLoader.js b/code/build/lib/snapshotLoader.js index 0e58ceedffa..7d9b3f154f1 100644 --- a/code/build/lib/snapshotLoader.js +++ b/code/build/lib/snapshotLoader.js @@ -3,6 +3,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.snaps = void 0; var snaps; (function (snaps) { const fs = require('fs'); @@ -52,5 +54,5 @@ var snaps; fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); cp.execFileSync(mksnapshot, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); } -})(snaps || (snaps = {})); +})(snaps || (exports.snaps = snaps = {})); //# sourceMappingURL=snapshotLoader.js.map \ No newline at end of file diff --git a/code/build/lib/snapshotLoader.ts b/code/build/lib/snapshotLoader.ts index c3d66dba7e1..3cb2191144d 100644 --- a/code/build/lib/snapshotLoader.ts +++ b/code/build/lib/snapshotLoader.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -namespace snaps { +export namespace snaps { const fs = require('fs'); const path = require('path'); diff --git a/code/build/lib/standalone.js b/code/build/lib/standalone.js index 16ae1e2b2d8..0e7a9ecc782 100644 --- a/code/build/lib/standalone.js +++ b/code/build/lib/standalone.js @@ -3,14 +3,50 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractEditor = extractEditor; exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; -const fs = require("fs"); -const path = require("path"); -const tss = require("./treeshaking"); -const REPO_ROOT = path.join(__dirname, '../../'); -const SRC_DIR = path.join(REPO_ROOT, 'src'); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const tss = __importStar(require("./treeshaking")); +const REPO_ROOT = path_1.default.join(__dirname, '../../'); +const SRC_DIR = path_1.default.join(REPO_ROOT, 'src'); const dirCache = {}; function writeFile(filePath, contents) { function ensureDirs(dirPath) { @@ -18,21 +54,21 @@ function writeFile(filePath, contents) { return; } dirCache[dirPath] = true; - ensureDirs(path.dirname(dirPath)); - if (fs.existsSync(dirPath)) { + ensureDirs(path_1.default.dirname(dirPath)); + if (fs_1.default.existsSync(dirPath)) { return; } - fs.mkdirSync(dirPath); + fs_1.default.mkdirSync(dirPath); } - ensureDirs(path.dirname(filePath)); - fs.writeFileSync(filePath, contents); + ensureDirs(path_1.default.dirname(filePath)); + fs_1.default.writeFileSync(filePath, contents); } function extractEditor(options) { const ts = require('typescript'); - const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); + const tsConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions; if (tsConfig.extends) { - compilerOptions = Object.assign({}, require(path.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); + compilerOptions = Object.assign({}, require(path_1.default.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); delete tsConfig.extends; } else { @@ -62,7 +98,7 @@ function extractEditor(options) { const result = tss.shake(options); for (const fileName in result) { if (result.hasOwnProperty(fileName)) { - writeFile(path.join(options.destRoot, fileName), result[fileName]); + writeFile(path_1.default.join(options.destRoot, fileName), result[fileName]); } } const copied = {}; @@ -71,12 +107,12 @@ function extractEditor(options) { return; } copied[fileName] = true; - const srcPath = path.join(options.sourcesRoot, fileName); - const dstPath = path.join(options.destRoot, fileName); - writeFile(dstPath, fs.readFileSync(srcPath)); + const srcPath = path_1.default.join(options.sourcesRoot, fileName); + const dstPath = path_1.default.join(options.destRoot, fileName); + writeFile(dstPath, fs_1.default.readFileSync(srcPath)); }; const writeOutputFile = (fileName, contents) => { - writeFile(path.join(options.destRoot, fileName), contents); + writeFile(path_1.default.join(options.destRoot, fileName), contents); }; for (const fileName in result) { if (result.hasOwnProperty(fileName)) { @@ -86,14 +122,14 @@ function extractEditor(options) { const importedFileName = info.importedFiles[i].fileName; let importedFilePath = importedFileName; if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { - importedFilePath = path.join(path.dirname(fileName), importedFilePath); + importedFilePath = path_1.default.join(path_1.default.dirname(fileName), importedFilePath); } if (/\.css$/.test(importedFilePath)) { transportCSS(importedFilePath, copyFile, writeOutputFile); } else { - const pathToCopy = path.join(options.sourcesRoot, importedFilePath); - if (fs.existsSync(pathToCopy) && !fs.statSync(pathToCopy).isDirectory()) { + const pathToCopy = path_1.default.join(options.sourcesRoot, importedFilePath); + if (fs_1.default.existsSync(pathToCopy) && !fs_1.default.statSync(pathToCopy).isDirectory()) { copyFile(importedFilePath); } } @@ -107,18 +143,18 @@ function extractEditor(options) { ].forEach(copyFile); } function createESMSourcesAndResources2(options) { - const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); - const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); - const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); + const SRC_FOLDER = path_1.default.join(REPO_ROOT, options.srcFolder); + const OUT_FOLDER = path_1.default.join(REPO_ROOT, options.outFolder); + const OUT_RESOURCES_FOLDER = path_1.default.join(REPO_ROOT, options.outResourcesFolder); const getDestAbsoluteFilePath = (file) => { const dest = options.renames[file.replace(/\\/g, '/')] || file; if (dest === 'tsconfig.json') { - return path.join(OUT_FOLDER, `tsconfig.json`); + return path_1.default.join(OUT_FOLDER, `tsconfig.json`); } if (/\.ts$/.test(dest)) { - return path.join(OUT_FOLDER, dest); + return path_1.default.join(OUT_FOLDER, dest); } - return path.join(OUT_RESOURCES_FOLDER, dest); + return path_1.default.join(OUT_RESOURCES_FOLDER, dest); }; const allFiles = walkDirRecursive(SRC_FOLDER); for (const file of allFiles) { @@ -126,15 +162,15 @@ function createESMSourcesAndResources2(options) { continue; } if (file === 'tsconfig.json') { - const tsConfig = JSON.parse(fs.readFileSync(path.join(SRC_FOLDER, file)).toString()); + const tsConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(SRC_FOLDER, file)).toString()); tsConfig.compilerOptions.module = 'es2022'; - tsConfig.compilerOptions.outDir = path.join(path.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); + tsConfig.compilerOptions.outDir = path_1.default.join(path_1.default.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); continue; } if (/\.ts$/.test(file) || /\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file) || /\.ttf$/.test(file)) { // Transport the files directly - write(getDestAbsoluteFilePath(file), fs.readFileSync(path.join(SRC_FOLDER, file))); + write(getDestAbsoluteFilePath(file), fs_1.default.readFileSync(path_1.default.join(SRC_FOLDER, file))); continue; } console.log(`UNKNOWN FILE: ${file}`); @@ -148,10 +184,10 @@ function createESMSourcesAndResources2(options) { return result; } function _walkDirRecursive(dir, result, trimPos) { - const files = fs.readdirSync(dir); + const files = fs_1.default.readdirSync(dir); for (let i = 0; i < files.length; i++) { - const file = path.join(dir, files[i]); - if (fs.statSync(file).isDirectory()) { + const file = path_1.default.join(dir, files[i]); + if (fs_1.default.statSync(file).isDirectory()) { _walkDirRecursive(file, result, trimPos); } else { @@ -206,8 +242,8 @@ function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; } - const filename = path.join(SRC_DIR, module); - const fileContents = fs.readFileSync(filename).toString(); + const filename = path_1.default.join(SRC_DIR, module); + const fileContents = fs_1.default.readFileSync(filename).toString(); const inlineResources = 'base64'; // see https://github.com/microsoft/monaco-editor/issues/148 const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64'); write(module, newContents); @@ -217,12 +253,12 @@ function transportCSS(module, enqueue, write) { const fontMatch = url.match(/^(.*).ttf\?(.*)$/); if (fontMatch) { const relativeFontPath = `${fontMatch[1]}.ttf`; // trim the query parameter - const fontPath = path.join(path.dirname(module), relativeFontPath); + const fontPath = path_1.default.join(path_1.default.dirname(module), relativeFontPath); enqueue(fontPath); return relativeFontPath; } - const imagePath = path.join(path.dirname(module), url); - const fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); + const imagePath = path_1.default.join(path_1.default.dirname(module), url); + const fileContents = fs_1.default.readFileSync(path_1.default.join(SRC_DIR, imagePath)); const MIME = /\.svg$/.test(url) ? 'image/svg+xml' : 'image/png'; let DATA = ';base64,' + fileContents.toString('base64'); if (!forceBase64 && /\.svg$/.test(url)) { diff --git a/code/build/lib/standalone.ts b/code/build/lib/standalone.ts index 8736583fb09..b2ae02f1007 100644 --- a/code/build/lib/standalone.ts +++ b/code/build/lib/standalone.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; import * as tss from './treeshaking'; const REPO_ROOT = path.join(__dirname, '../../'); diff --git a/code/build/lib/stats.js b/code/build/lib/stats.js index e089cb0c1b4..3f6d953ae40 100644 --- a/code/build/lib/stats.js +++ b/code/build/lib/stats.js @@ -3,11 +3,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.createStatsStream = createStatsStream; -const es = require("event-stream"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const event_stream_1 = __importDefault(require("event-stream")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); class Entry { name; totalCount; @@ -28,13 +31,13 @@ class Entry { } else { if (this.totalCount === 1) { - return `Stats for '${ansiColors.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; + return `Stats for '${ansi_colors_1.default.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; } else { const count = this.totalCount < 100 - ? ansiColors.green(this.totalCount.toString()) - : ansiColors.red(this.totalCount.toString()); - return `Stats for '${ansiColors.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; + ? ansi_colors_1.default.green(this.totalCount.toString()) + : ansi_colors_1.default.red(this.totalCount.toString()); + return `Stats for '${ansi_colors_1.default.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; } } } @@ -43,7 +46,7 @@ const _entries = new Map(); function createStatsStream(group, log) { const entry = new Entry(group, 0, 0); _entries.set(entry.name, entry); - return es.through(function (data) { + return event_stream_1.default.through(function (data) { const file = data; if (typeof file.path === 'string') { entry.totalCount += 1; @@ -61,13 +64,13 @@ function createStatsStream(group, log) { }, function () { if (log) { if (entry.totalCount === 1) { - fancyLog(`Stats for '${ansiColors.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); + (0, fancy_log_1.default)(`Stats for '${ansi_colors_1.default.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); } else { const count = entry.totalCount < 100 - ? ansiColors.green(entry.totalCount.toString()) - : ansiColors.red(entry.totalCount.toString()); - fancyLog(`Stats for '${ansiColors.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); + ? ansi_colors_1.default.green(entry.totalCount.toString()) + : ansi_colors_1.default.red(entry.totalCount.toString()); + (0, fancy_log_1.default)(`Stats for '${ansi_colors_1.default.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); } } this.emit('end'); diff --git a/code/build/lib/stats.ts b/code/build/lib/stats.ts index fe4b22453b5..8db55d3e777 100644 --- a/code/build/lib/stats.ts +++ b/code/build/lib/stats.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as File from 'vinyl'; +import es from 'event-stream'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import File from 'vinyl'; class Entry { constructor(readonly name: string, public totalCount: number, public totalSize: number) { } diff --git a/code/build/lib/stylelint/validateVariableNames.js b/code/build/lib/stylelint/validateVariableNames.js index 6a50d1d6894..b0e064e7b56 100644 --- a/code/build/lib/stylelint/validateVariableNames.js +++ b/code/build/lib/stylelint/validateVariableNames.js @@ -3,15 +3,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getVariableNameValidator = getVariableNameValidator; const fs_1 = require("fs"); -const path = require("path"); +const path_1 = __importDefault(require("path")); const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; let knownVariables; function getKnownVariableNames() { if (!knownVariables) { - const knownVariablesFileContent = (0, fs_1.readFileSync)(path.join(__dirname, './vscode-known-variables.json'), 'utf8').toString(); + const knownVariablesFileContent = (0, fs_1.readFileSync)(path_1.default.join(__dirname, './vscode-known-variables.json'), 'utf8').toString(); const knownVariablesInfo = JSON.parse(knownVariablesFileContent); knownVariables = new Set([...knownVariablesInfo.colors, ...knownVariablesInfo.others]); } diff --git a/code/build/lib/stylelint/validateVariableNames.ts b/code/build/lib/stylelint/validateVariableNames.ts index 6d9fa8a7cef..b28aed13f4b 100644 --- a/code/build/lib/stylelint/validateVariableNames.ts +++ b/code/build/lib/stylelint/validateVariableNames.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { readFileSync } from 'fs'; -import path = require('path'); +import path from 'path'; const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; diff --git a/code/build/lib/stylelint/vscode-known-variables.json b/code/build/lib/stylelint/vscode-known-variables.json index 23979f5686a..9cadf39caa3 100644 --- a/code/build/lib/stylelint/vscode-known-variables.json +++ b/code/build/lib/stylelint/vscode-known-variables.json @@ -370,6 +370,7 @@ "--vscode-inlineEdit-originalBackground", "--vscode-inlineEdit-originalChangedLineBackground", "--vscode-inlineEdit-originalChangedTextBackground", + "--vscode-inlineEdit-acceptedBackground", "--vscode-input-background", "--vscode-input-border", "--vscode-input-foreground", @@ -911,6 +912,7 @@ "--zoom-factor", "--test-bar-width", "--widget-color", - "--text-link-decoration" + "--text-link-decoration", + "--vscode-action-item-auto-timeout" ] } diff --git a/code/build/lib/task.js b/code/build/lib/task.js index 597b2a0d397..6887714681a 100644 --- a/code/build/lib/task.js +++ b/code/build/lib/task.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.series = series; exports.parallel = parallel; exports.define = define; -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); function _isPromise(p) { if (typeof p.then === 'function') { return true; @@ -21,14 +24,14 @@ function _renderTime(time) { async function _execute(task) { const name = task.taskName || task.displayName || ``; if (!task._tasks) { - fancyLog('Starting', ansiColors.cyan(name), '...'); + (0, fancy_log_1.default)('Starting', ansi_colors_1.default.cyan(name), '...'); } const startTime = process.hrtime(); await _doExecute(task); const elapsedArr = process.hrtime(startTime); const elapsedNanoseconds = (elapsedArr[0] * 1e9 + elapsedArr[1]); if (!task._tasks) { - fancyLog(`Finished`, ansiColors.cyan(name), 'after', ansiColors.magenta(_renderTime(elapsedNanoseconds / 1e6))); + (0, fancy_log_1.default)(`Finished`, ansi_colors_1.default.cyan(name), 'after', ansi_colors_1.default.magenta(_renderTime(elapsedNanoseconds / 1e6))); } } async function _doExecute(task) { diff --git a/code/build/lib/task.ts b/code/build/lib/task.ts index 7d2a4dee016..6af23983178 100644 --- a/code/build/lib/task.ts +++ b/code/build/lib/task.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; export interface BaseTask { displayName?: string; diff --git a/code/build/lib/test/i18n.test.js b/code/build/lib/test/i18n.test.js index b8f4a2bedef..41aa8a7f668 100644 --- a/code/build/lib/test/i18n.test.js +++ b/code/build/lib/test/i18n.test.js @@ -3,9 +3,45 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const assert = require("assert"); -const i18n = require("../i18n"); +const assert_1 = __importDefault(require("assert")); +const i18n = __importStar(require("../i18n")); suite('XLF Parser Tests', () => { const sampleXlf = 'Key #1Key #2 &'; const sampleTranslatedXlf = 'Key #1Кнопка #1Key #2 &Кнопка #2 &'; @@ -17,25 +53,25 @@ suite('XLF Parser Tests', () => { const xlf = new i18n.XLF('vscode-workbench'); xlf.addFile(name, keys, messages); const xlfString = xlf.toString(); - assert.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf); + assert_1.default.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf); }); test('XLF to keys & messages conversion', () => { i18n.XLF.parse(sampleTranslatedXlf).then(function (resolvedFiles) { - assert.deepStrictEqual(resolvedFiles[0].messages, translatedMessages); - assert.strictEqual(resolvedFiles[0].name, name); + assert_1.default.deepStrictEqual(resolvedFiles[0].messages, translatedMessages); + assert_1.default.strictEqual(resolvedFiles[0].name, name); }); }); test('JSON file source path to Transifex resource match', () => { const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench'; const platform = { name: 'vs/platform', project: editorProject }, editorContrib = { name: 'vs/editor/contrib', project: editorProject }, editor = { name: 'vs/editor', project: editorProject }, base = { name: 'vs/base', project: editorProject }, code = { name: 'vs/code', project: workbenchProject }, workbenchParts = { name: 'vs/workbench/contrib/html', project: workbenchProject }, workbenchServices = { name: 'vs/workbench/services/textfile', project: workbenchProject }, workbench = { name: 'vs/workbench', project: workbenchProject }; - assert.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); - assert.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); - assert.deepStrictEqual(i18n.getResource('vs/editor/common/modes/modesRegistry'), editor); - assert.deepStrictEqual(i18n.getResource('vs/base/common/errorMessage'), base); - assert.deepStrictEqual(i18n.getResource('vs/code/electron-main/window'), code); - assert.deepStrictEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); - assert.deepStrictEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); - assert.deepStrictEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); + assert_1.default.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); + assert_1.default.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); + assert_1.default.deepStrictEqual(i18n.getResource('vs/editor/common/modes/modesRegistry'), editor); + assert_1.default.deepStrictEqual(i18n.getResource('vs/base/common/errorMessage'), base); + assert_1.default.deepStrictEqual(i18n.getResource('vs/code/electron-main/window'), code); + assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); + assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); + assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); }); }); //# sourceMappingURL=i18n.test.js.map \ No newline at end of file diff --git a/code/build/lib/test/i18n.test.ts b/code/build/lib/test/i18n.test.ts index b8a68323dd7..4e4545548b8 100644 --- a/code/build/lib/test/i18n.test.ts +++ b/code/build/lib/test/i18n.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import assert = require('assert'); -import i18n = require('../i18n'); +import assert from 'assert'; +import * as i18n from '../i18n'; suite('XLF Parser Tests', () => { const sampleXlf = 'Key #1Key #2 &'; diff --git a/code/build/lib/treeshaking.js b/code/build/lib/treeshaking.js index af06f4e3ec5..d51eee91f1e 100644 --- a/code/build/lib/treeshaking.js +++ b/code/build/lib/treeshaking.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShakeLevel = void 0; exports.toStringShakeLevel = toStringShakeLevel; exports.shake = shake; -const fs = require("fs"); -const path = require("path"); -const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const TYPESCRIPT_LIB_FOLDER = path_1.default.dirname(require.resolve('typescript/lib/lib.d.ts')); var ShakeLevel; (function (ShakeLevel) { ShakeLevel[ShakeLevel["Files"] = 0] = "Files"; @@ -30,7 +33,7 @@ function printDiagnostics(options, diagnostics) { for (const diag of diagnostics) { let result = ''; if (diag.file) { - result += `${path.join(options.sourcesRoot, diag.file.fileName)}`; + result += `${path_1.default.join(options.sourcesRoot, diag.file.fileName)}`; } if (diag.file && diag.start) { const location = diag.file.getLineAndCharacterOfPosition(diag.start); @@ -72,8 +75,8 @@ function createTypeScriptLanguageService(ts, options) { }); // Add additional typings options.typings.forEach((typing) => { - const filePath = path.join(options.sourcesRoot, typing); - FILES[typing] = fs.readFileSync(filePath).toString(); + const filePath = path_1.default.join(options.sourcesRoot, typing); + FILES[typing] = fs_1.default.readFileSync(filePath).toString(); }); // Resolve libs const RESOLVED_LIBS = processLibFiles(ts, options); @@ -104,19 +107,19 @@ function discoverAndReadFiles(ts, options) { if (options.redirects[moduleId]) { redirectedModuleId = options.redirects[moduleId]; } - const dts_filename = path.join(options.sourcesRoot, redirectedModuleId + '.d.ts'); - if (fs.existsSync(dts_filename)) { - const dts_filecontents = fs.readFileSync(dts_filename).toString(); + const dts_filename = path_1.default.join(options.sourcesRoot, redirectedModuleId + '.d.ts'); + if (fs_1.default.existsSync(dts_filename)) { + const dts_filecontents = fs_1.default.readFileSync(dts_filename).toString(); FILES[`${moduleId}.d.ts`] = dts_filecontents; continue; } - const js_filename = path.join(options.sourcesRoot, redirectedModuleId + '.js'); - if (fs.existsSync(js_filename)) { + const js_filename = path_1.default.join(options.sourcesRoot, redirectedModuleId + '.js'); + if (fs_1.default.existsSync(js_filename)) { // This is an import for a .js file, so ignore it... continue; } - const ts_filename = path.join(options.sourcesRoot, redirectedModuleId + '.ts'); - const ts_filecontents = fs.readFileSync(ts_filename).toString(); + const ts_filename = path_1.default.join(options.sourcesRoot, redirectedModuleId + '.ts'); + const ts_filecontents = fs_1.default.readFileSync(ts_filename).toString(); const info = ts.preProcessFile(ts_filecontents); for (let i = info.importedFiles.length - 1; i >= 0; i--) { const importedFileName = info.importedFiles[i].fileName; @@ -126,7 +129,7 @@ function discoverAndReadFiles(ts, options) { } let importedModuleId = importedFileName; if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { - importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + importedModuleId = path_1.default.join(path_1.default.dirname(moduleId), importedModuleId); if (importedModuleId.endsWith('.js')) { // ESM: code imports require to be relative and have a '.js' file extension importedModuleId = importedModuleId.substr(0, importedModuleId.length - 3); } @@ -148,8 +151,8 @@ function processLibFiles(ts, options) { const key = `defaultLib:${filename}`; if (!result[key]) { // add this file - const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); - const sourceText = fs.readFileSync(filepath).toString(); + const filepath = path_1.default.join(TYPESCRIPT_LIB_FOLDER, filename); + const sourceText = fs_1.default.readFileSync(filepath).toString(); result[key] = sourceText; // precess dependencies and "recurse" const info = ts.preProcessFile(sourceText); @@ -459,7 +462,7 @@ function markNodes(ts, languageService, options) { if (importText.endsWith('.js')) { // ESM: code imports require to be relative and to have a '.js' file extension importText = importText.substr(0, importText.length - 3); } - fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + fullPath = path_1.default.join(path_1.default.dirname(nodeSourceFile.fileName), importText) + '.ts'; } else { fullPath = importText + '.ts'; diff --git a/code/build/lib/treeshaking.ts b/code/build/lib/treeshaking.ts index cd17c5f0278..ac71bb205da 100644 --- a/code/build/lib/treeshaking.ts +++ b/code/build/lib/treeshaking.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; import type * as ts from 'typescript'; const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); diff --git a/code/build/lib/tsb/builder.js b/code/build/lib/tsb/builder.js index e7a2519d1c9..f720699680d 100644 --- a/code/build/lib/tsb/builder.js +++ b/code/build/lib/tsb/builder.js @@ -3,16 +3,52 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.CancellationToken = void 0; exports.createTypeScriptBuilder = createTypeScriptBuilder; -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); -const utils = require("./utils"); -const colors = require("ansi-colors"); -const ts = require("typescript"); -const Vinyl = require("vinyl"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const crypto_1 = __importDefault(require("crypto")); +const utils = __importStar(require("./utils")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const typescript_1 = __importDefault(require("typescript")); +const vinyl_1 = __importDefault(require("vinyl")); const source_map_1 = require("source-map"); var CancellationToken; (function (CancellationToken) { @@ -28,7 +64,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { const host = new LanguageServiceHost(cmd, projectFile, _log); const outHost = new LanguageServiceHost({ ...cmd, options: { ...cmd.options, sourceRoot: cmd.options.outDir } }, cmd.options.outDir ?? '', _log); let lastCycleCheckVersion; - const service = ts.createLanguageService(host, ts.createDocumentRegistry()); + const service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry()); const lastBuildVersion = Object.create(null); const lastDtsHash = Object.create(null); const userWantsDeclarations = cmd.options.declaration; @@ -92,7 +128,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { if (/\.d\.ts$/.test(fileName)) { // if it's already a d.ts file just emit it signature const snapshot = host.getScriptSnapshot(fileName); - const signature = crypto.createHash('sha256') + const signature = crypto_1.default.createHash('sha256') .update(snapshot.getText(0, snapshot.getLength())) .digest('base64'); return resolve({ @@ -109,7 +145,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { continue; } if (/\.d\.ts$/.test(file.name)) { - signature = crypto.createHash('sha256') + signature = crypto_1.default.createHash('sha256') .update(file.text) .digest('base64'); if (!userWantsDeclarations) { @@ -117,7 +153,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { continue; } } - const vinyl = new Vinyl({ + const vinyl = new vinyl_1.default({ path: file.name, contents: Buffer.from(file.text), base: !config._emitWithoutBasePath && baseFor(host.getScriptSnapshot(fileName)) || undefined @@ -125,9 +161,9 @@ function createTypeScriptBuilder(config, projectFile, cmd) { if (!emitSourceMapsInStream && /\.js$/.test(file.name)) { const sourcemapFile = output.outputFiles.filter(f => /\.js\.map$/.test(f.name))[0]; if (sourcemapFile) { - const extname = path.extname(vinyl.relative); - const basename = path.basename(vinyl.relative, extname); - const dirname = path.dirname(vinyl.relative); + const extname = path_1.default.extname(vinyl.relative); + const basename = path_1.default.basename(vinyl.relative, extname); + const dirname = path_1.default.dirname(vinyl.relative); const tsname = (dirname === '.' ? '' : dirname + '/') + basename + '.ts'; let sourceMap = JSON.parse(sourcemapFile.text); sourceMap.sources[0] = tsname.replace(/\\/g, '/'); @@ -359,7 +395,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { delete oldErrors[projectFile]; if (oneCycle) { const cycleError = { - category: ts.DiagnosticCategory.Error, + category: typescript_1.default.DiagnosticCategory.Error, code: 1, file: undefined, start: undefined, @@ -383,7 +419,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { // print stats const headNow = process.memoryUsage().heapUsed; const MB = 1024 * 1024; - _log('[tsb]', `time: ${colors.yellow((Date.now() - t1) + 'ms')} + \nmem: ${colors.cyan(Math.ceil(headNow / MB) + 'MB')} ${colors.bgcyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`); + _log('[tsb]', `time: ${ansi_colors_1.default.yellow((Date.now() - t1) + 'ms')} + \nmem: ${ansi_colors_1.default.cyan(Math.ceil(headNow / MB) + 'MB')} ${ansi_colors_1.default.bgcyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`); headUsed = headNow; }); } @@ -480,11 +516,11 @@ class LanguageServiceHost { let result = this._snapshots[filename]; if (!result && resolve) { try { - result = new VinylScriptSnapshot(new Vinyl({ + result = new VinylScriptSnapshot(new vinyl_1.default({ path: filename, - contents: fs.readFileSync(filename), + contents: fs_1.default.readFileSync(filename), base: this.getCompilationSettings().outDir, - stat: fs.statSync(filename) + stat: fs_1.default.statSync(filename) })); this.addScriptSnapshot(filename, result); } @@ -529,16 +565,16 @@ class LanguageServiceHost { return delete this._snapshots[filename]; } getCurrentDirectory() { - return path.dirname(this._projectPath); + return path_1.default.dirname(this._projectPath); } getDefaultLibFileName(options) { - return ts.getDefaultLibFilePath(options); + return typescript_1.default.getDefaultLibFilePath(options); } - directoryExists = ts.sys.directoryExists; - getDirectories = ts.sys.getDirectories; - fileExists = ts.sys.fileExists; - readFile = ts.sys.readFile; - readDirectory = ts.sys.readDirectory; + directoryExists = typescript_1.default.sys.directoryExists; + getDirectories = typescript_1.default.sys.getDirectories; + fileExists = typescript_1.default.sys.fileExists; + readFile = typescript_1.default.sys.readFile; + readDirectory = typescript_1.default.sys.readDirectory; // ---- dependency management collectDependents(filename, target) { while (this._dependenciesRecomputeList.length) { @@ -570,18 +606,18 @@ class LanguageServiceHost { this._log('processFile', `Missing snapshot for: ${filename}`); return; } - const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true); + const info = typescript_1.default.preProcessFile(snapshot.getText(0, snapshot.getLength()), true); // (0) clear out old dependencies this._dependencies.resetNode(filename); // (1) ///-references info.referencedFiles.forEach(ref => { - const resolvedPath = path.resolve(path.dirname(filename), ref.fileName); + const resolvedPath = path_1.default.resolve(path_1.default.dirname(filename), ref.fileName); const normalizedPath = normalize(resolvedPath); this._dependencies.inertEdge(filename, normalizedPath); }); // (2) import-require statements info.importedFiles.forEach(ref => { - if (!ref.fileName.startsWith('.') || path.extname(ref.fileName) === '') { + if (!ref.fileName.startsWith('.') || path_1.default.extname(ref.fileName) === '') { // node module? return; } @@ -589,8 +625,8 @@ class LanguageServiceHost { let dirname = filename; let found = false; while (!found && dirname.indexOf(stopDirname) === 0) { - dirname = path.dirname(dirname); - let resolvedPath = path.resolve(dirname, ref.fileName); + dirname = path_1.default.dirname(dirname); + let resolvedPath = path_1.default.resolve(dirname, ref.fileName); if (resolvedPath.endsWith('.js')) { resolvedPath = resolvedPath.slice(0, -3); } diff --git a/code/build/lib/tsb/builder.ts b/code/build/lib/tsb/builder.ts index 509284d0cdc..403d2cec932 100644 --- a/code/build/lib/tsb/builder.ts +++ b/code/build/lib/tsb/builder.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; import * as utils from './utils'; -import * as colors from 'ansi-colors'; -import * as ts from 'typescript'; -import * as Vinyl from 'vinyl'; +import colors from 'ansi-colors'; +import ts from 'typescript'; +import Vinyl from 'vinyl'; import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map'; export interface IConfiguration { diff --git a/code/build/lib/tsb/index.js b/code/build/lib/tsb/index.js index 204c06e80ac..843b76c823f 100644 --- a/code/build/lib/tsb/index.js +++ b/code/build/lib/tsb/index.js @@ -3,17 +3,53 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.create = create; -const Vinyl = require("vinyl"); -const through = require("through"); -const builder = require("./builder"); -const ts = require("typescript"); +const vinyl_1 = __importDefault(require("vinyl")); +const through_1 = __importDefault(require("through")); +const builder = __importStar(require("./builder")); +const typescript_1 = __importDefault(require("typescript")); const stream_1 = require("stream"); const path_1 = require("path"); const utils_1 = require("./utils"); const fs_1 = require("fs"); -const log = require("fancy-log"); +const fancy_log_1 = __importDefault(require("fancy-log")); const transpiler_1 = require("./transpiler"); const colors = require("ansi-colors"); class EmptyDuplex extends stream_1.Duplex { @@ -32,31 +68,31 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) onError(diag.message); } else if (!diag.file || !diag.start) { - onError(ts.flattenDiagnosticMessageText(diag.messageText, '\n')); + onError(typescript_1.default.flattenDiagnosticMessageText(diag.messageText, '\n')); } else { const lineAndCh = diag.file.getLineAndCharacterOfPosition(diag.start); - onError(utils_1.strings.format('{0}({1},{2}): {3}', diag.file.fileName, lineAndCh.line + 1, lineAndCh.character + 1, ts.flattenDiagnosticMessageText(diag.messageText, '\n'))); + onError(utils_1.strings.format('{0}({1},{2}): {3}', diag.file.fileName, lineAndCh.line + 1, lineAndCh.character + 1, typescript_1.default.flattenDiagnosticMessageText(diag.messageText, '\n'))); } } - const parsed = ts.readConfigFile(projectPath, ts.sys.readFile); + const parsed = typescript_1.default.readConfigFile(projectPath, typescript_1.default.sys.readFile); if (parsed.error) { printDiagnostic(parsed.error); return createNullCompiler(); } - const cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, (0, path_1.dirname)(projectPath), existingOptions); + const cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, (0, path_1.dirname)(projectPath), existingOptions); if (cmdLine.errors.length > 0) { cmdLine.errors.forEach(printDiagnostic); return createNullCompiler(); } function logFn(topic, message) { if (config.verbose) { - log(colors.cyan(topic), message); + (0, fancy_log_1.default)(colors.cyan(topic), message); } } // FULL COMPILE stream doing transpile, syntax and semantic diagnostics function createCompileStream(builder, token) { - return through(function (file) { + return (0, through_1.default)(function (file) { // give the file to the compiler if (file.isStream()) { this.emit('error', 'no support for streams'); @@ -70,7 +106,7 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) } // TRANSPILE ONLY stream doing just TS to JS conversion function createTranspileStream(transpiler) { - return through(function (file) { + return (0, through_1.default)(function (file) { // give the file to the compiler if (file.isStream()) { this.emit('error', 'no support for streams'); @@ -116,7 +152,7 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) let path; for (; more && _pos < _fileNames.length; _pos++) { path = _fileNames[_pos]; - more = this.push(new Vinyl({ + more = this.push(new vinyl_1.default({ path, contents: (0, fs_1.readFileSync)(path), stat: (0, fs_1.statSync)(path), diff --git a/code/build/lib/tsb/index.ts b/code/build/lib/tsb/index.ts index 53c752d2655..e577d386cd9 100644 --- a/code/build/lib/tsb/index.ts +++ b/code/build/lib/tsb/index.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as Vinyl from 'vinyl'; -import * as through from 'through'; +import Vinyl from 'vinyl'; +import through from 'through'; import * as builder from './builder'; -import * as ts from 'typescript'; +import ts from 'typescript'; import { Readable, Writable, Duplex } from 'stream'; import { dirname } from 'path'; import { strings } from './utils'; import { readFileSync, statSync } from 'fs'; -import * as log from 'fancy-log'; +import log from 'fancy-log'; import { ESBuildTranspiler, ITranspiler, TscTranspiler } from './transpiler'; import colors = require('ansi-colors'); diff --git a/code/build/lib/tsb/transpiler.js b/code/build/lib/tsb/transpiler.js index a4439b8d7ae..adccb104416 100644 --- a/code/build/lib/tsb/transpiler.js +++ b/code/build/lib/tsb/transpiler.js @@ -3,28 +3,31 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.ESBuildTranspiler = exports.TscTranspiler = void 0; -const esbuild = require("esbuild"); -const ts = require("typescript"); -const threads = require("node:worker_threads"); -const Vinyl = require("vinyl"); +const esbuild_1 = __importDefault(require("esbuild")); +const typescript_1 = __importDefault(require("typescript")); +const node_worker_threads_1 = __importDefault(require("node:worker_threads")); +const vinyl_1 = __importDefault(require("vinyl")); const node_os_1 = require("node:os"); function transpile(tsSrc, options) { const isAmd = /\n(import|export)/m.test(tsSrc); - if (!isAmd && options.compilerOptions?.module === ts.ModuleKind.AMD) { + if (!isAmd && options.compilerOptions?.module === typescript_1.default.ModuleKind.AMD) { // enforce NONE module-system for not-amd cases - options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: ts.ModuleKind.None } } }; + options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: typescript_1.default.ModuleKind.None } } }; } - const out = ts.transpileModule(tsSrc, options); + const out = typescript_1.default.transpileModule(tsSrc, options); return { jsSrc: out.outputText, diag: out.diagnostics ?? [] }; } -if (!threads.isMainThread) { +if (!node_worker_threads_1.default.isMainThread) { // WORKER - threads.parentPort?.addListener('message', (req) => { + node_worker_threads_1.default.parentPort?.addListener('message', (req) => { const res = { jsSrcs: [], diagnostics: [] @@ -34,7 +37,7 @@ if (!threads.isMainThread) { res.jsSrcs.push(out.jsSrc); res.diagnostics.push(out.diag); } - threads.parentPort.postMessage(res); + node_worker_threads_1.default.parentPort.postMessage(res); }); } class OutputFileNameOracle { @@ -43,7 +46,7 @@ class OutputFileNameOracle { this.getOutputFileName = (file) => { try { // windows: path-sep normalizing - file = ts.normalizePath(file); + file = typescript_1.default.normalizePath(file); if (!cmdLine.options.configFilePath) { // this is needed for the INTERNAL getOutputFileNames-call below... cmdLine.options.configFilePath = configFilePath; @@ -53,7 +56,7 @@ class OutputFileNameOracle { file = file.slice(0, -5) + '.ts'; cmdLine.fileNames.push(file); } - const outfile = ts.getOutputFileNames(cmdLine, file, true)[0]; + const outfile = typescript_1.default.getOutputFileNames(cmdLine, file, true)[0]; if (isDts) { cmdLine.fileNames.pop(); } @@ -70,7 +73,7 @@ class OutputFileNameOracle { class TranspileWorker { static pool = 1; id = TranspileWorker.pool++; - _worker = new threads.Worker(__filename); + _worker = new node_worker_threads_1.default.Worker(__filename); _pending; _durations = []; constructor(outFileFn) { @@ -107,7 +110,7 @@ class TranspileWorker { } const outBase = options.compilerOptions?.outDir ?? file.base; const outPath = outFileFn(file.path); - outFiles.push(new Vinyl({ + outFiles.push(new vinyl_1.default({ path: outPath, base: outBase, contents: Buffer.from(jsSrc), @@ -249,7 +252,7 @@ class ESBuildTranspiler { compilerOptions: { ...this._cmdLine.options, ...{ - module: isExtension ? ts.ModuleKind.CommonJS : undefined + module: isExtension ? typescript_1.default.ModuleKind.CommonJS : undefined } } }), @@ -270,7 +273,7 @@ class ESBuildTranspiler { throw Error('file.contents must be a Buffer'); } const t1 = Date.now(); - this._jobs.push(esbuild.transform(file.contents, { + this._jobs.push(esbuild_1.default.transform(file.contents, { ...this._transformOpts, sourcefile: file.path, }).then(result => { @@ -281,7 +284,7 @@ class ESBuildTranspiler { } const outBase = this._cmdLine.options.outDir ?? file.base; const outPath = this._outputFileNames.getOutputFileName(file.path); - this.onOutfile(new Vinyl({ + this.onOutfile(new vinyl_1.default({ path: outPath, base: outBase, contents: Buffer.from(result.code), diff --git a/code/build/lib/tsb/transpiler.ts b/code/build/lib/tsb/transpiler.ts index ae841dcf88b..16a3b347538 100644 --- a/code/build/lib/tsb/transpiler.ts +++ b/code/build/lib/tsb/transpiler.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as esbuild from 'esbuild'; -import * as ts from 'typescript'; -import * as threads from 'node:worker_threads'; -import * as Vinyl from 'vinyl'; +import esbuild from 'esbuild'; +import ts from 'typescript'; +import threads from 'node:worker_threads'; +import Vinyl from 'vinyl'; import { cpus } from 'node:os'; interface TranspileReq { diff --git a/code/build/lib/typings/event-stream.d.ts b/code/build/lib/typings/event-stream.d.ts index 260051be52e..2b021ef258e 100644 --- a/code/build/lib/typings/event-stream.d.ts +++ b/code/build/lib/typings/event-stream.d.ts @@ -1,7 +1,7 @@ declare module "event-stream" { import { Stream } from 'stream'; import { ThroughStream as _ThroughStream } from 'through'; - import * as File from 'vinyl'; + import File from 'vinyl'; export interface ThroughStream extends _ThroughStream { queue(data: File | null): any; diff --git a/code/build/lib/util.js b/code/build/lib/util.js index 82e4189dd1a..8b6f0396281 100644 --- a/code/build/lib/util.js +++ b/code/build/lib/util.js @@ -3,6 +3,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.incremental = incremental; exports.debounce = debounce; @@ -23,20 +26,20 @@ exports.rebase = rebase; exports.filter = filter; exports.streamToPromise = streamToPromise; exports.getElectronVersion = getElectronVersion; -const es = require("event-stream"); -const _debounce = require("debounce"); -const _filter = require("gulp-filter"); -const rename = require("gulp-rename"); -const path = require("path"); -const fs = require("fs"); -const _rimraf = require("rimraf"); +const event_stream_1 = __importDefault(require("event-stream")); +const debounce_1 = __importDefault(require("debounce")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const gulp_rename_1 = __importDefault(require("gulp-rename")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const rimraf_1 = __importDefault(require("rimraf")); const url_1 = require("url"); -const ternaryStream = require("ternary-stream"); -const root = path.dirname(path.dirname(__dirname)); +const ternary_stream_1 = __importDefault(require("ternary-stream")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const NoCancellationToken = { isCancellationRequested: () => false }; function incremental(streamProvider, initial, supportsCancellation) { - const input = es.through(); - const output = es.through(); + const input = event_stream_1.default.through(); + const output = event_stream_1.default.through(); let state = 'idle'; let buffer = Object.create(null); const token = !supportsCancellation ? undefined : { isCancellationRequested: () => Object.keys(buffer).length > 0 }; @@ -45,7 +48,7 @@ function incremental(streamProvider, initial, supportsCancellation) { const stream = !supportsCancellation ? streamProvider() : streamProvider(isCancellable ? token : NoCancellationToken); input .pipe(stream) - .pipe(es.through(undefined, () => { + .pipe(event_stream_1.default.through(undefined, () => { state = 'idle'; eventuallyRun(); })) @@ -54,14 +57,14 @@ function incremental(streamProvider, initial, supportsCancellation) { if (initial) { run(initial, false); } - const eventuallyRun = _debounce(() => { + const eventuallyRun = (0, debounce_1.default)(() => { const paths = Object.keys(buffer); if (paths.length === 0) { return; } const data = paths.map(path => buffer[path]); buffer = Object.create(null); - run(es.readArray(data), true); + run(event_stream_1.default.readArray(data), true); }, 500); input.on('data', (f) => { buffer[f.path] = f; @@ -69,16 +72,16 @@ function incremental(streamProvider, initial, supportsCancellation) { eventuallyRun(); } }); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function debounce(task, duration = 500) { - const input = es.through(); - const output = es.through(); + const input = event_stream_1.default.through(); + const output = event_stream_1.default.through(); let state = 'idle'; const run = () => { state = 'running'; task() - .pipe(es.through(undefined, () => { + .pipe(event_stream_1.default.through(undefined, () => { const shouldRunAgain = state === 'stale'; state = 'idle'; if (shouldRunAgain) { @@ -88,7 +91,7 @@ function debounce(task, duration = 500) { .pipe(output); }; run(); - const eventuallyRun = _debounce(() => run(), duration); + const eventuallyRun = (0, debounce_1.default)(() => run(), duration); input.on('data', () => { if (state === 'idle') { eventuallyRun(); @@ -97,13 +100,13 @@ function debounce(task, duration = 500) { state = 'stale'; } }); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function fixWin32DirectoryPermissions() { if (!/win32/.test(process.platform)) { - return es.through(); + return event_stream_1.default.through(); } - return es.mapSync(f => { + return event_stream_1.default.mapSync(f => { if (f.stat && f.stat.isDirectory && f.stat.isDirectory()) { f.stat.mode = 16877; } @@ -111,7 +114,7 @@ function fixWin32DirectoryPermissions() { }); } function setExecutableBit(pattern) { - const setBit = es.mapSync(f => { + const setBit = event_stream_1.default.mapSync(f => { if (!f.stat) { f.stat = { isFile() { return true; } }; } @@ -121,13 +124,13 @@ function setExecutableBit(pattern) { if (!pattern) { return setBit; } - const input = es.through(); - const filter = _filter(pattern, { restore: true }); + const input = event_stream_1.default.through(); + const filter = (0, gulp_filter_1.default)(pattern, { restore: true }); const output = input .pipe(filter) .pipe(setBit) .pipe(filter.restore); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function toFileUri(filePath) { const match = filePath.match(/^([a-z])\:(.*)$/i); @@ -137,27 +140,27 @@ function toFileUri(filePath) { return 'file://' + filePath.replace(/\\/g, '/'); } function skipDirectories() { - return es.mapSync(f => { + return event_stream_1.default.mapSync(f => { if (!f.isDirectory()) { return f; } }); } function cleanNodeModules(rulePath) { - const rules = fs.readFileSync(rulePath, 'utf8') + const rules = fs_1.default.readFileSync(rulePath, 'utf8') .split(/\r?\n/g) .map(line => line.trim()) .filter(line => line && !/^#/.test(line)); const excludes = rules.filter(line => !/^!/.test(line)).map(line => `!**/node_modules/${line}`); const includes = rules.filter(line => /^!/.test(line)).map(line => `**/node_modules/${line.substr(1)}`); - const input = es.through(); - const output = es.merge(input.pipe(_filter(['**', ...excludes])), input.pipe(_filter(includes))); - return es.duplex(input, output); + const input = event_stream_1.default.through(); + const output = event_stream_1.default.merge(input.pipe((0, gulp_filter_1.default)(['**', ...excludes])), input.pipe((0, gulp_filter_1.default)(includes))); + return event_stream_1.default.duplex(input, output); } function loadSourcemaps() { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.map((f, cb) => { + .pipe(event_stream_1.default.map((f, cb) => { if (f.sourceMap) { cb(undefined, f); return; @@ -185,7 +188,7 @@ function loadSourcemaps() { return; } f.contents = Buffer.from(contents.replace(/\/\/# sourceMappingURL=(.*)$/g, ''), 'utf8'); - fs.readFile(path.join(path.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => { + fs_1.default.readFile(path_1.default.join(path_1.default.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => { if (err) { return cb(err); } @@ -193,54 +196,54 @@ function loadSourcemaps() { cb(undefined, f); }); })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function stripSourceMappingURL() { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.mapSync(f => { + .pipe(event_stream_1.default.mapSync(f => { const contents = f.contents.toString('utf8'); f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); return f; })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } /** Splits items in the stream based on the predicate, sending them to onTrue if true, or onFalse otherwise */ -function $if(test, onTrue, onFalse = es.through()) { +function $if(test, onTrue, onFalse = event_stream_1.default.through()) { if (typeof test === 'boolean') { return test ? onTrue : onFalse; } - return ternaryStream(test, onTrue, onFalse); + return (0, ternary_stream_1.default)(test, onTrue, onFalse); } /** Operator that appends the js files' original path a sourceURL, so debug locations map */ function appendOwnPathSourceURL() { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.mapSync(f => { + .pipe(event_stream_1.default.mapSync(f => { if (!(f.contents instanceof Buffer)) { throw new Error(`contents of ${f.path} are not a buffer`); } f.contents = Buffer.concat([f.contents, Buffer.from(`\n//# sourceURL=${(0, url_1.pathToFileURL)(f.path)}`)]); return f; })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function rewriteSourceMappingURL(sourceMappingURLBase) { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.mapSync(f => { + .pipe(event_stream_1.default.mapSync(f => { const contents = f.contents.toString('utf8'); - const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path.dirname(f.relative).replace(/\\/g, '/')}/$1`; + const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path_1.default.dirname(f.relative).replace(/\\/g, '/')}/$1`; f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, str)); return f; })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function rimraf(dir) { const result = () => new Promise((c, e) => { let retries = 0; const retry = () => { - _rimraf(dir, { maxBusyTries: 1 }, (err) => { + (0, rimraf_1.default)(dir, { maxBusyTries: 1 }, (err) => { if (!err) { return c(); } @@ -252,14 +255,14 @@ function rimraf(dir) { }; retry(); }); - result.taskName = `clean-${path.basename(dir).toLowerCase()}`; + result.taskName = `clean-${path_1.default.basename(dir).toLowerCase()}`; return result; } function _rreaddir(dirPath, prepend, result) { - const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { - _rreaddir(path.join(dirPath, entry.name), `${prepend}/${entry.name}`, result); + _rreaddir(path_1.default.join(dirPath, entry.name), `${prepend}/${entry.name}`, result); } else { result.push(`${prepend}/${entry.name}`); @@ -272,20 +275,20 @@ function rreddir(dirPath) { return result; } function ensureDir(dirPath) { - if (fs.existsSync(dirPath)) { + if (fs_1.default.existsSync(dirPath)) { return; } - ensureDir(path.dirname(dirPath)); - fs.mkdirSync(dirPath); + ensureDir(path_1.default.dirname(dirPath)); + fs_1.default.mkdirSync(dirPath); } function rebase(count) { - return rename(f => { + return (0, gulp_rename_1.default)(f => { const parts = f.dirname ? f.dirname.split(/[\/\\]/) : []; - f.dirname = parts.slice(count).join(path.sep); + f.dirname = parts.slice(count).join(path_1.default.sep); }); } function filter(fn) { - const result = es.through(function (data) { + const result = event_stream_1.default.through(function (data) { if (fn(data)) { this.emit('data', data); } @@ -293,7 +296,7 @@ function filter(fn) { result.restore.push(data); } }); - result.restore = es.through(); + result.restore = event_stream_1.default.through(); return result; } function streamToPromise(stream) { @@ -303,7 +306,7 @@ function streamToPromise(stream) { }); } function getElectronVersion() { - const npmrc = fs.readFileSync(path.join(root, '.npmrc'), 'utf8'); + const npmrc = fs_1.default.readFileSync(path_1.default.join(root, '.npmrc'), 'utf8'); const electronVersion = /^target="(.*)"$/m.exec(npmrc)[1]; const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; return { electronVersion, msBuildId }; diff --git a/code/build/lib/util.ts b/code/build/lib/util.ts index 08921834676..ad81730b3de 100644 --- a/code/build/lib/util.ts +++ b/code/build/lib/util.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import _debounce = require('debounce'); -import * as _filter from 'gulp-filter'; -import * as rename from 'gulp-rename'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as _rimraf from 'rimraf'; -import * as VinylFile from 'vinyl'; +import es from 'event-stream'; +import _debounce from 'debounce'; +import _filter from 'gulp-filter'; +import rename from 'gulp-rename'; +import path from 'path'; +import fs from 'fs'; +import _rimraf from 'rimraf'; +import VinylFile from 'vinyl'; import { ThroughStream } from 'through'; -import * as sm from 'source-map'; +import sm from 'source-map'; import { pathToFileURL } from 'url'; -import * as ternaryStream from 'ternary-stream'; +import ternaryStream from 'ternary-stream'; const root = path.dirname(path.dirname(__dirname)); diff --git a/code/build/lib/watch/index.js b/code/build/lib/watch/index.js index 86d2611febf..69eca78fd70 100644 --- a/code/build/lib/watch/index.js +++ b/code/build/lib/watch/index.js @@ -3,6 +3,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); module.exports = function () { return watch.apply(null, arguments); diff --git a/code/build/lib/watch/watch-win32.js b/code/build/lib/watch/watch-win32.js index 934d8e8110f..7b77981d620 100644 --- a/code/build/lib/watch/watch-win32.js +++ b/code/build/lib/watch/watch-win32.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const cp = require("child_process"); -const fs = require("fs"); -const File = require("vinyl"); -const es = require("event-stream"); -const filter = require("gulp-filter"); -const watcherPath = path.join(__dirname, 'watcher.exe'); +const path_1 = __importDefault(require("path")); +const child_process_1 = __importDefault(require("child_process")); +const fs_1 = __importDefault(require("fs")); +const vinyl_1 = __importDefault(require("vinyl")); +const event_stream_1 = __importDefault(require("event-stream")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const watcherPath = path_1.default.join(__dirname, 'watcher.exe'); function toChangeType(type) { switch (type) { case '0': return 'change'; @@ -19,8 +22,8 @@ function toChangeType(type) { } } function watch(root) { - const result = es.through(); - let child = cp.spawn(watcherPath, [root]); + const result = event_stream_1.default.through(); + let child = child_process_1.default.spawn(watcherPath, [root]); child.stdout.on('data', function (data) { const lines = data.toString('utf8').split('\n'); for (let i = 0; i < lines.length; i++) { @@ -34,8 +37,8 @@ function watch(root) { if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { continue; } - const changePathFull = path.join(root, changePath); - const file = new File({ + const changePathFull = path_1.default.join(root, changePath); + const file = new vinyl_1.default({ path: changePathFull, base: root }); @@ -60,20 +63,20 @@ function watch(root) { const cache = Object.create(null); module.exports = function (pattern, options) { options = options || {}; - const cwd = path.normalize(options.cwd || process.cwd()); + const cwd = path_1.default.normalize(options.cwd || process.cwd()); let watcher = cache[cwd]; if (!watcher) { watcher = cache[cwd] = watch(cwd); } - const rebase = !options.base ? es.through() : es.mapSync(function (f) { + const rebase = !options.base ? event_stream_1.default.through() : event_stream_1.default.mapSync(function (f) { f.base = options.base; return f; }); return watcher - .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git - .pipe(filter(pattern, { dot: options.dot })) - .pipe(es.map(function (file, cb) { - fs.stat(file.path, function (err, stat) { + .pipe((0, gulp_filter_1.default)(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe((0, gulp_filter_1.default)(pattern, { dot: options.dot })) + .pipe(event_stream_1.default.map(function (file, cb) { + fs_1.default.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } @@ -83,7 +86,7 @@ module.exports = function (pattern, options) { if (!stat.isFile()) { return cb(); } - fs.readFile(file.path, function (err, contents) { + fs_1.default.readFile(file.path, function (err, contents) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } diff --git a/code/build/lib/watch/watch-win32.ts b/code/build/lib/watch/watch-win32.ts index afde6a79f22..bbfde6afba9 100644 --- a/code/build/lib/watch/watch-win32.ts +++ b/code/build/lib/watch/watch-win32.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as cp from 'child_process'; -import * as fs from 'fs'; -import * as File from 'vinyl'; -import * as es from 'event-stream'; -import * as filter from 'gulp-filter'; +import path from 'path'; +import cp from 'child_process'; +import fs from 'fs'; +import File from 'vinyl'; +import es from 'event-stream'; +import filter from 'gulp-filter'; import { Stream } from 'stream'; const watcherPath = path.join(__dirname, 'watcher.exe'); diff --git a/code/build/linux/debian/calculate-deps.js b/code/build/linux/debian/calculate-deps.js index bbcb6bfc3de..34276ce7705 100644 --- a/code/build/linux/debian/calculate-deps.js +++ b/code/build/linux/debian/calculate-deps.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.generatePackageDeps = generatePackageDeps; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const os_1 = require("os"); -const path = require("path"); -const manifests = require("../../../cgmanifest.json"); +const path_1 = __importDefault(require("path")); +const cgmanifest_json_1 = __importDefault(require("../../../cgmanifest.json")); const dep_lists_1 = require("./dep-lists"); function generatePackageDeps(files, arch, chromiumSysroot, vscodeSysroot) { const dependencies = files.map(file => calculatePackageDeps(file, arch, chromiumSysroot, vscodeSysroot)); @@ -29,7 +32,7 @@ function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) console.error('Tried to stat ' + binaryPath + ' but failed.'); } // Get the Chromium dpkg-shlibdeps file. - const chromiumManifest = manifests.registrations.filter(registration => { + const chromiumManifest = cgmanifest_json_1.default.registrations.filter(registration => { return registration.component.type === 'git' && registration.component.git.name === 'chromium'; }); const dpkgShlibdepsUrl = `https://raw.githubusercontent.com/chromium/chromium/${chromiumManifest[0].version}/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl`; @@ -52,7 +55,7 @@ function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) } cmd.push(`-l${chromiumSysroot}/usr/lib`); cmd.push(`-L${vscodeSysroot}/debian/libxkbfile1/DEBIAN/shlibs`); - cmd.push('-O', '-e', path.resolve(binaryPath)); + cmd.push('-O', '-e', path_1.default.resolve(binaryPath)); const dpkgShlibdepsResult = (0, child_process_1.spawnSync)('perl', cmd, { cwd: chromiumSysroot }); if (dpkgShlibdepsResult.status !== 0) { throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `); diff --git a/code/build/linux/debian/calculate-deps.ts b/code/build/linux/debian/calculate-deps.ts index 92f8065f262..addc38696a8 100644 --- a/code/build/linux/debian/calculate-deps.ts +++ b/code/build/linux/debian/calculate-deps.ts @@ -6,8 +6,8 @@ import { spawnSync } from 'child_process'; import { constants, statSync } from 'fs'; import { tmpdir } from 'os'; -import path = require('path'); -import * as manifests from '../../../cgmanifest.json'; +import path from 'path'; +import manifests from '../../../cgmanifest.json'; import { additionalDeps } from './dep-lists'; import { DebianArchString } from './types'; diff --git a/code/build/linux/debian/install-sysroot.js b/code/build/linux/debian/install-sysroot.js index 354c67a2909..16d8d01468f 100644 --- a/code/build/linux/debian/install-sysroot.js +++ b/code/build/linux/debian/install-sysroot.js @@ -3,20 +3,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getVSCodeSysroot = getVSCodeSysroot; exports.getChromiumSysroot = getChromiumSysroot; const child_process_1 = require("child_process"); const os_1 = require("os"); -const fs = require("fs"); -const https = require("https"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const https_1 = __importDefault(require("https")); +const path_1 = __importDefault(require("path")); const crypto_1 = require("crypto"); -const ansiColors = require("ansi-colors"); +const ansi_colors_1 = __importDefault(require("ansi-colors")); // Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; const URL_PATH = 'sysroots/toolchain'; -const REPO_ROOT = path.dirname(path.dirname(path.dirname(__dirname))); +const REPO_ROOT = path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(__dirname))); const ghApiHeaders = { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'VSCode Build', @@ -29,7 +32,7 @@ const ghDownloadHeaders = { Accept: 'application/octet-stream', }; function getElectronVersion() { - const npmrc = fs.readFileSync(path.join(REPO_ROOT, '.npmrc'), 'utf8'); + const npmrc = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.npmrc'), 'utf8'); const electronVersion = /^target="(.*)"$/m.exec(npmrc)[1]; const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; return { electronVersion, msBuildId }; @@ -37,11 +40,11 @@ function getElectronVersion() { function getSha(filename) { const hash = (0, crypto_1.createHash)('sha256'); // Read file 1 MB at a time - const fd = fs.openSync(filename, 'r'); + const fd = fs_1.default.openSync(filename, 'r'); const buffer = Buffer.alloc(1024 * 1024); let position = 0; let bytesRead = 0; - while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { + while ((bytesRead = fs_1.default.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { hash.update(buffer); position += bytesRead; } @@ -49,7 +52,7 @@ function getSha(filename) { return hash.digest('hex'); } function getVSCodeSysrootChecksum(expectedName) { - const checksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'vscode-sysroot.txt'), 'utf8'); + const checksums = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, 'build', 'checksums', 'vscode-sysroot.txt'), 'utf8'); for (const line of checksums.split('\n')) { const [checksum, name] = line.split(/\s+/); if (name === expectedName) { @@ -86,22 +89,22 @@ async function fetchUrl(options, retries = 10, retryDelay = 1000) { }); if (assetResponse.ok && (assetResponse.status >= 200 && assetResponse.status < 300)) { const assetContents = Buffer.from(await assetResponse.arrayBuffer()); - console.log(`Fetched response body buffer: ${ansiColors.magenta(`${assetContents.byteLength} bytes`)}`); + console.log(`Fetched response body buffer: ${ansi_colors_1.default.magenta(`${assetContents.byteLength} bytes`)}`); if (options.checksumSha256) { const actualSHA256Checksum = (0, crypto_1.createHash)('sha256').update(assetContents).digest('hex'); if (actualSHA256Checksum !== options.checksumSha256) { - throw new Error(`Checksum mismatch for ${ansiColors.cyan(asset.url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); + throw new Error(`Checksum mismatch for ${ansi_colors_1.default.cyan(asset.url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); } } - console.log(`Verified SHA256 checksums match for ${ansiColors.cyan(asset.url)}`); + console.log(`Verified SHA256 checksums match for ${ansi_colors_1.default.cyan(asset.url)}`); const tarCommand = `tar -xz -C ${options.dest}`; (0, child_process_1.execSync)(tarCommand, { input: assetContents }); console.log(`Fetch complete!`); return; } - throw new Error(`Request ${ansiColors.magenta(asset.url)} failed with status code: ${assetResponse.status}`); + throw new Error(`Request ${ansi_colors_1.default.magenta(asset.url)} failed with status code: ${assetResponse.status}`); } - throw new Error(`Request ${ansiColors.magenta('https://api.github.com')} failed with status code: ${response.status}`); + throw new Error(`Request ${ansi_colors_1.default.magenta('https://api.github.com')} failed with status code: ${response.status}`); } finally { clearTimeout(timeout); @@ -139,21 +142,21 @@ async function getVSCodeSysroot(arch) { if (!checksumSha256) { throw new Error(`Could not find checksum for ${expectedName}`); } - const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path.join((0, os_1.tmpdir)(), `vscode-${arch}-sysroot`); - const stamp = path.join(sysroot, '.stamp'); + const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path_1.default.join((0, os_1.tmpdir)(), `vscode-${arch}-sysroot`); + const stamp = path_1.default.join(sysroot, '.stamp'); const result = `${sysroot}/${triple}/${triple}/sysroot`; - if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedName) { + if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === expectedName) { return result; } console.log(`Installing ${arch} root image: ${sysroot}`); - fs.rmSync(sysroot, { recursive: true, force: true }); - fs.mkdirSync(sysroot); + fs_1.default.rmSync(sysroot, { recursive: true, force: true }); + fs_1.default.mkdirSync(sysroot); await fetchUrl({ checksumSha256, assetName: expectedName, dest: sysroot }); - fs.writeFileSync(stamp, expectedName); + fs_1.default.writeFileSync(stamp, expectedName); return result; } async function getChromiumSysroot(arch) { @@ -168,24 +171,24 @@ async function getChromiumSysroot(arch) { const sysrootDict = sysrootInfo[sysrootArch]; const tarballFilename = sysrootDict['Tarball']; const tarballSha = sysrootDict['Sha256Sum']; - const sysroot = path.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']); + const sysroot = path_1.default.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']); const url = [URL_PREFIX, URL_PATH, tarballSha].join('/'); - const stamp = path.join(sysroot, '.stamp'); - if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === url) { + const stamp = path_1.default.join(sysroot, '.stamp'); + if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === url) { return sysroot; } console.log(`Installing Debian ${arch} root image: ${sysroot}`); - fs.rmSync(sysroot, { recursive: true, force: true }); - fs.mkdirSync(sysroot); - const tarball = path.join(sysroot, tarballFilename); + fs_1.default.rmSync(sysroot, { recursive: true, force: true }); + fs_1.default.mkdirSync(sysroot); + const tarball = path_1.default.join(sysroot, tarballFilename); console.log(`Downloading ${url}`); let downloadSuccess = false; for (let i = 0; i < 3 && !downloadSuccess; i++) { - fs.writeFileSync(tarball, ''); + fs_1.default.writeFileSync(tarball, ''); await new Promise((c) => { - https.get(url, (res) => { + https_1.default.get(url, (res) => { res.on('data', (chunk) => { - fs.appendFileSync(tarball, chunk); + fs_1.default.appendFileSync(tarball, chunk); }); res.on('end', () => { downloadSuccess = true; @@ -198,7 +201,7 @@ async function getChromiumSysroot(arch) { }); } if (!downloadSuccess) { - fs.rmSync(tarball); + fs_1.default.rmSync(tarball); throw new Error('Failed to download ' + url); } const sha = getSha(tarball); @@ -209,8 +212,8 @@ async function getChromiumSysroot(arch) { if (proc.status) { throw new Error('Tarball extraction failed with code ' + proc.status); } - fs.rmSync(tarball); - fs.writeFileSync(stamp, url); + fs_1.default.rmSync(tarball); + fs_1.default.writeFileSync(stamp, url); return sysroot; } //# sourceMappingURL=install-sysroot.js.map \ No newline at end of file diff --git a/code/build/linux/debian/install-sysroot.ts b/code/build/linux/debian/install-sysroot.ts index 8ea43a523cf..aa10e39f95f 100644 --- a/code/build/linux/debian/install-sysroot.ts +++ b/code/build/linux/debian/install-sysroot.ts @@ -5,12 +5,12 @@ import { spawnSync, execSync } from 'child_process'; import { tmpdir } from 'os'; -import * as fs from 'fs'; -import * as https from 'https'; -import * as path from 'path'; +import fs from 'fs'; +import https from 'https'; +import path from 'path'; import { createHash } from 'crypto'; import { DebianArchString } from './types'; -import * as ansiColors from 'ansi-colors'; +import ansiColors from 'ansi-colors'; // Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; diff --git a/code/build/linux/dependencies-generator.js b/code/build/linux/dependencies-generator.js index 80b11b3d5b7..38649559873 100644 --- a/code/build/linux/dependencies-generator.js +++ b/code/build/linux/dependencies-generator.js @@ -3,10 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDependencies = getDependencies; const child_process_1 = require("child_process"); -const path = require("path"); +const path_1 = __importDefault(require("path")); const install_sysroot_1 = require("./debian/install-sysroot"); const calculate_deps_1 = require("./debian/calculate-deps"); const calculate_deps_2 = require("./rpm/calculate-deps"); @@ -44,23 +47,23 @@ async function getDependencies(packageType, buildDir, applicationName, arch) { } // Get the files for which we want to find dependencies. const canAsar = false; // TODO@esm ASAR disabled in ESM - const nativeModulesPath = path.join(buildDir, 'resources', 'app', canAsar ? 'node_modules.asar.unpacked' : 'node_modules'); + const nativeModulesPath = path_1.default.join(buildDir, 'resources', 'app', canAsar ? 'node_modules.asar.unpacked' : 'node_modules'); const findResult = (0, child_process_1.spawnSync)('find', [nativeModulesPath, '-name', '*.node']); if (findResult.status) { console.error('Error finding files:'); console.error(findResult.stderr.toString()); return []; } - const appPath = path.join(buildDir, applicationName); + const appPath = path_1.default.join(buildDir, applicationName); // Add the native modules const files = findResult.stdout.toString().trimEnd().split('\n'); // Add the tunnel binary. - files.push(path.join(buildDir, 'bin', product.tunnelApplicationName)); + files.push(path_1.default.join(buildDir, 'bin', product.tunnelApplicationName)); // Add the main executable. files.push(appPath); // Add chrome sandbox and crashpad handler. - files.push(path.join(buildDir, 'chrome-sandbox')); - files.push(path.join(buildDir, 'chrome_crashpad_handler')); + files.push(path_1.default.join(buildDir, 'chrome-sandbox')); + files.push(path_1.default.join(buildDir, 'chrome_crashpad_handler')); // Generate the dependencies. let dependencies; if (packageType === 'deb') { diff --git a/code/build/linux/dependencies-generator.ts b/code/build/linux/dependencies-generator.ts index 3163aee5450..46be92eb847 100644 --- a/code/build/linux/dependencies-generator.ts +++ b/code/build/linux/dependencies-generator.ts @@ -6,7 +6,7 @@ 'use strict'; import { spawnSync } from 'child_process'; -import path = require('path'); +import path from 'path'; import { getChromiumSysroot, getVSCodeSysroot } from './debian/install-sysroot'; import { generatePackageDeps as generatePackageDepsDebian } from './debian/calculate-deps'; import { generatePackageDeps as generatePackageDepsRpm } from './rpm/calculate-deps'; diff --git a/code/build/linux/libcxx-fetcher.js b/code/build/linux/libcxx-fetcher.js index cfdc9498502..d6c998e5aea 100644 --- a/code/build/linux/libcxx-fetcher.js +++ b/code/build/linux/libcxx-fetcher.js @@ -3,23 +3,26 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.downloadLibcxxHeaders = downloadLibcxxHeaders; exports.downloadLibcxxObjects = downloadLibcxxObjects; // Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. -const fs = require("fs"); -const path = require("path"); -const debug = require("debug"); -const extract = require("extract-zip"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const debug_1 = __importDefault(require("debug")); +const extract_zip_1 = __importDefault(require("extract-zip")); const get_1 = require("@electron/get"); -const root = path.dirname(path.dirname(__dirname)); -const d = debug('libcxx-fetcher'); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const d = (0, debug_1.default)('libcxx-fetcher'); async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { - if (await fs.existsSync(path.resolve(outDir, 'include'))) { + if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'include'))) { return; } - if (!await fs.existsSync(outDir)) { - await fs.mkdirSync(outDir, { recursive: true }); + if (!await fs_1.default.existsSync(outDir)) { + await fs_1.default.mkdirSync(outDir, { recursive: true }); } d(`downloading ${lib_name}_headers`); const headers = await (0, get_1.downloadArtifact)({ @@ -28,14 +31,14 @@ async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { artifactName: `${lib_name}_headers.zip`, }); d(`unpacking ${lib_name}_headers from ${headers}`); - await extract(headers, { dir: outDir }); + await (0, extract_zip_1.default)(headers, { dir: outDir }); } async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64') { - if (await fs.existsSync(path.resolve(outDir, 'libc++.a'))) { + if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'libc++.a'))) { return; } - if (!await fs.existsSync(outDir)) { - await fs.mkdirSync(outDir, { recursive: true }); + if (!await fs_1.default.existsSync(outDir)) { + await fs_1.default.mkdirSync(outDir, { recursive: true }); } d(`downloading libcxx-objects-linux-${targetArch}`); const objects = await (0, get_1.downloadArtifact)({ @@ -45,14 +48,14 @@ async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64' arch: targetArch, }); d(`unpacking libcxx-objects from ${objects}`); - await extract(objects, { dir: outDir }); + await (0, extract_zip_1.default)(objects, { dir: outDir }); } async function main() { const libcxxObjectsDirPath = process.env['VSCODE_LIBCXX_OBJECTS_DIR']; const libcxxHeadersDownloadDir = process.env['VSCODE_LIBCXX_HEADERS_DIR']; const libcxxabiHeadersDownloadDir = process.env['VSCODE_LIBCXXABI_HEADERS_DIR']; const arch = process.env['VSCODE_ARCH']; - const packageJSON = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + const packageJSON = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf8')); const electronVersion = packageJSON.devDependencies.electron; if (!libcxxObjectsDirPath || !libcxxHeadersDownloadDir || !libcxxabiHeadersDownloadDir) { throw new Error('Required build env not set'); diff --git a/code/build/linux/libcxx-fetcher.ts b/code/build/linux/libcxx-fetcher.ts index 6abb67faa76..6bdbd8a4f30 100644 --- a/code/build/linux/libcxx-fetcher.ts +++ b/code/build/linux/libcxx-fetcher.ts @@ -5,10 +5,10 @@ // Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. -import * as fs from 'fs'; -import * as path from 'path'; -import * as debug from 'debug'; -import * as extract from 'extract-zip'; +import fs from 'fs'; +import path from 'path'; +import debug from 'debug'; +import extract from 'extract-zip'; import { downloadArtifact } from '@electron/get'; const root = path.dirname(path.dirname(__dirname)); diff --git a/code/build/package-lock.json b/code/build/package-lock.json index d3ed72e5c75..801e8eb1ed6 100644 --- a/code/build/package-lock.json +++ b/code/build/package-lock.json @@ -42,6 +42,7 @@ "@types/workerpool": "^6.4.0", "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/vsce": "2.20.1", "byline": "^5.0.0", "debug": "^4.3.2", @@ -1274,6 +1275,19 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==", "dev": true }, + "node_modules/@vscode/ripgrep": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", + "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^7.0.2", + "proxy-from-env": "^1.1.0", + "yauzl": "^2.9.2" + } + }, "node_modules/@vscode/vsce": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.20.1.tgz", @@ -1877,6 +1891,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3631,6 +3646,13 @@ "node": ">=0.4.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/code/build/package.json b/code/build/package.json index c85b600983f..76caffbce89 100644 --- a/code/build/package.json +++ b/code/build/package.json @@ -36,6 +36,7 @@ "@types/workerpool": "^6.4.0", "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/vsce": "2.20.1", "byline": "^5.0.0", "debug": "^4.3.2", diff --git a/code/build/tsconfig.json b/code/build/tsconfig.json index ce7a493a7aa..f3ad981d62f 100644 --- a/code/build/tsconfig.json +++ b/code/build/tsconfig.json @@ -4,7 +4,7 @@ "lib": [ "ES2020" ], - "module": "commonjs", + "module": "nodenext", "alwaysStrict": true, "removeComments": false, "preserveConstEnums": true, diff --git a/code/build/win32/explorer-appx-fetcher.js b/code/build/win32/explorer-appx-fetcher.js index 554b449d872..78d2317147e 100644 --- a/code/build/win32/explorer-appx-fetcher.js +++ b/code/build/win32/explorer-appx-fetcher.js @@ -3,23 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.downloadExplorerAppx = downloadExplorerAppx; -const fs = require("fs"); -const debug = require("debug"); -const extract = require("extract-zip"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const debug_1 = __importDefault(require("debug")); +const extract_zip_1 = __importDefault(require("extract-zip")); +const path_1 = __importDefault(require("path")); const get_1 = require("@electron/get"); -const root = path.dirname(path.dirname(__dirname)); -const d = debug('explorer-appx-fetcher'); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const d = (0, debug_1.default)('explorer-appx-fetcher'); async function downloadExplorerAppx(outDir, quality = 'stable', targetArch = 'x64') { const fileNamePrefix = quality === 'insider' ? 'code_insiders' : 'code'; const fileName = `${fileNamePrefix}_explorer_${targetArch}.zip`; - if (await fs.existsSync(path.resolve(outDir, 'resources.pri'))) { + if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'resources.pri'))) { return; } - if (!await fs.existsSync(outDir)) { - await fs.mkdirSync(outDir, { recursive: true }); + if (!await fs_1.default.existsSync(outDir)) { + await fs_1.default.mkdirSync(outDir, { recursive: true }); } d(`downloading ${fileName}`); const artifact = await (0, get_1.downloadArtifact)({ @@ -34,14 +37,14 @@ async function downloadExplorerAppx(outDir, quality = 'stable', targetArch = 'x6 } }); d(`unpacking from ${fileName}`); - await extract(artifact, { dir: fs.realpathSync(outDir) }); + await (0, extract_zip_1.default)(artifact, { dir: fs_1.default.realpathSync(outDir) }); } async function main(outputDir) { const arch = process.env['VSCODE_ARCH']; if (!outputDir) { throw new Error('Required build env not set'); } - const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); + const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); await downloadExplorerAppx(outputDir, product.quality, arch); } if (require.main === module) { diff --git a/code/build/win32/explorer-appx-fetcher.ts b/code/build/win32/explorer-appx-fetcher.ts index 89fbb57c064..95121cd6503 100644 --- a/code/build/win32/explorer-appx-fetcher.ts +++ b/code/build/win32/explorer-appx-fetcher.ts @@ -5,10 +5,10 @@ 'use strict'; -import * as fs from 'fs'; -import * as debug from 'debug'; -import * as extract from 'extract-zip'; -import * as path from 'path'; +import fs from 'fs'; +import debug from 'debug'; +import extract from 'extract-zip'; +import path from 'path'; import { downloadArtifact } from '@electron/get'; const root = path.dirname(path.dirname(__dirname)); diff --git a/code/cli/ThirdPartyNotices.txt b/code/cli/ThirdPartyNotices.txt index 9f51f4c7be6..03ac5838391 100644 --- a/code/cli/ThirdPartyNotices.txt +++ b/code/cli/ThirdPartyNotices.txt @@ -1741,7 +1741,7 @@ SOFTWARE. deranged 0.3.11 - MIT OR Apache-2.0 https://github.com/jhpratt/deranged -Copyright (c) 2022 Jacob Pratt et al. +Copyright (c) 2024 Jacob Pratt et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1928,6 +1928,36 @@ SOFTWARE. --------------------------------------------------------- +displaydoc 0.2.5 - MIT OR Apache-2.0 +https://github.com/yaahc/displaydoc + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + encode_unicode 0.3.6 - MIT/Apache-2.0 https://github.com/tormol/encode_unicode @@ -3513,7 +3543,537 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -idna 0.5.0 - MIT OR Apache-2.0 +icu_collections 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_locid 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_locid_transform 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_locid_transform_data 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_normalizer 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_normalizer_data 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_properties 1.5.1 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_properties_data 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_provider 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_provider_macros 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +idna 1.0.3 - MIT OR Apache-2.0 https://github.com/servo/rust-url/ Copyright (c) 2013-2022 The rust-url developers @@ -3545,6 +4105,38 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +idna_adapter 1.2.0 - Apache-2.0 OR MIT +https://github.com/hsivonen/idna_adapter + +Copyright (c) The rust-url developers + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + indexmap 1.9.3 - Apache-2.0 OR MIT indexmap 2.2.6 - Apache-2.0 OR MIT https://github.com/indexmap-rs/indexmap @@ -4101,6 +4693,59 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +litemap 0.7.4 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + lock_api 0.4.12 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot @@ -8359,6 +9004,38 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +stable_deref_trait 1.2.0 - MIT/Apache-2.0 +https://github.com/storyyeller/stable_deref_trait + +Copyright (c) 2017 Robert Grosse + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + static_assertions 1.1.0 - MIT OR Apache-2.0 https://github.com/nvzqz/static-assertions-rs @@ -8667,14 +9344,30 @@ Apache License --------------------------------------------------------- -sysinfo 0.29.11 - MIT -https://github.com/GuillaumeGomez/sysinfo +synstructure 0.13.1 - MIT +https://github.com/mystor/synstructure The MIT License (MIT) -Copyright (c) 2015 Guillaume Gomez +Copyright 2016 Nika Layzell -Permission is hereby granted, free of charge, to any person obtaining a copy +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +sysinfo 0.29.11 - MIT +https://github.com/GuillaumeGomez/sysinfo + +The MIT License (MIT) + +Copyright (c) 2015 Guillaume Gomez + +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 @@ -8935,42 +9628,55 @@ SOFTWARE. --------------------------------------------------------- -tinyvec 1.6.0 - Zlib OR Apache-2.0 OR MIT -https://github.com/Lokathor/tinyvec +tinystr 0.7.6 - Unicode-3.0 +https://github.com/unicode-org/icu4x -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: +UNICODE LICENSE V3 -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +COPYRIGHT AND PERMISSION NOTICE -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. ---------------------------------------------------------- +Copyright © 2020-2024 Unicode, Inc. ---------------------------------------------------------- +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. -tinyvec_macros 0.1.1 - MIT OR Apache-2.0 OR Zlib -https://github.com/Soveu/tinyvec_macros +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. -MIT License +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. -Copyright (c) 2020 Soveu +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. -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: +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +SPDX-License-Identifier: Unicode-3.0 -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. +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. --------------------------------------------------------- --------------------------------------------------------- @@ -9458,10 +10164,8 @@ MIT License --------------------------------------------------------- -unicode-bidi 0.3.15 - MIT OR Apache-2.0 -https://github.com/servo/unicode-bidi - -Copyright (c) 2015 The Rust Project Developers +unicode-ident 1.0.12 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 +https://github.com/dtolnay/unicode-ident Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9490,8 +10194,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-ident 1.0.12 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 -https://github.com/dtolnay/unicode-ident +unicode-width 0.1.12 - MIT OR Apache-2.0 +https://github.com/unicode-rs/unicode-width + +Copyright (c) 2015 The Rust Project Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9520,8 +10226,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-normalization 0.1.23 - MIT/Apache-2.0 -https://github.com/unicode-rs/unicode-normalization +unicode-xid 0.2.4 - MIT OR Apache-2.0 +https://github.com/unicode-rs/unicode-xid Copyright (c) 2015 The Rust Project Developers @@ -9552,10 +10258,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-width 0.1.12 - MIT OR Apache-2.0 -https://github.com/unicode-rs/unicode-width +url 2.5.4 - MIT OR Apache-2.0 +https://github.com/servo/rust-url -Copyright (c) 2015 The Rust Project Developers +Copyright (c) 2013-2022 The rust-url developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9584,10 +10290,37 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-xid 0.2.4 - MIT OR Apache-2.0 -https://github.com/unicode-rs/unicode-xid +urlencoding 2.1.3 - MIT +https://github.com/kornelski/rust_urlencoding -Copyright (c) 2015 The Rust Project Developers +The MIT License (MIT) + +© 2016 Bertram Truong +© 2021 Kornel Lesiński + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +utf-8 0.7.6 - MIT OR Apache-2.0 +https://github.com/SimonSapin/rust-utf8 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9616,10 +10349,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -url 2.5.0 - MIT OR Apache-2.0 -https://github.com/servo/rust-url +utf16_iter 1.0.5 - Apache-2.0 OR MIT +https://github.com/hsivonen/utf16_iter -Copyright (c) 2013-2022 The rust-url developers +Copyright Mozilla Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9648,37 +10381,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -urlencoding 2.1.3 - MIT -https://github.com/kornelski/rust_urlencoding +utf8_iter 1.0.4 - Apache-2.0 OR MIT +https://github.com/hsivonen/utf8_iter -The MIT License (MIT) - -© 2016 Bertram Truong -© 2021 Kornel Lesiński - -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. ---------------------------------------------------------- - ---------------------------------------------------------- - -utf-8 0.7.6 - MIT OR Apache-2.0 -https://github.com/SimonSapin/rust-utf8 +Copyright Mozilla Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -10624,6 +11330,91 @@ THE SOFTWARE. --------------------------------------------------------- +write16 1.0.0 - Apache-2.0 OR MIT +https://github.com/hsivonen/write16 + +Copyright Mozilla Foundation + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +writeable 0.5.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + xattr 1.3.1 - MIT/Apache-2.0 https://github.com/Stebalien/xattr @@ -10702,10 +11493,142 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- +yoke 0.7.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +yoke-derive 0.7.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +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. --------------------------------------------------------- --------------------------------------------------------- @@ -10713,7 +11636,33 @@ LICENSE-MIT zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +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. --------------------------------------------------------- --------------------------------------------------------- @@ -10721,7 +11670,139 @@ LICENSE-MIT zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +zerofrom 0.1.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +zerofrom-derive 0.1.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. --------------------------------------------------------- --------------------------------------------------------- @@ -10780,6 +11861,112 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- +zerovec 0.10.4 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +zerovec-derive 0.10.3 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + zip 0.6.6 - MIT https://github.com/zip-rs/zip2 @@ -10814,7 +12001,33 @@ licences; see files named LICENSE.*.txt for details. zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +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. --------------------------------------------------------- --------------------------------------------------------- @@ -10822,7 +12035,33 @@ LICENSE-MIT zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +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. --------------------------------------------------------- --------------------------------------------------------- @@ -10830,5 +12069,31 @@ LICENSE-MIT zvariant_utils 1.0.1 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +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. --------------------------------------------------------- \ No newline at end of file diff --git a/code/extensions/git-base/src/api/api1.ts b/code/extensions/git-base/src/api/api1.ts index 74edc7f4452..005a7930356 100644 --- a/code/extensions/git-base/src/api/api1.ts +++ b/code/extensions/git-base/src/api/api1.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Disposable, commands } from 'vscode'; +import { Disposable, commands } from 'vscode'; import { Model } from '../model'; -import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource } from '../remoteSource'; +import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource'; import { GitBaseExtensionImpl } from './extension'; import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base'; @@ -21,10 +21,6 @@ export class ApiImpl implements API { return getRemoteSourceActions(this._model, url); } - getRemoteSourceControlHistoryItemCommands(url: string): Promise { - return getRemoteSourceControlHistoryItemCommands(this._model, url); - } - registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/code/extensions/git-base/src/api/git-base.d.ts b/code/extensions/git-base/src/api/git-base.d.ts index 37dd2c4229c..d4ec49df47d 100644 --- a/code/extensions/git-base/src/api/git-base.d.ts +++ b/code/extensions/git-base/src/api/git-base.d.ts @@ -9,7 +9,6 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -82,7 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/code/extensions/git-base/src/remoteSource.ts b/code/extensions/git-base/src/remoteSource.ts index 8d8d4ab102f..eb86b27367a 100644 --- a/code/extensions/git-base/src/remoteSource.ts +++ b/code/extensions/git-base/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable, Command } from 'vscode'; +import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable } from 'vscode'; import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base'; import { Model } from './model'; import { throttle, debounce } from './decorators'; @@ -123,20 +123,6 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise return remoteSourceActions; } -export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise { - const providers = model.getRemoteProviders(); - - const remoteSourceCommands = []; - for (const provider of providers) { - const providerCommands = await provider.getRemoteSourceControlHistoryItemCommands?.(url); - if (providerCommands?.length) { - remoteSourceCommands.push(...providerCommands); - } - } - - return remoteSourceCommands; -} - export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { diff --git a/code/extensions/git/package.json b/code/extensions/git/package.json index 66e8dd4981d..d23ef7b949c 100644 --- a/code/extensions/git/package.json +++ b/code/extensions/git/package.json @@ -32,6 +32,7 @@ "scmSelectedProvider", "scmTextDocument", "scmValidation", + "statusBarItemTooltip", "tabInputMultiDiff", "tabInputTextMerge", "textEditorDiffInformation", @@ -448,20 +449,20 @@ "enablement": "!operationInProgress" }, { - "command": "git.checkoutDetached", - "title": "%command.checkoutDetached%", + "command": "git.graph.checkout", + "title": "%command.graphCheckout%", "category": "Git", "enablement": "!operationInProgress" }, { - "command": "git.checkoutRef", - "title": "%command.checkoutRef%", + "command": "git.checkoutDetached", + "title": "%command.checkoutDetached%", "category": "Git", "enablement": "!operationInProgress" }, { - "command": "git.checkoutRefDetached", - "title": "%command.checkoutRefDetached%", + "command": "git.graph.checkoutDetached", + "title": "%command.graphCheckoutDetached%", "category": "Git", "enablement": "!operationInProgress" }, @@ -483,6 +484,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.graph.deleteBranch", + "title": "%command.graphDeleteBranch%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.renameBranch", "title": "%command.renameBranch%", @@ -519,6 +526,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.graph.deleteTag", + "title": "%command.graphDeleteTag%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.deleteRemoteTag", "title": "%command.deleteRemoteTag%", @@ -632,8 +645,8 @@ "enablement": "!operationInProgress" }, { - "command": "git.cherryPickRef", - "title": "%command.cherryPickRef%", + "command": "git.graph.cherryPick", + "title": "%command.graphCherryPick%", "category": "Git", "enablement": "!operationInProgress" }, @@ -921,6 +934,16 @@ "command": "git.copyCommitMessage", "title": "%command.timelineCopyCommitMessage%", "category": "Git" + }, + { + "command": "git.blame.toggleEditorDecoration", + "title": "%command.blameToggleEditorDecoration%", + "category": "Git" + }, + { + "command": "git.blame.toggleStatusBarItem", + "title": "%command.blameToggleStatusBarItem%", + "category": "Git" } ], "continueEditSession": [ @@ -1466,15 +1489,23 @@ "when": "false" }, { - "command": "git.checkoutRef", + "command": "git.graph.checkout", "when": "false" }, { - "command": "git.checkoutRefDetached", + "command": "git.graph.checkoutDetached", "when": "false" }, { - "command": "git.cherryPickRef", + "command": "git.graph.deleteBranch", + "when": "false" + }, + { + "command": "git.graph.deleteTag", + "when": "false" + }, + { + "command": "git.graph.cherryPick", "when": "false" }, { @@ -1997,24 +2028,24 @@ ], "scm/historyItem/context": [ { - "command": "git.createTag", + "command": "git.graph.checkoutDetached", "when": "scmProvider == git", - "group": "1_create@1" + "group": "1_checkout@2" }, { "command": "git.branch", "when": "scmProvider == git", - "group": "1_create@2" + "group": "2_branch@2" }, { - "command": "git.cherryPickRef", + "command": "git.createTag", "when": "scmProvider == git", - "group": "2_modify@1" + "group": "3_tag@1" }, { - "command": "git.checkoutRefDetached", + "command": "git.graph.cherryPick", "when": "scmProvider == git", - "group": "2_modify@2" + "group": "4_modify@1" }, { "command": "git.copyCommitId", @@ -2027,7 +2058,23 @@ "group": "9_copy@2" } ], - "scm/historyItemRef/context": [], + "scm/historyItemRef/context": [ + { + "command": "git.graph.checkout", + "when": "scmProvider == git", + "group": "1_checkout@1" + }, + { + "command": "git.graph.deleteBranch", + "when": "scmProvider == git && scmHistoryItemRef =~ /^refs\\/heads\\//", + "group": "2_branch@2" + }, + { + "command": "git.graph.deleteTag", + "when": "scmProvider == git && scmHistoryItemRef =~ /^refs\\/tags\\//", + "group": "3_tag@2" + } + ], "editor/title": [ { "command": "git.openFile", @@ -2039,16 +2086,6 @@ "group": "navigation", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInNotebookTextDiffEditor && resourceScheme =~ /^git$|^file$/" }, - { - "command": "git.stage", - "group": "navigation@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges" - }, - { - "command": "git.unstage", - "group": "navigation@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges" - }, { "command": "git.openChange", "group": "navigation@2", @@ -2065,30 +2102,50 @@ "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit" }, { - "command": "git.stageSelectedRanges", + "command": "git.stashApplyEditor", + "alt": "git.stashPopEditor", + "group": "navigation@1", + "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + }, + { + "command": "git.stashDropEditor", + "group": "navigation@2", + "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + }, + { + "command": "git.stage", + "group": "2_git@1", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges" + }, + { + "command": "git.unstage", + "group": "2_git@2", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges" + }, + { + "command": "git.stage", "group": "2_git@1", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { - "command": "git.unstageSelectedRanges", + "command": "git.stageSelectedRanges", "group": "2_git@2", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { - "command": "git.revertSelectedRanges", + "command": "git.unstage", "group": "2_git@3", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" }, { - "command": "git.stashApplyEditor", - "alt": "git.stashPopEditor", - "group": "navigation@1", - "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + "command": "git.unstageSelectedRanges", + "group": "2_git@4", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" }, { - "command": "git.stashDropEditor", - "group": "navigation@2", - "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + "command": "git.revertSelectedRanges", + "group": "2_git@5", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" } ], "editor/context": [ @@ -3211,7 +3268,7 @@ }, "git.blame.statusBarItem.enabled": { "type": "boolean", - "default": false, + "default": true, "markdownDescription": "%config.blameStatusBarItem.enabled%" }, "git.blame.statusBarItem.template": { diff --git a/code/extensions/git/package.nls.json b/code/extensions/git/package.nls.json index 0fb33c69b48..5cfb2b1a7b2 100644 --- a/code/extensions/git/package.nls.json +++ b/code/extensions/git/package.nls.json @@ -62,14 +62,11 @@ "command.undoCommit": "Undo Last Commit", "command.checkout": "Checkout to...", "command.checkoutDetached": "Checkout to (Detached)...", - "command.checkoutRef": "Checkout", - "command.checkoutRefDetached": "Checkout (Detached)", "command.branch": "Create Branch...", "command.branchFrom": "Create Branch From...", "command.deleteBranch": "Delete Branch...", "command.renameBranch": "Rename Branch...", "command.cherryPick": "Cherry Pick...", - "command.cherryPickRef": "Cherry Pick", "command.cherryPickAbort": "Abort Cherry Pick", "command.merge": "Merge...", "command.mergeAbort": "Abort Merge", @@ -122,10 +119,17 @@ "command.timelineCompareWithSelected": "Compare with Selected", "command.manageUnsafeRepositories": "Manage Unsafe Repositories", "command.openRepositoriesInParentFolders": "Open Repositories In Parent Folders", - "command.viewChanges": "View Changes", - "command.viewStagedChanges": "View Staged Changes", - "command.viewUntrackedChanges": "View Untracked Changes", - "command.viewCommit": "View Commit", + "command.viewChanges": "Open Changes", + "command.viewStagedChanges": "Open Staged Changes", + "command.viewUntrackedChanges": "Open Untracked Changes", + "command.viewCommit": "Open Commit", + "command.graphCheckout": "Checkout", + "command.graphCheckoutDetached": "Checkout (Detached)", + "command.graphCherryPick": "Cherry Pick", + "command.graphDeleteBranch": "Delete Branch", + "command.graphDeleteTag": "Delete Tag", + "command.blameToggleEditorDecoration": "Toggle Git Blame Editor Decoration", + "command.blameToggleStatusBarItem": "Toggle Git Blame Status Bar Item", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", diff --git a/code/extensions/git/src/api/api1.ts b/code/extensions/git/src/api/api1.ts index 518269c4162..d715c535a50 100644 --- a/code/extensions/git/src/api/api1.ts +++ b/code/extensions/git/src/api/api1.ts @@ -7,7 +7,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -414,6 +414,10 @@ export class ApiImpl implements API { return this.#model.registerPushErrorHandler(handler); } + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable { + return this.#model.registerSourceControlHistoryItemDetailsProvider(provider); + } + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable { return this.#model.registerBranchProtectionProvider(root, provider); } diff --git a/code/extensions/git/src/api/git.d.ts b/code/extensions/git/src/api/git.d.ts index ea78ac4d99a..5bbb4b03a9c 100644 --- a/code/extensions/git/src/api/git.d.ts +++ b/code/extensions/git/src/api/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -200,6 +200,7 @@ export interface Repository { readonly ui: RepositoryUIState; readonly onDidCommit: Event; + readonly onDidCheckout: Event; getConfigs(): Promise<{ key: string; value: string; }[]>; getConfig(key: string): Promise; @@ -326,6 +327,23 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface AvatarQueryCommit { + readonly hash: string; + readonly authorName?: string; + readonly authorEmail?: string; +} + +export interface AvatarQuery { + readonly commits: AvatarQueryCommit[]; + readonly size: number; +} + +export interface SourceControlHistoryItemDetailsProvider { + provideAvatar(repository: Repository, query: AvatarQuery): ProviderResult>; + provideHoverCommands(repository: Repository): ProviderResult; + provideMessageLinks(repository: Repository, message: string): ProviderResult; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -353,6 +371,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; } export interface GitExtension { diff --git a/code/extensions/git/src/blame.ts b/code/extensions/git/src/blame.ts index 110b4601b15..f29212c51a8 100644 --- a/code/extensions/git/src/blame.ts +++ b/code/extensions/git/src/blame.ts @@ -9,10 +9,14 @@ import { dispose, fromNow, getCommitShortHash, IDisposable } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation, Commit } from './git'; -import { fromGitUri, isGitUri } from './uri'; +import { fromGitUri, isGitUri, toGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; -import { getRemoteSourceControlHistoryItemCommands } from './remoteSource'; +import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { AvatarQuery, AvatarQueryCommit } from './api/git'; +import { LRUCache } from './cache'; + +const AVATAR_SIZE = 20; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -75,82 +79,34 @@ type BlameInformationTemplateTokens = { readonly authorDateAgo: string; }; -interface RepositoryBlameInformation { - /** - * Track the current HEAD of the repository so that we can clear cache entries - */ - HEAD: string; - - /** - * Outer map - maps resource scheme to resource blame information. Using the uri - * scheme as the key so that we can easily delete the cache entries for the "file" - * scheme as those entries are outdated when the HEAD of the repository changes. - * - * Inner map - maps commit + resource to blame information. - */ - readonly blameInformation: Map>; -} - interface LineBlameInformation { readonly lineNumber: number; readonly blameInformation: BlameInformation | string; } class GitBlameInformationCache { - private readonly _cache = new Map(); - - getRepositoryHEAD(repository: Repository): string | undefined { - return this._cache.get(repository)?.HEAD; - } - - setRepositoryHEAD(repository: Repository, commit: string): void { - const repositoryBlameInformation = this._cache.get(repository) ?? { - HEAD: commit, - blameInformation: new Map>() - } satisfies RepositoryBlameInformation; - - this._cache.set(repository, { - ...repositoryBlameInformation, - HEAD: commit - } satisfies RepositoryBlameInformation); - } + private readonly _cache = new Map>(); - deleteBlameInformation(repository: Repository, scheme?: string): boolean { - if (scheme === undefined) { - return this._cache.delete(repository); - } - - return this._cache.get(repository)?.blameInformation.delete(scheme) === true; + delete(repository: Repository): boolean { + return this._cache.delete(repository); } - getBlameInformation(repository: Repository, resource: Uri, commit: string): BlameInformation[] | undefined { - const blameInformationKey = this._getBlameInformationKey(resource, commit); - return this._cache.get(repository)?.blameInformation.get(resource.scheme)?.get(blameInformationKey); + get(repository: Repository, resource: Uri, commit: string): BlameInformation[] | undefined { + const key = this._getCacheKey(resource, commit); + return this._cache.get(repository)?.get(key); } - setBlameInformation(repository: Repository, resource: Uri, commit: string, blameInformation: BlameInformation[]): void { - if (!repository.HEAD?.commit) { - return; - } - + set(repository: Repository, resource: Uri, commit: string, blameInformation: BlameInformation[]): void { if (!this._cache.has(repository)) { - this._cache.set(repository, { - HEAD: repository.HEAD.commit, - blameInformation: new Map>() - } satisfies RepositoryBlameInformation); - } - - const repositoryBlameInformation = this._cache.get(repository)!; - if (!repositoryBlameInformation.blameInformation.has(resource.scheme)) { - repositoryBlameInformation.blameInformation.set(resource.scheme, new Map()); + this._cache.set(repository, new LRUCache(100)); } - const resourceSchemeBlameInformation = repositoryBlameInformation.blameInformation.get(resource.scheme)!; - resourceSchemeBlameInformation.set(this._getBlameInformationKey(resource, commit), blameInformation); + const key = this._getCacheKey(resource, commit); + this._cache.get(repository)!.set(key, blameInformation); } - private _getBlameInformationKey(resource: Uri, commit: string): string { - return `${commit}:${resource.toString()}`; + private _getCacheKey(resource: Uri, commit: string): string { + return toGitUri(resource, commit).toString(); } } @@ -169,6 +125,8 @@ export class GitBlameController { this._onDidChangeBlameInformation.fire(); } + private _HEAD: string | undefined; + private readonly _commitInformationCache = new LRUCache(100); private readonly _repositoryBlameCache = new GitBlameInformationCache(); private _editorDecoration: GitBlameEditorDecoration | undefined; @@ -203,24 +161,45 @@ export class GitBlameController { }); } - async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { + async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation): Promise { + const remoteHoverCommands: Command[] = []; + let commitAvatar: string | undefined; let commitInformation: Commit | undefined; - const remoteSourceCommands: Command[] = []; + let commitMessageWithLinks: string | undefined; const repository = this._model.getRepository(documentUri); if (repository) { - // Commit details - if (includeCommitDetails) { - try { + try { + // Commit details + commitInformation = this._commitInformationCache.get(blameInformation.hash); + if (!commitInformation) { commitInformation = await repository.getCommit(blameInformation.hash); - } catch { } - } + this._commitInformationCache.set(blameInformation.hash, commitInformation); + } - // Remote commands - const defaultRemote = repository.getDefaultRemote(); - if (defaultRemote?.fetchUrl) { - remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl)); + // Avatar + const avatarQuery = { + commits: [{ + hash: blameInformation.hash, + authorName: blameInformation.authorName, + authorEmail: blameInformation.authorEmail + } satisfies AvatarQueryCommit], + size: AVATAR_SIZE + } satisfies AvatarQuery; + + const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, avatarQuery); + commitAvatar = avatarResult?.get(blameInformation.hash); + } catch { } + + // Remote hover commands + const unpublishedCommits = await repository.getUnpublishedCommits(); + if (!unpublishedCommits.has(blameInformation.hash)) { + remoteHoverCommands.push(...await provideSourceControlHistoryItemHoverCommands(this._model, repository) ?? []); } + + // Message links + commitMessageWithLinks = await provideSourceControlHistoryItemMessageLinks( + this._model, repository, commitInformation?.message ?? blameInformation.subject ?? ''); } const markdownString = new MarkdownString(); @@ -229,16 +208,18 @@ export class GitBlameController { markdownString.supportThemeIcons = true; // Author, date + const hash = commitInformation?.hash ?? blameInformation.hash; const authorName = commitInformation?.authorName ?? blameInformation.authorName; const authorEmail = commitInformation?.authorEmail ?? blameInformation.authorEmail; const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate; + const avatar = commitAvatar ? `![${authorName}](${commitAvatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` : '$(account)'; if (authorName) { if (authorEmail) { const emailTitle = l10n.t('Email'); - markdownString.appendMarkdown(`$(account) [**${authorName}**](mailto:${authorEmail} "${emailTitle} ${authorName}")`); + markdownString.appendMarkdown(`${avatar} [**${authorName}**](mailto:${authorEmail} "${emailTitle} ${authorName}")`); } else { - markdownString.appendMarkdown(`$(account) **${authorName}**`); + markdownString.appendMarkdown(`${avatar} **${authorName}**`); } if (authorDate) { @@ -252,7 +233,7 @@ export class GitBlameController { } // Subject | Message - markdownString.appendMarkdown(`${emojify(commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); + markdownString.appendMarkdown(`${emojify(commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); markdownString.appendMarkdown(`---\n\n`); // Short stats @@ -277,17 +258,15 @@ export class GitBlameController { } // Commands - const hash = commitInformation?.hash ?? blameInformation.hash; - - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('Open Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); - // Remote commands - if (remoteSourceCommands.length > 0) { + // Remote hover commands + if (remoteHoverCommands.length > 0) { markdownString.appendMarkdown('  |  '); - const remoteCommandsMarkdown = remoteSourceCommands + const remoteCommandsMarkdown = remoteHoverCommands .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`); markdownString.appendMarkdown(remoteCommandsMarkdown.join(' ')); } @@ -363,23 +342,16 @@ export class GitBlameController { } this._repositoryDisposables.delete(repository); - this._repositoryBlameCache.deleteBlameInformation(repository); + this._repositoryBlameCache.delete(repository); } private _onDidRunGitStatus(repository: Repository): void { - const repositoryHEAD = this._repositoryBlameCache.getRepositoryHEAD(repository); - if (!repositoryHEAD || !repository.HEAD?.commit) { + if (!repository.HEAD?.commit || this._HEAD === repository.HEAD.commit) { return; } - // If the HEAD of the repository changed we can remove the cache - // entries for the "file" scheme as those entries are outdated. - if (repositoryHEAD !== repository.HEAD.commit) { - this._repositoryBlameCache.deleteBlameInformation(repository, 'file'); - this._repositoryBlameCache.setRepositoryHEAD(repository, repository.HEAD.commit); - - this._updateTextEditorBlameInformation(window.activeTextEditor); - } + this._HEAD = repository.HEAD.commit; + this._updateTextEditorBlameInformation(window.activeTextEditor); } private async _getBlameInformation(resource: Uri, commit: string): Promise { @@ -388,18 +360,18 @@ export class GitBlameController { return undefined; } - const resourceBlameInformation = this._repositoryBlameCache.getBlameInformation(repository, resource, commit); + const resourceBlameInformation = this._repositoryBlameCache.get(repository, resource, commit); if (resourceBlameInformation) { return resourceBlameInformation; } - // Ensure that the emojis are loaded. We will - // use them when formatting the blame information. + // Ensure that the emojis are loaded as we will need + // access to them when formatting the blame information. await ensureEmojis(); // Get blame information for the resource and cache it const blameInformation = await repository.blame2(resource.fsPath, commit) ?? []; - this._repositoryBlameCache.setBlameInformation(repository, resource, commit, blameInformation); + this._repositoryBlameCache.set(repository, resource, commit, blameInformation); return blameInformation; } @@ -590,7 +562,7 @@ class GitBlameEditorDecoration implements HoverProvider { return undefined; } - const contents = await this._controller.getBlameInformationHover(textEditor.document.uri, lineBlameInformation.blameInformation, true); + const contents = await this._controller.getBlameInformationHover(textEditor.document.uri, lineBlameInformation.blameInformation); if (!contents || token.isCancellationRequested) { return undefined; @@ -722,10 +694,16 @@ class GitBlameStatusBarItem { const config = workspace.getConfiguration('git'); const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); - this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; - this._statusBarItem.tooltip = await this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage( + window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; + + this._statusBarItem.tooltip2 = (cancellationToken: CancellationToken) => { + return this._provideTooltip(window.activeTextEditor!.document.uri, + blameInformation[0].blameInformation as BlameInformation, cancellationToken); + }; + this._statusBarItem.command = { - title: l10n.t('View Commit'), + title: l10n.t('Open Commit'), command: 'git.viewCommit', arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; @@ -734,6 +712,15 @@ class GitBlameStatusBarItem { this._statusBarItem.show(); } + private async _provideTooltip(uri: Uri, blameInformation: BlameInformation, cancellationToken: CancellationToken): Promise { + if (cancellationToken.isCancellationRequested) { + return undefined; + } + + const tooltip = await this._controller.getBlameInformationHover(uri, blameInformation); + return cancellationToken.isCancellationRequested ? undefined : tooltip; + } + dispose() { this._disposables = dispose(this._disposables); } diff --git a/code/extensions/git/src/cache.ts b/code/extensions/git/src/cache.ts new file mode 100644 index 00000000000..df0c0df5561 --- /dev/null +++ b/code/extensions/git/src/cache.ts @@ -0,0 +1,485 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +interface Item { + previous: Item | undefined; + next: Item | undefined; + key: K; + value: V; +} + +const enum Touch { + None = 0, + AsOld = 1, + AsNew = 2 +} + +class LinkedMap implements Map { + + readonly [Symbol.toStringTag] = 'LinkedMap'; + + private _map: Map>; + private _head: Item | undefined; + private _tail: Item | undefined; + private _size: number; + + private _state: number; + + constructor() { + this._map = new Map>(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state = 0; + } + + clear(): void { + this._map.clear(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state++; + } + + isEmpty(): boolean { + return !this._head && !this._tail; + } + + get size(): number { + return this._size; + } + + get first(): V | undefined { + return this._head?.value; + } + + get last(): V | undefined { + return this._tail?.value; + } + + has(key: K): boolean { + return this._map.has(key); + } + + get(key: K, touch: Touch = Touch.None): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + if (touch !== Touch.None) { + this.touch(item, touch); + } + return item.value; + } + + set(key: K, value: V, touch: Touch = Touch.None): this { + let item = this._map.get(key); + if (item) { + item.value = value; + if (touch !== Touch.None) { + this.touch(item, touch); + } + } else { + item = { key, value, next: undefined, previous: undefined }; + switch (touch) { + case Touch.None: + this.addItemLast(item); + break; + case Touch.AsOld: + this.addItemFirst(item); + break; + case Touch.AsNew: + this.addItemLast(item); + break; + default: + this.addItemLast(item); + break; + } + this._map.set(key, item); + this._size++; + } + return this; + } + + delete(key: K): boolean { + return !!this.remove(key); + } + + remove(key: K): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + this._map.delete(key); + this.removeItem(item); + this._size--; + return item.value; + } + + shift(): V | undefined { + if (!this._head && !this._tail) { + return undefined; + } + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + const item = this._head; + this._map.delete(item.key); + this.removeItem(item); + this._size--; + return item.value; + } + + forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + const state = this._state; + let current = this._head; + while (current) { + if (thisArg) { + callbackfn.bind(thisArg)(current.value, current.key, this); + } else { + callbackfn(current.value, current.key, this); + } + if (this._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + current = current.next; + } + } + + keys(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.key, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + values(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.value, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + entries(): IterableIterator<[K, V]> { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator<[K, V]> = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult<[K, V]> { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } + + protected trimOld(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._head; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.next; + currentSize--; + } + this._head = current; + this._size = currentSize; + if (current) { + current.previous = undefined; + } + this._state++; + } + + protected trimNew(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._tail; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.previous; + currentSize--; + } + this._tail = current; + this._size = currentSize; + if (current) { + current.next = undefined; + } + this._state++; + } + + private addItemFirst(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._tail = item; + } else if (!this._head) { + throw new Error('Invalid list'); + } else { + item.next = this._head; + this._head.previous = item; + } + this._head = item; + this._state++; + } + + private addItemLast(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._head = item; + } else if (!this._tail) { + throw new Error('Invalid list'); + } else { + item.previous = this._tail; + this._tail.next = item; + } + this._tail = item; + this._state++; + } + + private removeItem(item: Item): void { + if (item === this._head && item === this._tail) { + this._head = undefined; + this._tail = undefined; + } + else if (item === this._head) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.next) { + throw new Error('Invalid list'); + } + item.next.previous = undefined; + this._head = item.next; + } + else if (item === this._tail) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.previous) { + throw new Error('Invalid list'); + } + item.previous.next = undefined; + this._tail = item.previous; + } + else { + const next = item.next; + const previous = item.previous; + if (!next || !previous) { + throw new Error('Invalid list'); + } + next.previous = previous; + previous.next = next; + } + item.next = undefined; + item.previous = undefined; + this._state++; + } + + private touch(item: Item, touch: Touch): void { + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + if ((touch !== Touch.AsOld && touch !== Touch.AsNew)) { + return; + } + + if (touch === Touch.AsOld) { + if (item === this._head) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item + if (item === this._tail) { + // previous must be defined since item was not head but is tail + // So there are more than on item in the map + previous!.next = undefined; + this._tail = previous; + } + else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + + // Insert the node at head + item.previous = undefined; + item.next = this._head; + this._head.previous = item; + this._head = item; + this._state++; + } else if (touch === Touch.AsNew) { + if (item === this._tail) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item. + if (item === this._head) { + // next must be defined since item was not tail but is head + // So there are more than on item in the map + next!.previous = undefined; + this._head = next; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + item.next = undefined; + item.previous = this._tail; + this._tail.next = item; + this._tail = item; + this._state++; + } + } + + toJSON(): [K, V][] { + const data: [K, V][] = []; + + this.forEach((value, key) => { + data.push([key, value]); + }); + + return data; + } + + fromJSON(data: [K, V][]): void { + this.clear(); + + for (const [key, value] of data) { + this.set(key, value); + } + } +} + +abstract class Cache extends LinkedMap { + + protected _limit: number; + protected _ratio: number; + + constructor(limit: number, ratio: number = 1) { + super(); + this._limit = limit; + this._ratio = Math.min(Math.max(0, ratio), 1); + } + + get limit(): number { + return this._limit; + } + + set limit(limit: number) { + this._limit = limit; + this.checkTrim(); + } + + get ratio(): number { + return this._ratio; + } + + set ratio(ratio: number) { + this._ratio = Math.min(Math.max(0, ratio), 1); + this.checkTrim(); + } + + override get(key: K, touch: Touch = Touch.AsNew): V | undefined { + return super.get(key, touch); + } + + peek(key: K): V | undefined { + return super.get(key, Touch.None); + } + + override set(key: K, value: V): this { + super.set(key, value, Touch.AsNew); + return this; + } + + protected checkTrim() { + if (this.size > this._limit) { + this.trim(Math.round(this._limit * this._ratio)); + } + } + + protected abstract trim(newSize: number): void; +} + +export class LRUCache extends Cache { + + constructor(limit: number, ratio: number = 1) { + super(limit, ratio); + } + + protected override trim(newSize: number) { + this.trimOld(newSize); + } + + override set(key: K, value: V): this { + super.set(key, value); + this.checkTrim(); + return this; + } +} diff --git a/code/extensions/git/src/commands.ts b/code/extensions/git/src/commands.ts index d3e852ceab8..abb00af22ba 100644 --- a/code/extensions/git/src/commands.ts +++ b/code/extensions/git/src/commands.ts @@ -1341,6 +1341,10 @@ export class CommandCenter { } await repository.move(from, to); + + // Close active editor and open the renamed file + await commands.executeCommand('workbench.action.closeActiveEditor'); + await commands.executeCommand('vscode.open', Uri.file(path.join(repository.root, to)), { viewColumn: ViewColumn.Active }); } @command('git.stage') @@ -2539,23 +2543,38 @@ export class CommandCenter { return this._checkout(repository, { treeish }); } - @command('git.checkoutDetached', { repository: true }) - async checkoutDetached(repository: Repository, treeish?: string): Promise { - return this._checkout(repository, { detached: true, treeish }); - } - - @command('git.checkoutRef', { repository: true }) - async checkoutRef(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + @command('git.graph.checkout', { repository: true }) + async checkout2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); if (!historyItemRef) { - return false; + return; } - return this._checkout(repository, { treeish: historyItemRefId }); + const config = workspace.getConfiguration('git', Uri.file(repository.root)); + const pullBeforeCheckout = config.get('pullBeforeCheckout', false) === true; + + // Branch, tag + if (historyItemRef.id.startsWith('refs/heads/') || historyItemRef.id.startsWith('refs/tags/')) { + await repository.checkout(historyItemRef.name, { pullBeforeCheckout }); + return; + } + + // Remote branch + const branches = await repository.findTrackingBranches(historyItemRef.name); + if (branches.length > 0) { + await repository.checkout(branches[0].name!, { pullBeforeCheckout }); + } else { + await repository.checkoutTracking(historyItemRef.name); + } + } + + @command('git.checkoutDetached', { repository: true }) + async checkoutDetached(repository: Repository, treeish?: string): Promise { + return this._checkout(repository, { detached: true, treeish }); } - @command('git.checkoutRefDetached', { repository: true }) - async checkoutRefDetached(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { + @command('git.graph.checkoutDetached', { repository: true }) + async checkoutDetached2(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { if (!historyItem) { return false; } @@ -2860,10 +2879,24 @@ export class CommandCenter { } @command('git.deleteBranch', { repository: true }) - async deleteBranch(repository: Repository, name: string, force?: boolean): Promise { + async deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise { + await this._deleteBranch(repository, name, force); + } + + @command('git.graph.deleteBranch', { repository: true }) + async deleteBranch2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); + if (!historyItemRef) { + return; + } + + await this._deleteBranch(repository, historyItemRef.name); + } + + private async _deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise { let run: (force?: boolean) => Promise; if (typeof name === 'string') { - run = force => repository.deleteBranch(name, force); + run = force => repository.deleteBranch(name!, force); } else { const getBranchPicks = async () => { const refs = await repository.getRefs({ pattern: 'refs/heads' }); @@ -2999,12 +3032,21 @@ export class CommandCenter { const placeHolder = l10n.t('Select a tag to delete'); const choice = await this.pickRef(tagPicks(), placeHolder); - if (choice instanceof TagDeleteItem) { await choice.run(repository); } } + @command('git.graph.deleteTag', { repository: true }) + async deleteTag2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); + if (!historyItemRef) { + return; + } + + await repository.deleteTag(historyItemRef.name); + } + @command('git.deleteRemoteTag', { repository: true }) async deleteRemoteTag(repository: Repository): Promise { const remotePicks = repository.remotes @@ -3363,11 +3405,12 @@ export class CommandCenter { await repository.cherryPick(hash); } - @command('git.cherryPickRef', { repository: true }) - async cherryPickRef(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { + @command('git.graph.cherryPick', { repository: true }) + async cherryPick2(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { if (!historyItem) { return; } + await repository.cherryPick(historyItem.id); } @@ -4317,6 +4360,23 @@ export class CommandCenter { env.clipboard.writeText(content); } + @command('git.blame.toggleEditorDecoration') + toggleBlameEditorDecoration(): void { + this._toggleBlameSetting('blame.editorDecoration.enabled'); + } + + @command('git.blame.toggleStatusBarItem') + toggleBlameStatusBarItem(): void { + this._toggleBlameSetting('blame.statusBarItem.enabled'); + } + + private _toggleBlameSetting(setting: string): void { + const config = workspace.getConfiguration('git'); + const enabled = config.get(setting) === true; + + config.update(setting, !enabled, true); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/code/extensions/git/src/fileSystemProvider.ts b/code/extensions/git/src/fileSystemProvider.ts index 0847fe8d745..6e30118128d 100644 --- a/code/extensions/git/src/fileSystemProvider.ts +++ b/code/extensions/git/src/fileSystemProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError } from 'vscode'; +import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError, LogOutputChannel } from 'vscode'; import { debounce, throttle } from './decorators'; import { fromGitUri, toGitUri } from './uri'; import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model'; @@ -43,7 +43,7 @@ export class GitFileSystemProvider implements FileSystemProvider { private mtime = new Date().getTime(); private disposables: Disposable[] = []; - constructor(private model: Model) { + constructor(private readonly model: Model, private readonly logger: LogOutputChannel) { this.disposables.push( model.onDidChangeRepository(this.onDidChangeRepository, this), model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this), @@ -136,17 +136,18 @@ export class GitFileSystemProvider implements FileSystemProvider { const { submoduleOf, path, ref } = fromGitUri(uri); const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri); if (!repository) { + this.logger.warn(`[GitFileSystemProvider][stat] Repository not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); } - let size = 0; try { const details = await repository.getObjectDetails(sanitizeRef(ref, path, repository), path); - size = details.size; + return { type: FileType.File, size: details.size, mtime: this.mtime, ctime: 0 }; } catch { - // noop + // File does not exist in git. This could be because the file is untracked or ignored + this.logger.warn(`[GitFileSystemProvider][stat] File not found - ${uri.toString()}`); + throw FileSystemError.FileNotFound(); } - return { type: FileType.File, size: size, mtime: this.mtime, ctime: 0 }; } readDirectory(): Thenable<[string, FileType][]> { @@ -181,6 +182,7 @@ export class GitFileSystemProvider implements FileSystemProvider { const repository = this.model.getRepository(uri); if (!repository) { + this.logger.warn(`[GitFileSystemProvider][readFile] Repository not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); } @@ -191,9 +193,9 @@ export class GitFileSystemProvider implements FileSystemProvider { try { return await repository.buffer(sanitizeRef(ref, path, repository), path); - } catch (err) { - // File does not exist in git. This could be - // because the file is untracked or ignored + } catch { + // File does not exist in git. This could be because the file is untracked or ignored + this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); } } diff --git a/code/extensions/git/src/git.ts b/code/extensions/git/src/git.ts index b000245a0f2..431d50b88af 100644 --- a/code/extensions/git/src/git.ts +++ b/code/extensions/git/src/git.ts @@ -1098,7 +1098,7 @@ function parseGitBlame(data: string): BlameInformation[] { authorName = line.substring('author '.length); } if (commitHash && line.startsWith('author-mail ')) { - authorEmail = line.substring('author-mail '.length); + authorEmail = line.substring('author-mail <'.length, line.length - 1); } if (commitHash && line.startsWith('author-time ')) { authorTime = Number(line.substring('author-time '.length)) * 1000; @@ -1367,14 +1367,17 @@ export class Repository { } async getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> { - if (!treeish) { // index + if (!treeish || treeish === ':1' || treeish === ':2' || treeish === ':3') { // index const elements = await this.lsfiles(path); if (elements.length === 0) { throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath }); } - const { mode, object } = elements[0]; + const { mode, object } = treeish !== '' + ? elements.find(e => e.stage === treeish.substring(1)) ?? elements[0] + : elements[0]; + const catFile = await this.exec(['cat-file', '-s', object]); const size = parseInt(catFile.stdout); @@ -1592,8 +1595,14 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } - async diffTrees(treeish1: string, treeish2?: string): Promise { - const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR', treeish1]; + async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise { + const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR']; + + if (options?.similarityThreshold) { + args.push(`--find-renames=${options.similarityThreshold}%`); + } + + args.push(treeish1); if (treeish2) { args.push(treeish2); @@ -2445,7 +2454,9 @@ export class Repository { // Upstream commit if (HEAD && HEAD.upstream) { - const ref = `refs/remotes/${HEAD.upstream.remote}/${HEAD.upstream.name}`; + const ref = HEAD.upstream.remote !== '.' + ? `refs/remotes/${HEAD.upstream.remote}/${HEAD.upstream.name}` + : `refs/heads/${HEAD.upstream.name}`; const commit = await this.revParse(ref); HEAD = { ...HEAD, upstream: { ...HEAD.upstream, commit } }; } @@ -2848,6 +2859,15 @@ export class Repository { return commits[0]; } + async revList(ref1: string, ref2: string): Promise { + const result = await this.exec(['rev-list', `${ref1}..${ref2}`]); + if (result.stderr) { + return []; + } + + return result.stdout.trim().split('\n'); + } + async revParse(ref: string): Promise { try { const result = await fs.readFile(path.join(this.dotGit.path, ref), 'utf8'); diff --git a/code/extensions/git/src/historyItemDetailsProvider.ts b/code/extensions/git/src/historyItemDetailsProvider.ts new file mode 100644 index 00000000000..be0e2b337f8 --- /dev/null +++ b/code/extensions/git/src/historyItemDetailsProvider.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command, Disposable } from 'vscode'; +import { AvatarQuery, SourceControlHistoryItemDetailsProvider } from './api/git'; +import { Repository } from './repository'; +import { ApiRepository } from './api/api1'; + +export interface ISourceControlHistoryItemDetailsProviderRegistry { + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; + getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[]; +} + +export async function provideSourceControlHistoryItemAvatar( + registry: ISourceControlHistoryItemDetailsProviderRegistry, + repository: Repository, + query: AvatarQuery +): Promise | undefined> { + for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { + const result = await provider.provideAvatar(new ApiRepository(repository), query); + + if (result) { + return result; + } + } + + return undefined; +} + +export async function provideSourceControlHistoryItemHoverCommands( + registry: ISourceControlHistoryItemDetailsProviderRegistry, + repository: Repository +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { + const result = await provider.provideHoverCommands(new ApiRepository(repository)); + + if (result) { + return result; + } + } + + return undefined; +} + +export async function provideSourceControlHistoryItemMessageLinks( + registry: ISourceControlHistoryItemDetailsProviderRegistry, + repository: Repository, + message: string +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { + const result = await provider.provideMessageLinks( + new ApiRepository(repository), message); + + if (result) { + return result; + } + } + + return undefined; +} diff --git a/code/extensions/git/src/historyProvider.ts b/code/extensions/git/src/historyProvider.ts index 9512facf774..f15c5899fda 100644 --- a/code/extensions/git/src/historyProvider.ts +++ b/code/extensions/git/src/historyProvider.ts @@ -7,11 +7,12 @@ import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util'; -import { toGitUri } from './uri'; -import { Branch, LogOptions, Ref, RefType } from './api/git'; +import { toMultiFileDiffEditorUris } from './uri'; +import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; +import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { const rootUri = Uri.file(repository.root); @@ -96,7 +97,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private disposables: Disposable[] = []; - constructor(protected readonly repository: Repository, private readonly logger: LogOutputChannel) { + constructor( + private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry, + private readonly repository: Repository, + private readonly logger: LogOutputChannel + ) { const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly); this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this)); @@ -131,12 +136,27 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec historyItemRefName = this.repository.HEAD.name; // Remote - this._currentHistoryItemRemoteRef = this.repository.HEAD.upstream ? { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - revision: this.repository.HEAD.upstream.commit, - icon: new ThemeIcon('cloud') - } : undefined; + if (this.repository.HEAD.upstream) { + if (this.repository.HEAD.upstream.remote === '.') { + // Local branch + this._currentHistoryItemRemoteRef = { + id: `refs/heads/${this.repository.HEAD.upstream.name}`, + name: this.repository.HEAD.upstream.name, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('gi-branch') + }; + } else { + // Remote branch + this._currentHistoryItemRemoteRef = { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('cloud') + }; + } + } else { + this._currentHistoryItemRemoteRef = undefined; + } // Base if (this._HEAD?.name !== this.repository.HEAD.name) { @@ -262,24 +282,51 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const commits = await this.repository.log({ ...logOptions, silent: true }); + // Avatars + const avatarQuery = { + commits: commits.map(c => ({ + hash: c.hash, + authorName: c.authorName, + authorEmail: c.authorEmail + } satisfies AvatarQueryCommit)), + size: 20 + } satisfies AvatarQuery; + + const commitAvatars = await provideSourceControlHistoryItemAvatar( + this.historyItemDetailProviderRegistry, this.repository, avatarQuery); + await ensureEmojis(); - return commits.map(commit => { + const historyItems: SourceControlHistoryItem[] = []; + for (const commit of commits) { + const message = emojify(commit.message); + const messageWithLinks = await provideSourceControlHistoryItemMessageLinks( + this.historyItemDetailProviderRegistry, this.repository, message) ?? message; + + const newLineIndex = message.indexOf('\n'); + const subject = newLineIndex !== -1 + ? `${message.substring(0, newLineIndex)}\u2026` + : message; + + const avatarUrl = commitAvatars?.get(commit.hash); const references = this._resolveHistoryItemRefs(commit); - return { + historyItems.push({ id: commit.hash, parentIds: commit.parents, - message: emojify(commit.message), + subject, + message: messageWithLinks, author: commit.authorName, authorEmail: commit.authorEmail, - icon: new ThemeIcon('git-commit'), + authorIcon: avatarUrl ? Uri.parse(avatarUrl) : new ThemeIcon('account'), displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, references: references.length !== 0 ? references : undefined - }; - }); + } satisfies SourceControlHistoryItem); + } + + return historyItems; } catch (err) { this.logger.error(`[GitHistoryProvider][provideHistoryItems] Failed to get history items with options '${JSON.stringify(options)}': ${err}`); return []; @@ -301,10 +348,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // History item change historyItemChanges.push({ uri: historyItemUri, - originalUri: toGitUri(change.originalUri, historyItemParentId), - modifiedUri: toGitUri(change.uri, historyItemId), - renameUri: change.renameUri, - }); + ...toMultiFileDiffEditorUris(change, historyItemParentId, historyItemId) + } satisfies SourceControlHistoryItemChange); // History item change decoration const letter = Resource.getStatusLetter(change.status); @@ -369,6 +414,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref.substring('HEAD -> '.length), name: ref.substring('HEAD -> refs/heads/'.length), revision: commit.hash, + category: l10n.t('branches'), icon: new ThemeIcon('target') }); break; @@ -377,6 +423,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref, name: ref.substring('refs/heads/'.length), revision: commit.hash, + category: l10n.t('branches'), icon: new ThemeIcon('git-branch') }); break; @@ -385,6 +432,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref, name: ref.substring('refs/remotes/'.length), revision: commit.hash, + category: l10n.t('remote branches'), icon: new ThemeIcon('cloud') }); break; @@ -393,6 +441,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref.substring('tag: '.length), name: ref.substring('tag: refs/tags/'.length), revision: commit.hash, + category: l10n.t('tags'), icon: new ThemeIcon('tag') }); break; diff --git a/code/extensions/git/src/main.ts b/code/extensions/git/src/main.ts index 515f57c12cf..8205d8de69f 100644 --- a/code/extensions/git/src/main.ts +++ b/code/extensions/git/src/main.ts @@ -112,7 +112,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, const cc = new CommandCenter(git, model, context.globalState, logger, telemetryReporter); disposables.push( cc, - new GitFileSystemProvider(model), + new GitFileSystemProvider(model, logger), new GitDecorations(model), new GitBlameController(model), new GitTimelineProvider(model, cc), diff --git a/code/extensions/git/src/model.ts b/code/extensions/git/src/model.ts index 142d073914f..f64528275d0 100644 --- a/code/extensions/git/src/model.ts +++ b/code/extensions/git/src/model.ts @@ -12,13 +12,14 @@ import { Git } from './git'; import * as path from 'path'; import * as fs from 'fs'; import { fromGitUri } from './uri'; -import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git'; +import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, SourceControlHistoryItemDetailsProvider } from './api/git'; import { Askpass } from './askpass'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { IPostCommitCommandsProviderRegistry } from './postCommitCommands'; import { IBranchProtectionProviderRegistry } from './branchProtection'; +import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; class RepositoryPick implements QuickPickItem { @memoize get label(): string { @@ -170,7 +171,7 @@ class UnsafeRepositoriesManager { } } -export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry { +export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry, ISourceControlHistoryItemDetailsProviderRegistry { private _onDidOpenRepository = new EventEmitter(); readonly onDidOpenRepository: Event = this._onDidOpenRepository.event; @@ -236,6 +237,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event; private pushErrorHandlers = new Set(); + private historyItemDetailsProviders = new Set(); private _unsafeRepositoriesManager: UnsafeRepositoriesManager; get unsafeRepositories(): string[] { @@ -633,7 +635,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Open repository const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]); - const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); + const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); @@ -1002,6 +1004,15 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return [...this.pushErrorHandlers]; } + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable { + this.historyItemDetailsProviders.add(provider); + return toDisposable(() => this.historyItemDetailsProviders.delete(provider)); + } + + getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[] { + return [...this.historyItemDetailsProviders]; + } + getUnsafeRepositoryPath(repository: string): string | undefined { return this._unsafeRepositoriesManager.getRepositoryPath(repository); } diff --git a/code/extensions/git/src/remoteSource.ts b/code/extensions/git/src/remoteSource.ts index dfdb36fc11f..eb63e5db81f 100644 --- a/code/extensions/git/src/remoteSource.ts +++ b/code/extensions/git/src/remoteSource.ts @@ -15,7 +15,3 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): P export async function getRemoteSourceActions(url: string) { return GitBaseApi.getAPI().getRemoteSourceActions(url); } - -export async function getRemoteSourceControlHistoryItemCommands(url: string) { - return GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(url); -} diff --git a/code/extensions/git/src/repository.ts b/code/extensions/git/src/repository.ts index c576bc790b0..3f7ccb765f3 100644 --- a/code/extensions/git/src/repository.ts +++ b/code/extensions/git/src/repository.ts @@ -26,6 +26,7 @@ import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { detectEncoding } from './encoding'; +import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -841,6 +842,7 @@ export class Repository implements Disposable { private isRepositoryHuge: false | { limit: number } = false; private didWarnAboutLimit = false; + private unpublishedCommits: Set | undefined = undefined; private branchProtection = new Map(); private commitCommandCenter: CommitCommandsCenter; private resourceCommandResolver = new ResourceCommandResolver(this); @@ -854,6 +856,7 @@ export class Repository implements Disposable { remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry, postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry, private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry, + historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry, globalState: Memento, private readonly logger: LogOutputChannel, private telemetryReporter: TelemetryReporter @@ -892,7 +895,7 @@ export class Repository implements Disposable { this._sourceControl.quickDiffProvider = this; - this._historyProvider = new GitHistoryProvider(this, logger); + this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger); this._sourceControl.historyProvider = this._historyProvider; this.disposables.push(this._historyProvider); @@ -1163,7 +1166,10 @@ export class Repository implements Disposable { } diffTrees(treeish1: string, treeish2?: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2)); + const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root)); + const similarityThreshold = scopedConfig.get('similarityThreshold', 50); + + return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2, { similarityThreshold })); } getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { @@ -1517,7 +1523,7 @@ export class Repository implements Disposable { async getBranches(query: BranchQuery = {}, cancellationToken?: CancellationToken): Promise { return await this.run(Operation.GetBranches, async () => { const refs = await this.getRefs(query, cancellationToken); - return refs.filter(value => (value.type === RefType.Head || value.type === RefType.RemoteHead) && (query.remote || !value.remote)); + return refs.filter(value => value.type === RefType.Head || (value.type === RefType.RemoteHead && query.remote)); }); } @@ -2270,6 +2276,17 @@ export class Repository implements Disposable { this.isCherryPickInProgress(), this.getInputTemplate()]); + // Reset the list of unpublished commits if HEAD has + // changed (ex: checkout, fetch, pull, push, publish, etc.). + // The list of unpublished commits will be computed lazily + // on demand. + if (this.HEAD?.name !== HEAD?.name || + this.HEAD?.commit !== HEAD?.commit || + this.HEAD?.ahead !== HEAD?.ahead || + this.HEAD?.upstream !== HEAD?.upstream) { + this.unpublishedCommits = undefined; + } + this._HEAD = HEAD; this._remotes = remotes!; this._submodules = submodules!; @@ -2513,7 +2530,9 @@ export class Repository implements Disposable { private async maybeAutoStash(runOperation: () => Promise): Promise { const config = workspace.getConfiguration('git', Uri.file(this.root)); const shouldAutoStash = config.get('autoStash') - && this.workingTreeGroup.resourceStates.some(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED); + && (this.indexGroup.resourceStates.length > 0 + || this.workingTreeGroup.resourceStates.some( + r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED)); if (!shouldAutoStash) { return await runOperation(); @@ -2746,6 +2765,24 @@ export class Repository implements Disposable { return false; } + async getUnpublishedCommits(): Promise> { + if (this.unpublishedCommits) { + return this.unpublishedCommits; + } + + if (this.HEAD && this.HEAD.name && this.HEAD.upstream && this.HEAD.ahead && this.HEAD.ahead > 0) { + const ref1 = `${this.HEAD.upstream.remote}/${this.HEAD.upstream.name}`; + const ref2 = this.HEAD.name; + + const revList = await this.repository.revList(ref1, ref2); + this.unpublishedCommits = new Set(revList); + } else { + this.unpublishedCommits = new Set(); + } + + return this.unpublishedCommits; + } + dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/code/extensions/git/src/timelineProvider.ts b/code/extensions/git/src/timelineProvider.ts index fbf9b139a0a..f29264e4078 100644 --- a/code/extensions/git/src/timelineProvider.ts +++ b/code/extensions/git/src/timelineProvider.ts @@ -12,7 +12,10 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; -import { getRemoteSourceControlHistoryItemCommands } from './remoteSource'; +import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { AvatarQuery, AvatarQueryCommit } from './api/git'; + +const AVATAR_SIZE = 20; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -51,16 +54,20 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } - setItemDetails(uri: Uri, hash: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { + setItemDetails(uri: Uri, hash: string | undefined, avatar: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { this.tooltip = new MarkdownString('', true); this.tooltip.isTrusted = true; this.tooltip.supportHtml = true; + const avatarMarkdown = avatar + ? `![${author}](${avatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` + : '$(account)'; + if (email) { const emailTitle = l10n.t('Email'); - this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")`); + this.tooltip.appendMarkdown(`${avatarMarkdown} [**${author}**](mailto:${email} "${emailTitle} ${author}")`); } else { - this.tooltip.appendMarkdown(`$(account) **${author}**`); + this.tooltip.appendMarkdown(`${avatarMarkdown} **${author}**`); } this.tooltip.appendMarkdown(`, $(history) ${date}\n\n`); @@ -69,25 +76,26 @@ export class GitTimelineItem extends TimelineItem { if (shortStat) { this.tooltip.appendMarkdown(`---\n\n`); + const labels: string[] = []; if (shortStat.insertions) { - this.tooltip.appendMarkdown(`${shortStat.insertions === 1 ? + labels.push(`${shortStat.insertions === 1 ? l10n.t('{0} insertion{1}', shortStat.insertions, '(+)') : l10n.t('{0} insertions{1}', shortStat.insertions, '(+)')}`); } if (shortStat.deletions) { - this.tooltip.appendMarkdown(`, ${shortStat.deletions === 1 ? + labels.push(`${shortStat.deletions === 1 ? l10n.t('{0} deletion{1}', shortStat.deletions, '(-)') : l10n.t('{0} deletions{1}', shortStat.deletions, '(-)')}`); } - this.tooltip.appendMarkdown(`\n\n`); + this.tooltip.appendMarkdown(`${labels.join(', ')}\n\n`); } if (hash) { this.tooltip.appendMarkdown(`---\n\n`); - this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); + this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('Open Commit')}")`); this.tooltip.appendMarkdown(' '); this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); @@ -214,23 +222,37 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); - const defaultRemote = repo.getDefaultRemote(); - const remoteSourceCommands: Command[] = defaultRemote?.fetchUrl - ? await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl) - : []; + const unpublishedCommits = await repo.getUnpublishedCommits(); + const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.model, repo); + + const avatarQuery = { + commits: commits.map(c => ({ + hash: c.hash, + authorName: c.authorName, + authorEmail: c.authorEmail + }) satisfies AvatarQueryCommit), + size: 20 + } satisfies AvatarQuery; + const avatars = await provideSourceControlHistoryItemAvatar(this.model, repo, avatarQuery); + + const items: GitTimelineItem[] = []; + for (let index = 0; index < commits.length; index++) { + const c = commits[index]; - const items = commits.map((c, i) => { const date = dateType === 'authored' ? c.authorDate : c.commitDate; const message = emojify(c.message); - const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); + const item = new GitTimelineItem(c.hash, commits[index + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); item.iconPath = new ThemeIcon('git-commit'); if (showAuthor) { item.description = c.authorName; } - item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat, remoteSourceCommands); + const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteHoverCommands : []; + const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; + + item.setItemDetails(uri, c.hash, avatars?.get(c.hash), c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -241,8 +263,8 @@ export class GitTimelineProvider implements TimelineProvider { }; } - return item; - }); + items.push(item); + } if (options.cursor === undefined) { const you = l10n.t('You'); @@ -255,7 +277,7 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); + item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -277,7 +299,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); item.iconPath = new ThemeIcon('circle-outline'); item.description = ''; - item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); + item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { diff --git a/code/extensions/git/src/typings/git-base.d.ts b/code/extensions/git/src/typings/git-base.d.ts index 37dd2c4229c..d4ec49df47d 100644 --- a/code/extensions/git/src/typings/git-base.d.ts +++ b/code/extensions/git/src/typings/git-base.d.ts @@ -9,7 +9,6 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -82,7 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/code/extensions/git/src/uri.ts b/code/extensions/git/src/uri.ts index 169abd1bc35..8b04fabe583 100644 --- a/code/extensions/git/src/uri.ts +++ b/code/extensions/git/src/uri.ts @@ -64,12 +64,24 @@ export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } { export function toMultiFileDiffEditorUris(change: Change, originalRef: string, modifiedRef: string): { originalUri: Uri | undefined; modifiedUri: Uri | undefined } { switch (change.status) { case Status.INDEX_ADDED: - return { originalUri: undefined, modifiedUri: toGitUri(change.uri, modifiedRef) }; + return { + originalUri: undefined, + modifiedUri: toGitUri(change.uri, modifiedRef) + }; case Status.DELETED: - return { originalUri: toGitUri(change.uri, originalRef), modifiedUri: undefined }; + return { + originalUri: toGitUri(change.uri, originalRef), + modifiedUri: undefined + }; case Status.INDEX_RENAMED: - return { originalUri: toGitUri(change.originalUri, originalRef), modifiedUri: toGitUri(change.uri, modifiedRef) }; + return { + originalUri: toGitUri(change.originalUri, originalRef), + modifiedUri: toGitUri(change.uri, modifiedRef) + }; default: - return { originalUri: toGitUri(change.uri, originalRef), modifiedUri: toGitUri(change.uri, modifiedRef) }; + return { + originalUri: toGitUri(change.uri, originalRef), + modifiedUri: toGitUri(change.uri, modifiedRef) + }; } } diff --git a/code/extensions/git/tsconfig.json b/code/extensions/git/tsconfig.json index 5a65f5c82ae..42218d8decb 100644 --- a/code/extensions/git/tsconfig.json +++ b/code/extensions/git/tsconfig.json @@ -14,6 +14,7 @@ "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", "../../src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", @@ -21,11 +22,11 @@ "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", "../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts", "../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts", + "../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", - "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../types/lib.textEncoder.d.ts" ] } diff --git a/code/extensions/github-authentication/src/flows.ts b/code/extensions/github-authentication/src/flows.ts index a2497b2b0b2..8920ea5d357 100644 --- a/code/extensions/github-authentication/src/flows.ts +++ b/code/extensions/github-authentication/src/flows.ts @@ -105,8 +105,6 @@ async function exchangeCodeForToken( headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': body.toString() - }, body: body.toString() }); diff --git a/code/extensions/github-authentication/src/node/fetch.ts b/code/extensions/github-authentication/src/node/fetch.ts index 58718078e69..ca344d436b1 100644 --- a/code/extensions/github-authentication/src/node/fetch.ts +++ b/code/extensions/github-authentication/src/node/fetch.ts @@ -2,6 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import fetch from 'node-fetch'; -export const fetching = fetch; +let _fetch: typeof fetch; +try { + _fetch = require('electron').net.fetch; +} catch { + _fetch = fetch; +} +export const fetching = _fetch; diff --git a/code/extensions/github/package.json b/code/extensions/github/package.json index f99a41d5979..524cee5bbea 100644 --- a/code/extensions/github/package.json +++ b/code/extensions/github/package.json @@ -27,33 +27,46 @@ } }, "enabledApiProposals": [ - "contribShareMenu", - "contribEditSessions", "canonicalUriProvider", - "shareProvider" + "contribEditSessions", + "contribShareMenu", + "contribSourceControlHistoryItemMenu", + "scmHistoryProvider", + "shareProvider", + "timeline" ], "contributes": { "commands": [ { "command": "github.publish", - "title": "Publish to GitHub" + "title": "%command.publish%" }, { "command": "github.copyVscodeDevLink", - "title": "Copy vscode.dev Link" + "title": "%command.copyVscodeDevLink%" }, { "command": "github.copyVscodeDevLinkFile", - "title": "Copy vscode.dev Link" + "title": "%command.copyVscodeDevLink%" }, { "command": "github.copyVscodeDevLinkWithoutRange", - "title": "Copy vscode.dev Link" + "title": "%command.copyVscodeDevLink%" }, { "command": "github.openOnVscodeDev", - "title": "Open in vscode.dev", + "title": "%command.openOnVscodeDev%", "icon": "$(globe)" + }, + { + "command": "github.graph.openOnGitHub", + "title": "%command.openOnGitHub%", + "icon": "$(github)" + }, + { + "command": "github.timeline.openOnGitHub", + "title": "%command.openOnGitHub%", + "icon": "$(github)" } ], "continueEditSession": [ @@ -71,6 +84,10 @@ "command": "github.publish", "when": "git-base.gitEnabled && workspaceFolderCount != 0 && remoteName != 'codespaces'" }, + { + "command": "github.graph.openOnGitHub", + "when": "false" + }, { "command": "github.copyVscodeDevLink", "when": "false" @@ -86,6 +103,10 @@ { "command": "github.openOnVscodeDev", "when": "false" + }, + { + "command": "github.timeline.openOnGitHub", + "when": "false" } ], "file/share": [ @@ -127,6 +148,27 @@ "when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'", "group": "0_vscode@0" } + ], + "scm/historyItem/context": [ + { + "command": "github.graph.openOnGitHub", + "when": "github.hasGitHubRepo", + "group": "0_view@2" + } + ], + "scm/historyItem/hover": [ + { + "command": "github.graph.openOnGitHub", + "when": "github.hasGitHubRepo", + "group": "1_open@1" + } + ], + "timeline/item/context": [ + { + "command": "github.timeline.openOnGitHub", + "group": "1_actions@3", + "when": "github.hasGitHubRepo && timelineItem =~ /git:file:commit\\b/" + } ] }, "configuration": [ @@ -153,6 +195,12 @@ ], "default": "https", "description": "%config.gitProtocol%" + }, + "github.showAvatar": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%config.showAvatar%" } } } diff --git a/code/extensions/github/package.nls.json b/code/extensions/github/package.nls.json index 5ead7903af2..40271bea980 100644 --- a/code/extensions/github/package.nls.json +++ b/code/extensions/github/package.nls.json @@ -1,9 +1,14 @@ { "displayName": "GitHub", "description": "GitHub features for VS Code", + "command.copyVscodeDevLink": "Copy vscode.dev Link", + "command.publish": "Publish to GitHub", + "command.openOnGitHub": "Open on GitHub", + "command.openOnVscodeDev": "Open in vscode.dev", "config.branchProtection": "Controls whether to query repository rules for GitHub repositories", "config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", "config.gitProtocol": "Controls which protocol is used to clone a GitHub repository", + "config.showAvatar": "Controls whether to show the GitHub avatar of the commit author in various hovers (ex: Git blame, Timeline, Source Control Graph, etc.)", "welcome.publishFolder": { "message": "You can directly publish this folder to a GitHub repository. Once published, you'll have access to source control features powered by Git and GitHub.\n[$(github) Publish to GitHub](command:github.publish)", "comment": [ diff --git a/code/extensions/github/src/branchProtection.ts b/code/extensions/github/src/branchProtection.ts index a7d18c41b9f..52b4fbfae22 100644 --- a/code/extensions/github/src/branchProtection.ts +++ b/code/extensions/github/src/branchProtection.ts @@ -48,7 +48,7 @@ const REPOSITORY_RULESETS_QUERY = ` } `; -export class GithubBranchProtectionProviderManager { +export class GitHubBranchProtectionProviderManager { private readonly disposables = new DisposableStore(); private readonly providerDisposables = new DisposableStore(); @@ -61,7 +61,7 @@ export class GithubBranchProtectionProviderManager { if (enabled) { for (const repository of this.gitAPI.repositories) { - this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); + this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); } } else { this.providerDisposables.dispose(); @@ -77,7 +77,7 @@ export class GithubBranchProtectionProviderManager { private readonly telemetryReporter: TelemetryReporter) { this.disposables.add(this.gitAPI.onDidOpenRepository(repository => { if (this._enabled) { - this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); + this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); } })); @@ -102,7 +102,7 @@ export class GithubBranchProtectionProviderManager { } -export class GithubBranchProtectionProvider implements BranchProtectionProvider { +export class GitHubBranchProtectionProvider implements BranchProtectionProvider { private readonly _onDidChangeBranchProtection = new EventEmitter(); onDidChangeBranchProtection = this._onDidChangeBranchProtection.event; @@ -173,12 +173,12 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider } // Repository details - this.logger.trace(`Fetching repository details for "${repository.owner}/${repository.repo}".`); + this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Fetching repository details for "${repository.owner}/${repository.repo}".`); const repositoryDetails = await this.getRepositoryDetails(repository.owner, repository.repo); // Check repository write permission if (repositoryDetails.viewerPermission !== 'ADMIN' && repositoryDetails.viewerPermission !== 'MAINTAIN' && repositoryDetails.viewerPermission !== 'WRITE') { - this.logger.trace(`Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`); + this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`); continue; } @@ -201,7 +201,7 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider // Save branch protection to global state await this.globalState.update(this.globalStateKey, branchProtection); - this.logger.trace(`Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`); + this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`); /* __GDPR__ "branchProtection" : { @@ -211,7 +211,7 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider */ this.telemetryReporter.sendTelemetryEvent('branchProtection', undefined, { rulesetCount: this.branchProtection.length }); } catch (err) { - this.logger.warn(`Failed to update repository branch protection: ${err.message}`); + this.logger.warn(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Failed to update repository branch protection: ${err.message}`); if (err instanceof AuthenticationError) { // A GitHub authentication session could be missing if the user has not yet diff --git a/code/extensions/github/src/commands.ts b/code/extensions/github/src/commands.ts index 2fc47e096f6..4e5587c09b5 100644 --- a/code/extensions/github/src/commands.ts +++ b/code/extensions/github/src/commands.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI } from './typings/git'; +import { API as GitAPI, RefType, Repository } from './typings/git'; import { publishRepository } from './publish'; -import { DisposableStore } from './util'; +import { DisposableStore, getRepositoryFromUrl } from './util'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links'; async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) { @@ -34,6 +34,29 @@ async function openVscodeDevLink(gitAPI: GitAPI): Promise { + // Get the unique remotes that contain the commit + const branches = await repository.getBranches({ contains: commit, remote: true }); + const remoteNames = new Set(branches.filter(b => b.type === RefType.RemoteHead && b.remote).map(b => b.remote!)); + + // GitHub remotes that contain the commit + const remotes = repository.state.remotes + .filter(r => remoteNames.has(r.name) && r.fetchUrl && getRepositoryFromUrl(r.fetchUrl)); + + if (remotes.length === 0) { + vscode.window.showInformationMessage(vscode.l10n.t('No GitHub remotes found that contain this commit.')); + return; + } + + // upstream -> origin -> first + const remote = remotes.find(r => r.name === 'upstream') + ?? remotes.find(r => r.name === 'origin') + ?? remotes[0]; + + const link = getCommitLink(remote.fetchUrl!, commit); + vscode.env.openExternal(vscode.Uri.parse(link)); +} + export function registerCommands(gitAPI: GitAPI): vscode.Disposable { const disposables = new DisposableStore(); @@ -62,6 +85,32 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { vscode.env.openExternal(vscode.Uri.parse(link)); })); + disposables.add(vscode.commands.registerCommand('github.graph.openOnGitHub', async (repository: vscode.SourceControl, historyItem: vscode.SourceControlHistoryItem) => { + if (!repository || !historyItem) { + return; + } + + const apiRepository = gitAPI.repositories.find(r => r.rootUri.fsPath === repository.rootUri?.fsPath); + if (!apiRepository) { + return; + } + + await openOnGitHub(apiRepository, historyItem.id); + })); + + disposables.add(vscode.commands.registerCommand('github.timeline.openOnGitHub', async (item: vscode.TimelineItem, uri: vscode.Uri) => { + if (!item.id || !uri) { + return; + } + + const apiRepository = gitAPI.getRepository(uri); + if (!apiRepository) { + return; + } + + await openOnGitHub(apiRepository, item.id); + })); + disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => { return openVscodeDevLink(gitAPI); })); diff --git a/code/extensions/github/src/extension.ts b/code/extensions/github/src/extension.ts index 1827768312e..de0349ba9d3 100644 --- a/code/extensions/github/src/extension.ts +++ b/code/extensions/github/src/extension.ts @@ -13,9 +13,10 @@ import { DisposableStore, repositoryHasGitHubRemote } from './util'; import { GithubPushErrorHandler } from './pushErrorHandler'; import { GitBaseExtension } from './typings/git-base'; import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; -import { GithubBranchProtectionProviderManager } from './branchProtection'; +import { GitHubBranchProtectionProviderManager } from './branchProtection'; import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; import { VscodeDevShareProvider } from './shareProviders'; +import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDetailsProvider'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -97,9 +98,10 @@ function initializeGitExtension(context: ExtensionContext, telemetryReporter: Te disposables.add(registerCommands(gitAPI)); disposables.add(new GithubCredentialProviderManager(gitAPI)); - disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); + disposables.add(new GitHubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter))); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); + disposables.add(gitAPI.registerSourceControlHistoryItemDetailsProvider(new GitHubSourceControlHistoryItemDetailsProvider(gitAPI, logger))); disposables.add(new GitHubCanonicalUriProvider(gitAPI)); disposables.add(new VscodeDevShareProvider(gitAPI)); setGitHubContext(gitAPI, disposables); diff --git a/code/extensions/github/src/historyItemDetailsProvider.ts b/code/extensions/github/src/historyItemDetailsProvider.ts new file mode 100644 index 00000000000..b31126e2ada --- /dev/null +++ b/code/extensions/github/src/historyItemDetailsProvider.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { authentication, Command, l10n, LogOutputChannel, workspace } from 'vscode'; +import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; +import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; +import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util'; +import { AuthenticationError, getOctokitGraphql } from './auth'; +import { getAvatarLink } from './links'; + +const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; + +const ASSIGNABLE_USERS_QUERY = ` + query assignableUsers($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + assignableUsers(first: 100) { + nodes { + id + login + name + email + avatarUrl + } + } + } + } +`; + +const COMMIT_AUTHOR_QUERY = ` + query commitAuthor($owner: String!, $repo: String!, $commit: String!) { + repository(owner: $owner, name: $repo) { + object(expression: $commit) { + ... on Commit { + author { + name + email + avatarUrl + user { + id + login + } + } + } + } + } + } +`; + +interface GitHubRepositoryStore { + readonly users: GitHubUser[]; + readonly commits: Set; +} + +interface GitHubUser { + readonly id: string; + readonly login: string; + readonly name?: Maybe; + readonly email: string; + readonly avatarUrl: string; +} + +function getUserIdFromNoReplyEmail(email: string | undefined): string | undefined { + const match = email?.match(/^([0-9]+)\+[^@]+@users\.noreply\.github\.com$/); + return match?.[1]; +} + +function compareAvatarQuery(a: AvatarQueryCommit, b: AvatarQueryCommit): number { + // Email + const emailComparison = (a.authorEmail ?? '').localeCompare(b.authorEmail ?? ''); + if (emailComparison !== 0) { + return emailComparison; + } + + // Name + return (a.authorName ?? '').localeCompare(b.authorName ?? ''); +} + +export class GitHubSourceControlHistoryItemDetailsProvider implements SourceControlHistoryItemDetailsProvider { + private _isUserAuthenticated = true; + private readonly _store = new Map(); + private readonly _disposables = new DisposableStore(); + + constructor(private readonly _gitAPI: API, private readonly _logger: LogOutputChannel) { + this._disposables.add(this._gitAPI.onDidCloseRepository(repository => this._onDidCloseRepository(repository))); + + this._disposables.add(authentication.onDidChangeSessions(e => { + if (e.provider.id === 'github') { + this._isUserAuthenticated = true; + } + })); + + this._disposables.add(workspace.onDidChangeConfiguration(e => { + if (!e.affectsConfiguration('github.showAvatar')) { + return; + } + + this._store.clear(); + })); + } + + async provideAvatar(repository: Repository, query: AvatarQuery): Promise | undefined> { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath}.`); + + const config = workspace.getConfiguration('github', repository.rootUri); + const showAvatar = config.get('showAvatar', true) === true; + + if (!this._isUserAuthenticated || !showAvatar) { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled. (${showAvatar === false ? 'setting' : 'auth'})`); + return undefined; + } + + const descriptor = getRepositoryDefaultRemote(repository); + if (!descriptor) { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Repository does not have a GitHub remote.`); + return undefined; + } + + + try { + const logs = { cached: 0, email: 0, github: 0, incomplete: 0 }; + + // Warm up the in-memory cache with the first page + // (100 users) from this list of assignable users + await this._loadAssignableUsers(descriptor); + + const repositoryStore = this._store.get(this._getRepositoryKey(descriptor)); + if (!repositoryStore) { + return undefined; + } + + // Group the query by author + const authorQuery = groupBy(query.commits, compareAvatarQuery); + + const results = new Map(); + await Promise.all(authorQuery.map(async commits => { + if (commits.length === 0) { + return; + } + + // Query the in-memory cache for the user + const avatarUrl = repositoryStore.users.find( + user => user.email === commits[0].authorEmail || user.name === commits[0].authorName)?.avatarUrl; + + // Cache hit + if (avatarUrl) { + // Add avatar for each commit + logs.cached += commits.length; + commits.forEach(({ hash }) => results.set(hash, `${avatarUrl}&s=${query.size}`)); + return; + } + + // Check if any of the commit are being tracked in the list + // of known commits that have incomplte author information + if (commits.some(({ hash }) => repositoryStore.commits.has(hash))) { + commits.forEach(({ hash }) => results.set(hash, undefined)); + return; + } + + // Try to extract the user identifier from GitHub no-reply emails + const userIdFromEmail = getUserIdFromNoReplyEmail(commits[0].authorEmail); + if (userIdFromEmail) { + logs.email += commits.length; + const avatarUrl = getAvatarLink(userIdFromEmail, query.size); + commits.forEach(({ hash }) => results.set(hash, avatarUrl)); + return; + } + + // Get the commit details + const commitAuthor = await this._getCommitAuthor(descriptor, commits[0].hash); + if (!commitAuthor) { + // The commit has incomplete author information, so + // we should not try to query the authors details again + logs.incomplete += commits.length; + for (const { hash } of commits) { + repositoryStore.commits.add(hash); + results.set(hash, undefined); + } + return; + } + + // Save the user to the cache + repositoryStore.users.push(commitAuthor); + + // Add avatar for each commit + logs.github += commits.length; + commits.forEach(({ hash }) => results.set(hash, `${commitAuthor.avatarUrl}&s=${query.size}`)); + })); + + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath} complete: ${JSON.stringify(logs)}.`); + + return results; + } catch (err) { + // A GitHub authentication session could be missing if the user has not yet + // signed in with their GitHub account or they have signed out. Disable the + // avatar resolution until the user signes in with their GitHub account. + if (err instanceof AuthenticationError) { + this._isUserAuthenticated = false; + } + + return undefined; + } + } + + async provideHoverCommands(repository: Repository): Promise { + const url = getRepositoryDefaultRemoteUrl(repository); + if (!url) { + return undefined; + } + + return [{ + title: l10n.t('{0} Open on GitHub', '$(github)'), + tooltip: l10n.t('Open on GitHub'), + command: 'github.openOnGitHub', + arguments: [url] + }]; + } + + async provideMessageLinks(repository: Repository, message: string): Promise { + const descriptor = getRepositoryDefaultRemote(repository); + if (!descriptor) { + return undefined; + } + + return message.replace( + ISSUE_EXPRESSION, + (match, _group1, owner: string | undefined, repo: string | undefined, _group2, number: string | undefined) => { + if (!number || Number.isNaN(parseInt(number))) { + return match; + } + + const label = owner && repo + ? `${owner}/${repo}#${number}` + : `#${number}`; + + owner = owner ?? descriptor.owner; + repo = repo ?? descriptor.repo; + + return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`; + }); + } + + private _onDidCloseRepository(repository: Repository) { + for (const remote of repository.state.remotes) { + if (!remote.fetchUrl) { + continue; + } + + const repository = getRepositoryFromUrl(remote.fetchUrl); + if (!repository) { + continue; + } + + this._store.delete(this._getRepositoryKey(repository)); + } + } + + @sequentialize + private async _loadAssignableUsers(descriptor: { owner: string; repo: string }): Promise { + if (this._store.has(this._getRepositoryKey(descriptor))) { + return; + } + + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Querying assignable user(s) for ${descriptor.owner}/${descriptor.repo}.`); + + try { + const graphql = await getOctokitGraphql(); + const { repository } = await graphql<{ repository: GitHubRepository }>(ASSIGNABLE_USERS_QUERY, descriptor); + + const users: GitHubUser[] = []; + for (const node of repository.assignableUsers.nodes ?? []) { + if (!node) { + continue; + } + + users.push({ + id: node.id, + login: node.login, + name: node.name, + email: node.email, + avatarUrl: node.avatarUrl, + } satisfies GitHubUser); + } + + this._store.set(this._getRepositoryKey(descriptor), { users, commits: new Set() }); + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Successfully queried assignable user(s) for ${descriptor.owner}/${descriptor.repo}: ${users.length} user(s).`); + } catch (err) { + this._logger.warn(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Failed to load assignable user(s) for ${descriptor.owner}/${descriptor.repo}: ${err}`); + throw err; + } + } + + private async _getCommitAuthor(descriptor: { owner: string; repo: string }, commit: string): Promise { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Querying commit author for ${descriptor.owner}/${descriptor.repo}/${commit}.`); + + try { + const graphql = await getOctokitGraphql(); + const { repository } = await graphql<{ repository: GitHubRepository }>(COMMIT_AUTHOR_QUERY, { ...descriptor, commit }); + + const commitAuthor = (repository.object as Commit).author; + if (!commitAuthor?.user?.id || !commitAuthor.user?.login || + !commitAuthor?.name || !commitAuthor?.email || !commitAuthor?.avatarUrl) { + this._logger.info(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Incomplete commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${JSON.stringify(repository.object)}`); + + return undefined; + } + + const user = { + id: commitAuthor.user.id, + login: commitAuthor.user.login, + name: commitAuthor.name, + email: commitAuthor.email, + avatarUrl: commitAuthor.avatarUrl, + } satisfies GitHubUser; + + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Successfully queried commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${user.login}.`); + return user; + } catch (err) { + this._logger.warn(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Failed to get commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${err}`); + throw err; + } + } + + private _getRepositoryKey(descriptor: { owner: string; repo: string }): string { + return `${descriptor.owner}/${descriptor.repo}`; + } + + dispose(): void { + this._disposables.dispose(); + } +} diff --git a/code/extensions/github/src/links.ts b/code/extensions/github/src/links.ts index fe97d172249..fdcac0c5cfd 100644 --- a/code/extensions/github/src/links.ts +++ b/code/extensions/github/src/links.ts @@ -176,6 +176,10 @@ export async function getLink(gitAPI: GitAPI, useSelection: boolean, shouldEnsur return `${uriWithoutFileSegments}${fileSegments}`; } +export function getAvatarLink(userId: string, size: number): string { + return `https://avatars.githubusercontent.com/u/${userId}?s=${size}`; +} + export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') { const repo = getRepositoryFromUrl(url); if (!repo) { diff --git a/code/extensions/github/src/remoteSourceProvider.ts b/code/extensions/github/src/remoteSourceProvider.ts index 0c2ef166832..0d8b9340695 100644 --- a/code/extensions/github/src/remoteSourceProvider.ts +++ b/code/extensions/github/src/remoteSourceProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Uri, env, l10n, workspace } from 'vscode'; +import { Uri, env, l10n, workspace } from 'vscode'; import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base'; import { getOctokit } from './auth'; import { Octokit } from '@octokit/rest'; @@ -136,18 +136,4 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { } }]; } - - async getRemoteSourceControlHistoryItemCommands(url: string): Promise { - const repository = getRepositoryFromUrl(url); - if (!repository) { - return []; - } - - return [{ - title: l10n.t('{0} Open on GitHub', '$(github)'), - tooltip: l10n.t('Open on GitHub'), - command: 'github.openOnGitHub', - arguments: [url] - }]; - } } diff --git a/code/extensions/github/src/typings/git-base.d.ts b/code/extensions/github/src/typings/git-base.d.ts index 37dd2c4229c..d4ec49df47d 100644 --- a/code/extensions/github/src/typings/git-base.d.ts +++ b/code/extensions/github/src/typings/git-base.d.ts @@ -9,7 +9,6 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -82,7 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/code/extensions/github/src/typings/git.d.ts b/code/extensions/github/src/typings/git.d.ts index 7ac67937a47..e600b767c7c 100644 --- a/code/extensions/github/src/typings/git.d.ts +++ b/code/extensions/github/src/typings/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -289,6 +289,23 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface AvatarQueryCommit { + readonly hash: string; + readonly authorName?: string; + readonly authorEmail?: string; +} + +export interface AvatarQuery { + readonly commits: AvatarQueryCommit[]; + readonly size: number; +} + +export interface SourceControlHistoryItemDetailsProvider { + provideAvatar(repository: Repository, query: AvatarQuery): Promise | undefined>; + provideHoverCommands(repository: Repository): Promise; + provideMessageLinks(repository: Repository, message: string): Promise; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -316,6 +333,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; } export interface GitExtension { diff --git a/code/extensions/github/src/util.ts b/code/extensions/github/src/util.ts index 3d8bf4a40be..4c8a032405d 100644 --- a/code/extensions/github/src/util.ts +++ b/code/extensions/github/src/util.ts @@ -23,6 +23,54 @@ export class DisposableStore { } } +function decorate(decorator: (fn: Function, key: string) => Function): Function { + return (_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; + } else if (typeof descriptor.get === 'function') { + fnKey = 'get'; + fn = descriptor.get; + } + + if (!fn || !fnKey) { + throw new Error('not supported'); + } + + descriptor[fnKey] = decorator(fn, key); + }; +} + +function _sequentialize(fn: Function, key: string): Function { + const currentKey = `__$sequence$${key}`; + + return function (this: any, ...args: any[]) { + const currentPromise = this[currentKey] as Promise || Promise.resolve(null); + const run = async () => await fn.apply(this, args); + this[currentKey] = currentPromise.then(run, run); + return this[currentKey]; + }; +} + +export const sequentialize = decorate(_sequentialize); + +export function groupBy(data: ReadonlyArray, compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined = undefined; + for (const element of data.slice(0).sort(compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; +} + export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined { const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url) || /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url); @@ -37,3 +85,24 @@ export function getRepositoryFromQuery(query: string): { owner: string; repo: st export function repositoryHasGitHubRemote(repository: Repository) { return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined); } + +export function getRepositoryDefaultRemoteUrl(repository: Repository): string | undefined { + const remotes = repository.state.remotes + .filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl)); + + if (remotes.length === 0) { + return undefined; + } + + // upstream -> origin -> first + const remote = remotes.find(remote => remote.name === 'upstream') + ?? remotes.find(remote => remote.name === 'origin') + ?? remotes[0]; + + return remote.fetchUrl; +} + +export function getRepositoryDefaultRemote(repository: Repository): { owner: string; repo: string } | undefined { + const fetchUrl = getRepositoryDefaultRemoteUrl(repository); + return fetchUrl ? getRepositoryFromUrl(fetchUrl) : undefined; +} diff --git a/code/extensions/github/tsconfig.json b/code/extensions/github/tsconfig.json index 452c74b8bc4..8435c0d09e8 100644 --- a/code/extensions/github/tsconfig.json +++ b/code/extensions/github/tsconfig.json @@ -11,6 +11,8 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.shareProvider.d.ts" + "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.shareProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.timeline.d.ts" ] } diff --git a/code/extensions/json-language-features/server/package-lock.json b/code/extensions/json-language-features/server/package-lock.json index 88f31145625..384ce045c9c 100644 --- a/code/extensions/json-language-features/server/package-lock.json +++ b/code/extensions/json-language-features/server/package-lock.json @@ -12,7 +12,7 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.2", + "vscode-json-languageservice": "^5.4.3", "vscode-languageserver": "^10.0.0-next.11", "vscode-uri": "^3.0.8" }, @@ -64,9 +64,9 @@ "dev": true }, "node_modules/vscode-json-languageservice": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.2.tgz", - "integrity": "sha512-2qujUseKRbLEwLXvEOFAxaz3y1ssdNCXXi95LRdG8AFchJHSnmI2qCg9ixoYxbJtSehIrXOmkhV87Y9lIivOgQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.3.tgz", + "integrity": "sha512-NVSEQDloP9NYccuqKg4eI46kutZpwucBY4csBB6FCxbM7AZVoBt0oxTItPVA+ZwhnG1bg/fmiBRAwcGJyNQoPA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", diff --git a/code/extensions/json-language-features/server/package.json b/code/extensions/json-language-features/server/package.json index 0253274289a..6dcd82930d2 100644 --- a/code/extensions/json-language-features/server/package.json +++ b/code/extensions/json-language-features/server/package.json @@ -15,7 +15,7 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.2", + "vscode-json-languageservice": "^5.4.3", "vscode-languageserver": "^10.0.0-next.11", "vscode-uri": "^3.0.8" }, @@ -29,6 +29,7 @@ "watch": "npx gulp watch-extension:json-language-features-server", "clean": "../../../node_modules/.bin/rimraf out", "install-service-next": "npm install vscode-json-languageservice@next", + "install-service-latest": "npm install vscode-json-languageservice", "install-service-local": "npm link vscode-json-languageservice", "install-server-next": "npm install vscode-languageserver@next", "install-server-local": "npm link vscode-languageserver-server", diff --git a/code/extensions/markdown-language-features/package-lock.json b/code/extensions/markdown-language-features/package-lock.json index c13d2a007ad..d4c7fec69bf 100644 --- a/code/extensions/markdown-language-features/package-lock.json +++ b/code/extensions/markdown-language-features/package-lock.json @@ -29,7 +29,7 @@ "@types/picomatch": "^2.3.0", "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", - "@vscode/markdown-it-katex": "^1.0.2", + "@vscode/markdown-it-katex": "^1.1.1", "lodash.throttle": "^4.1.1", "vscode-languageserver-types": "^3.17.2", "vscode-markdown-languageservice": "^0.3.0-alpha.3" @@ -252,10 +252,11 @@ "integrity": "sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA==" }, "node_modules/@vscode/markdown-it-katex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.0.2.tgz", - "integrity": "sha512-QY/OnOHPTqc8tQoCoAjVblILX4yE6xGZHKODtiTKqA328OXra+lSpeJO5Ouo9AAvrs9AwcCLz6xvW3zwcsPBQg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.1.tgz", + "integrity": "sha512-3KTlbsRBPJQLE2YmLL7K6nunTlU+W9T5+FjfNdWuIUKgxSS6HWLQHaO3L4MkJi7z7MpIPpY+g4N+cWNBPE/MSA==", "dev": true, + "license": "MIT", "dependencies": { "katex": "^0.16.4" } @@ -289,6 +290,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -408,14 +410,15 @@ } }, "node_modules/katex": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", - "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], + "license": "MIT", "dependencies": { "commander": "^8.3.0" }, diff --git a/code/extensions/markdown-language-features/package.json b/code/extensions/markdown-language-features/package.json index 6d097be5470..c411df23570 100644 --- a/code/extensions/markdown-language-features/package.json +++ b/code/extensions/markdown-language-features/package.json @@ -793,7 +793,7 @@ "@types/picomatch": "^2.3.0", "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", - "@vscode/markdown-it-katex": "^1.0.2", + "@vscode/markdown-it-katex": "^1.1.1", "lodash.throttle": "^4.1.1", "vscode-languageserver-types": "^3.17.2", "vscode-markdown-languageservice": "^0.3.0-alpha.3" diff --git a/code/extensions/markdown-language-features/package.nls.json b/code/extensions/markdown-language-features/package.nls.json index 47f26b05581..be3f6db00f9 100644 --- a/code/extensions/markdown-language-features/package.nls.json +++ b/code/extensions/markdown-language-features/package.nls.json @@ -72,7 +72,7 @@ "configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.", "configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.", "configuration.markdown.copyFiles.destination": { - "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.", + "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.\n- `${unixTime}` — The current Unix timestamp in seconds.", "comment": [ "This setting is use the user drops or pastes image data into the editor. In this case, VS Code automatically creates a new image file in the workspace containing the dropped/pasted image.", "It's easier to explain this setting with an example. For example, let's say the setting value was:", diff --git a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts index dd41b5fa924..26f8186dbc3 100644 --- a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts +++ b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts @@ -96,6 +96,7 @@ function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string // File ['fileName', fileName], // The file name of the dropped file, e.g. `image.png`. ['fileExtName', path.extname(fileName).replace('.', '')], // The extension of the dropped file, e.g. `png`. + ['unixTime', Math.floor(Date.now() / 1000).toString()], // The current Unix timestamp in seconds. ]); return outDest.replaceAll(/(?\\\$)|(?\w+)(?:\/(?(?:\\\/|[^\}\/])+)\/(?(?:\\\/|[^\}\/])*)\/)?\}/g, (match, _escape, name, pattern, replacement, _offset, _str, groups) => { diff --git a/code/extensions/markdown-math/package-lock.json b/code/extensions/markdown-math/package-lock.json index 56d5bd40faa..73ae907e680 100644 --- a/code/extensions/markdown-math/package-lock.json +++ b/code/extensions/markdown-math/package-lock.json @@ -44,18 +44,20 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/katex": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", - "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], + "license": "MIT", "dependencies": { "commander": "^8.3.0" }, diff --git a/code/extensions/markdown-math/package.json b/code/extensions/markdown-math/package.json index 5628816d6c3..6e599ae2a0e 100644 --- a/code/extensions/markdown-math/package.json +++ b/code/extensions/markdown-math/package.json @@ -94,7 +94,9 @@ }, "markdown.math.macros": { "type": "object", - "additionalProperties": { "type": "string" }, + "additionalProperties": { + "type": "string" + }, "default": {}, "description": "%config.markdown.math.macros%", "scope": "resource" @@ -108,9 +110,6 @@ "watch": "npm run build-notebook", "build-notebook": "node ./esbuild" }, - "dependencies": { - "@vscode/markdown-it-katex": "^1.1.1" - }, "devDependencies": { "@types/markdown-it": "^0.0.0", "@types/vscode-notebook-renderer": "^1.60.0" @@ -118,5 +117,8 @@ "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" + }, + "dependencies": { + "@vscode/markdown-it-katex": "^1.1.1" } } diff --git a/code/extensions/media-preview/package.json b/code/extensions/media-preview/package.json index e7ddad4354e..7e2b70293fc 100644 --- a/code/extensions/media-preview/package.json +++ b/code/extensions/media-preview/package.json @@ -50,7 +50,7 @@ "priority": "builtin", "selector": [ { - "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp,avif}" + "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp,avif,svg}" } ] }, diff --git a/code/extensions/microsoft-authentication/package.json b/code/extensions/microsoft-authentication/package.json index 947bfa8d37c..cde66004b4b 100644 --- a/code/extensions/microsoft-authentication/package.json +++ b/code/extensions/microsoft-authentication/package.json @@ -102,7 +102,7 @@ "properties": { "microsoft-authentication.implementation": { "type": "string", - "default": "classic", + "default": "msal", "enum": [ "msal", "classic" @@ -111,10 +111,26 @@ "%microsoft-authentication.implementation.enumDescriptions.msal%", "%microsoft-authentication.implementation.enumDescriptions.classic%" ], - "description": "%microsoft-authentication.implementation.description%", + "markdownDescription": "%microsoft-authentication.implementation.description%", + "tags": [ + "onExP" + ] + }, + "microsoft-authentication.clientIdVersion": { + "type": "string", + "default": "v1", + "enum": [ + "v2", + "v1" + ], + "enumDescriptions": [ + "%microsoft-authentication.clientIdVersion.enumDescriptions.v2%", + "%microsoft-authentication.clientIdVersion.enumDescriptions.v1%" + ], + "markdownDescription": "%microsoft-authentication.clientIdVersion.description%", "tags": [ "onExP", - "preview" + "experimental" ] } } diff --git a/code/extensions/microsoft-authentication/package.nls.json b/code/extensions/microsoft-authentication/package.nls.json index dd33d036abc..ece95ac75c3 100644 --- a/code/extensions/microsoft-authentication/package.nls.json +++ b/code/extensions/microsoft-authentication/package.nls.json @@ -3,9 +3,18 @@ "description": "Microsoft authentication provider", "signIn": "Sign In", "signOut": "Sign Out", - "microsoft-authentication.implementation.description": "The authentication implementation to use for signing in with a Microsoft account.", + "microsoft-authentication.implementation.description": { + "message": "The authentication implementation to use for signing in with a Microsoft account.\n\n*NOTE: The `classic` implementation is deprecated and will be removed, along with this setting, in a future release. If only the `classic` implementation works for you, please [open an issue](command:workbench.action.openIssueReporter) and explain what you are trying to log in to.*", + "comment": [ + "{Locked='[(command:workbench.action.openIssueReporter)]'}", + "The `command:` syntax will turn into a link. Do not translate it." + ] + }, "microsoft-authentication.implementation.enumDescriptions.msal": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", - "microsoft-authentication.implementation.enumDescriptions.classic": "Use the classic authentication flow to sign in with a Microsoft account.", + "microsoft-authentication.implementation.enumDescriptions.classic": "(deprecated) Use the classic authentication flow to sign in with a Microsoft account.", + "microsoft-authentication.clientIdVersion.description": "The version of the Microsoft Account client ID to use for signing in with a Microsoft account. Only change this if you have been asked to. The default is `v1`.", + "microsoft-authentication.clientIdVersion.enumDescriptions.v1": "Use the v1 Microsoft Account client ID to sign in with a Microsoft account.", + "microsoft-authentication.clientIdVersion.enumDescriptions.v2": "Use the v2 Microsoft Account client ID to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/code/extensions/microsoft-authentication/src/common/scopeData.ts b/code/extensions/microsoft-authentication/src/common/scopeData.ts index 4432abfed43..a43f2c431dd 100644 --- a/code/extensions/microsoft-authentication/src/common/scopeData.ts +++ b/code/extensions/microsoft-authentication/src/common/scopeData.ts @@ -3,14 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; -const DEFAULT_TENANT = 'organizations'; +import { workspace } from 'vscode'; + +const DEFAULT_CLIENT_ID_V1 = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const DEFAULT_TENANT_V1 = 'organizations'; +const DEFAULT_CLIENT_ID_V2 = 'c27c220f-ce2f-4904-927d-333864217eeb'; +const DEFAULT_TENANT_V2 = 'common'; const OIDC_SCOPES = ['openid', 'email', 'profile', 'offline_access']; const GRAPH_TACK_ON_SCOPE = 'User.Read'; export class ScopeData { + private readonly _defaultClientId: string; + private readonly _defaultTenant: string; + /** * The full list of scopes including: * * the original scopes passed to the constructor @@ -40,6 +47,14 @@ export class ScopeData { readonly tenant: string; constructor(readonly originalScopes: readonly string[] = []) { + if (workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion') === 'v2') { + this._defaultClientId = DEFAULT_CLIENT_ID_V2; + this._defaultTenant = DEFAULT_TENANT_V2; + } else { + this._defaultClientId = DEFAULT_CLIENT_ID_V1; + this._defaultTenant = DEFAULT_TENANT_V1; + } + const modifiedScopes = [...originalScopes]; modifiedScopes.sort(); this.allScopes = modifiedScopes; @@ -55,7 +70,7 @@ export class ScopeData { return current.split('VSCODE_CLIENT_ID:')[1]; } return prev; - }, undefined) ?? DEFAULT_CLIENT_ID; + }, undefined) ?? this._defaultClientId; } private getTenantId(scopes: string[]) { @@ -64,7 +79,7 @@ export class ScopeData { return current.split('VSCODE_TENANT:')[1]; } return prev; - }, undefined) ?? DEFAULT_TENANT; + }, undefined) ?? this._defaultTenant; } private getScopesToSend(scopes: string[]) { diff --git a/code/extensions/microsoft-authentication/src/extension.ts b/code/extensions/microsoft-authentication/src/extension.ts index 9d04d16408f..bd32a82290a 100644 --- a/code/extensions/microsoft-authentication/src/extension.ts +++ b/code/extensions/microsoft-authentication/src/extension.ts @@ -35,11 +35,11 @@ function shouldUseMsal(expService: IExperimentationService): boolean { } Logger.info('Acquired MSAL enablement value from default. Value: false'); - // If no setting or experiment value is found, default to false - return false; + // If no setting or experiment value is found, default to true + return true; } -let useMsal: boolean | undefined; +let useMsal: boolean | undefined; export async function activate(context: ExtensionContext) { const mainTelemetryReporter = new MicrosoftAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey); const expService = await createExperimentationService( @@ -48,9 +48,14 @@ export async function activate(context: ExtensionContext) { env.uriScheme !== 'vscode', // isPreRelease ); useMsal = shouldUseMsal(expService); + const clientIdVersion = workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion', 'v1'); context.subscriptions.push(workspace.onDidChangeConfiguration(async e => { - if (!e.affectsConfiguration('microsoft-authentication.implementation') || useMsal === shouldUseMsal(expService)) { + if (!e.affectsConfiguration('microsoft-authentication')) { + return; + } + + if (useMsal === shouldUseMsal(expService) && clientIdVersion === workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion', 'v1')) { return; } diff --git a/code/extensions/microsoft-authentication/src/node/flows.ts b/code/extensions/microsoft-authentication/src/node/flows.ts index 3e1d8c513f0..c6f40a943f6 100644 --- a/code/extensions/microsoft-authentication/src/node/flows.ts +++ b/code/extensions/microsoft-authentication/src/node/flows.ts @@ -41,8 +41,8 @@ interface IMsalFlow { class DefaultLoopbackFlow implements IMsalFlow { label = 'default'; options: IMsalFlowOptions = { - supportsRemoteExtensionHost: true, - supportsWebWorkerExtensionHost: true + supportsRemoteExtensionHost: false, + supportsWebWorkerExtensionHost: false }; async trigger({ cachedPca, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { @@ -62,7 +62,7 @@ class DefaultLoopbackFlow implements IMsalFlow { class UrlHandlerFlow implements IMsalFlow { label = 'protocol handler'; options: IMsalFlowOptions = { - supportsRemoteExtensionHost: false, + supportsRemoteExtensionHost: true, supportsWebWorkerExtensionHost: false }; diff --git a/code/extensions/notebook-renderers/src/index.ts b/code/extensions/notebook-renderers/src/index.ts index 8f5fa908cb9..1c81a249d82 100644 --- a/code/extensions/notebook-renderers/src/index.ts +++ b/code/extensions/notebook-renderers/src/index.ts @@ -404,9 +404,9 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, linkifyFilePaths: ctx.settings.linkifyFilePaths }; const content = createOutputContent(outputInfo.id, text, outputOptions); content.classList.add('output-plaintext'); - outputElement.classList.toggle('word-wrap', ctx.settings.outputWordWrap); + content.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { - outputElement.classList.toggle('word-wrap', e.outputWordWrap); + content.classList.toggle('word-wrap', e.outputWordWrap); })); content.classList.toggle('scrollable', outputScrolling); diff --git a/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index dfc7e2b15f8..9dc8f6c845e 100644 --- a/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -152,8 +152,12 @@ suite('Notebook builtin output renderer', () => { const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); assert.ok(outputElement.classList.contains('remove-padding'), `Padding should be removed for scrollable outputs ${outputElement.classList}`); - assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), - `output content classList should contain word-wrap and scrollable ${inserted.classList}`); + if (mimeType === 'text/plain') { + assert.ok(inserted.classList.contains('word-wrap'), `Word wrap should be enabled for text/plain ${outputElement.classList}`); + } else { + assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), + `output content classList should contain word-wrap and scrollable ${inserted.classList}`); + } assert.ok(inserted.innerHTML.indexOf('>content -1, `Content was not added to output element: ${outputElement.innerHTML}`); }); diff --git a/code/extensions/package-lock.json b/code/extensions/package-lock.json index aa142087652..0210240ff55 100644 --- a/code/extensions/package-lock.json +++ b/code/extensions/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "typescript": "^5.7.2" + "typescript": "^5.7.3" }, "devDependencies": { "@parcel/watcher": "2.5.0", @@ -905,9 +905,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/code/extensions/package.json b/code/extensions/package.json index fc61c8cfd7e..db30f5712c3 100644 --- a/code/extensions/package.json +++ b/code/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.7.2" + "typescript": "^5.7.3" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/code/extensions/terminal-suggest/.gitignore b/code/extensions/terminal-suggest/.gitignore new file mode 100644 index 00000000000..76b510c710f --- /dev/null +++ b/code/extensions/terminal-suggest/.gitignore @@ -0,0 +1 @@ +third_party/ diff --git a/code/extensions/terminal-suggest/README.md b/code/extensions/terminal-suggest/README.md index adaffc410ac..cd1c1f4afa5 100644 --- a/code/extensions/terminal-suggest/README.md +++ b/code/extensions/terminal-suggest/README.md @@ -1,6 +1,6 @@ # Terminal Suggestions -**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. To enable the completions from this extension, set `terminal.integrated.suggest.enabled` and `terminal.integrated.suggest.enableExtensionCompletions` to `true`. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. To enable the completions from this extension, set `terminal.integrated.suggest.enabled` to `true`. ## Features diff --git a/code/extensions/terminal-suggest/package.json b/code/extensions/terminal-suggest/package.json index 611ef00ed4c..82e488dd9f5 100644 --- a/code/extensions/terminal-suggest/package.json +++ b/code/extensions/terminal-suggest/package.json @@ -14,7 +14,9 @@ "Other" ], "enabledApiProposals": [ - "terminalCompletionProvider" + "terminalCompletionProvider", + "terminalShellEnv", + "terminalShellType" ], "scripts": { "compile": "npx gulp compile-extension:terminal-suggest", diff --git a/code/extensions/terminal-suggest/scripts/clone-fig.ps1 b/code/extensions/terminal-suggest/scripts/clone-fig.ps1 new file mode 100644 index 00000000000..11ec13e560e --- /dev/null +++ b/code/extensions/terminal-suggest/scripts/clone-fig.ps1 @@ -0,0 +1 @@ +git clone https://github.com/withfig/autocomplete third_party/autocomplete diff --git a/code/extensions/terminal-suggest/scripts/clone-fig.sh b/code/extensions/terminal-suggest/scripts/clone-fig.sh new file mode 100644 index 00000000000..11ec13e560e --- /dev/null +++ b/code/extensions/terminal-suggest/scripts/clone-fig.sh @@ -0,0 +1 @@ +git clone https://github.com/withfig/autocomplete third_party/autocomplete diff --git a/code/extensions/terminal-suggest/scripts/update-specs.js b/code/extensions/terminal-suggest/scripts/update-specs.js new file mode 100644 index 00000000000..3573c6664d6 --- /dev/null +++ b/code/extensions/terminal-suggest/scripts/update-specs.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +const fs = require('fs'); +const path = require('path'); + +const upstreamSpecs = require('../out/constants.js').upstreamSpecs; + +const extRoot = path.resolve(path.join(__dirname, '..')); +for (const spec of upstreamSpecs) { + const source = path.join(extRoot, `third_party/autocomplete/src/${spec}.ts`); + const destination = path.join(extRoot, `src/completions/upstream/${spec}.ts`); + fs.copyFileSync(source, destination); +} diff --git a/code/extensions/terminal-suggest/scripts/update-specs.ps1 b/code/extensions/terminal-suggest/scripts/update-specs.ps1 new file mode 100644 index 00000000000..0f129190379 --- /dev/null +++ b/code/extensions/terminal-suggest/scripts/update-specs.ps1 @@ -0,0 +1 @@ +node "$PSScriptRoot/update-specs.js" diff --git a/code/extensions/terminal-suggest/scripts/update-specs.sh b/code/extensions/terminal-suggest/scripts/update-specs.sh new file mode 100644 index 00000000000..4efd5bbf20d --- /dev/null +++ b/code/extensions/terminal-suggest/scripts/update-specs.sh @@ -0,0 +1 @@ +node ./update-specs.js diff --git a/code/extensions/terminal-suggest/src/completions/code-insiders.ts b/code/extensions/terminal-suggest/src/completions/code-insiders.ts index 8014c97e8c1..89d01dc536f 100644 --- a/code/extensions/terminal-suggest/src/completions/code-insiders.ts +++ b/code/extensions/terminal-suggest/src/completions/code-insiders.ts @@ -7,6 +7,7 @@ import code from './code'; const codeInsidersCompletionSpec: Fig.Spec = { ...code, name: 'code-insiders', + description: 'Visual Studio Code Insiders', }; export default codeInsidersCompletionSpec; diff --git a/code/extensions/terminal-suggest/src/completions/index.d.ts b/code/extensions/terminal-suggest/src/completions/index.d.ts index e56afd803ca..de76233ecb4 100644 --- a/code/extensions/terminal-suggest/src/completions/index.d.ts +++ b/code/extensions/terminal-suggest/src/completions/index.d.ts @@ -666,7 +666,7 @@ declare namespace Fig { * }, * ``` */ - generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; + generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; /** * Generating a spec can be expensive, but due to current guarantees they are not cached. diff --git a/code/extensions/terminal-suggest/src/completions/upstream/echo.ts b/code/extensions/terminal-suggest/src/completions/upstream/echo.ts new file mode 100644 index 00000000000..8ca21b858b3 --- /dev/null +++ b/code/extensions/terminal-suggest/src/completions/upstream/echo.ts @@ -0,0 +1,42 @@ +const environmentVariableGenerator: Fig.Generator = { + custom: async (tokens, _, context) => { + if (tokens.length < 3 || tokens[tokens.length - 1].startsWith("$")) { + return Object.keys(context.environmentVariables).map((suggestion) => ({ + name: `$${suggestion}`, + type: "arg", + description: "Environment Variable", + })); + } else { + return []; + } + }, + trigger: "$", +}; + +const completionSpec: Fig.Spec = { + name: "echo", + description: "Write arguments to the standard output", + args: { + name: "string", + isVariadic: true, + optionsCanBreakVariadicArg: false, + suggestCurrentToken: true, + generators: environmentVariableGenerator, + }, + options: [ + { + name: "-n", + description: "Do not print the trailing newline character", + }, + { + name: "-e", + description: "Interpret escape sequences", + }, + { + name: "-E", + description: "Disable escape sequences", + }, + ], +}; + +export default completionSpec; diff --git a/code/extensions/terminal-suggest/src/completions/upstream/ls.ts b/code/extensions/terminal-suggest/src/completions/upstream/ls.ts new file mode 100644 index 00000000000..91afc0b75ed --- /dev/null +++ b/code/extensions/terminal-suggest/src/completions/upstream/ls.ts @@ -0,0 +1,227 @@ +const completionSpec: Fig.Spec = { + name: "ls", + description: "List directory contents", + args: { + isVariadic: true, + template: ["filepaths", "folders"], + filterStrategy: "fuzzy", + }, + options: [ + { + name: "-@", + description: + "Display extended attribute keys and sizes in long (-l) output", + }, + { + name: "-1", + description: + "(The numeric digit ``one''.) Force output to be one entry per line. This is the default when output is not to a terminal", + }, + { + name: "-A", + description: + "List all entries except for . and ... Always set for the super-user", + }, + { + name: "-a", + description: "Include directory entries whose names begin with a dot (.)", + }, + { + name: "-B", + description: + "Force printing of non-printable characters (as defined by ctype(3) and current locale settings) in file names as xxx, where xxx is the numeric value of the character in octal", + }, + { + name: "-b", + description: "As -B, but use C escape codes whenever possible", + }, + { + name: "-C", + description: + "Force multi-column output; this is the default when output is to a terminal", + }, + { + name: "-c", + description: + "Use time when file status was last changed for sorting (-t) or long printing (-l)", + }, + { + name: "-d", + description: + "Directories are listed as plain files (not searched recursively)", + }, + { + name: "-e", + description: + "Print the Access Control List (ACL) associated with the file, if present, in long (-l) output", + }, + { + name: "-F", + description: + "Display a slash (/) immediately after each pathname that is a directory, an asterisk (*) after each that is executable, an at sign (@) after each symbolic link, an equals sign (=) after each socket, a percent sign (%) after each whiteout, and a vertical bar (|) after each that is a FIFO", + }, + { + name: "-f", + description: "Output is not sorted. This option turns on the -a option", + }, + { + name: "-G", + description: + "Enable colorized output. This option is equivalent to defining CLICOLOR in the environment. (See below.)", + }, + { + name: "-g", + description: + "This option is only available for compatibility with POSIX; it is used to display the group name in the long (-l) format output (the owner name is suppressed)", + }, + { + name: "-H", + description: + "Symbolic links on the command line are followed. This option is assumed if none of the -F, -d, or -l options are specified", + }, + { + name: "-h", + description: + "When used with the -l option, use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and Petabyte in order to reduce the number of digits to three or less using base 2 for sizes", + }, + { + name: "-i", + description: + "For each file, print the file's file serial number (inode number)", + }, + { + name: "-k", + description: + "If the -s option is specified, print the file size allocation in kilobytes, not blocks. This option overrides the environment variable BLOCKSIZE", + }, + { + name: "-L", + description: + "Follow all symbolic links to final target and list the file or directory the link references rather than the link itself. This option cancels the -P option", + }, + { + name: "-l", + description: + "(The lowercase letter ``ell''.) List in long format. (See below.) A total sum for all the file sizes is output on a line before the long listing", + }, + { + name: "-m", + description: + "Stream output format; list files across the page, separated by commas", + }, + { + name: "-n", + description: + "Display user and group IDs numerically, rather than converting to a user or group name in a long (-l) output. This option turns on the -l option", + }, + { + name: "-O", + description: "Include the file flags in a long (-l) output", + }, + { name: "-o", description: "List in long format, but omit the group id" }, + { + name: "-P", + description: + "If argument is a symbolic link, list the link itself rather than the object the link references. This option cancels the -H and -L options", + }, + { + name: "-p", + description: + "Write a slash (`/') after each filename if that file is a directory", + }, + { + name: "-q", + description: + "Force printing of non-graphic characters in file names as the character `?'; this is the default when output is to a terminal", + }, + { name: "-R", description: "Recursively list subdirectories encountered" }, + { + name: "-r", + description: + "Reverse the order of the sort to get reverse lexicographical order or the oldest entries first (or largest files last, if combined with sort by size", + }, + { name: "-S", description: "Sort files by size" }, + { + name: "-s", + description: + "Display the number of file system blocks actually used by each file, in units of 512 bytes, where partial units are rounded up to the next integer value. If the output is to a terminal, a total sum for all the file sizes is output on a line before the listing. The environment variable BLOCKSIZE overrides the unit size of 512 bytes", + }, + { + name: "-T", + description: + "When used with the -l (lowercase letter ``ell'') option, display complete time information for the file, including month, day, hour, minute, second, and year", + }, + { + name: "-t", + description: + "Sort by time modified (most recently modified first) before sorting the operands by lexicographical order", + }, + { + name: "-u", + description: + "Use time of last access, instead of last modification of the file for sorting (-t) or long printing (-l)", + }, + { + name: "-U", + description: + "Use time of file creation, instead of last modification for sorting (-t) or long output (-l)", + }, + { + name: "-v", + description: + "Force unedited printing of non-graphic characters; this is the default when output is not to a terminal", + }, + { + name: "-W", + description: "Display whiteouts when scanning directories. (-S) flag)", + }, + { + name: "-w", + description: + "Force raw printing of non-printable characters. This is the default when output is not to a terminal", + }, + { + name: "-x", + description: + "The same as -C, except that the multi-column output is produced with entries sorted across, rather than down, the columns", + }, + { + name: "-%", + description: + "Distinguish dataless files and directories with a '%' character in long (-l) output, and don't materialize dataless directories when listing them", + }, + { + name: "-,", + description: `When the -l option is set, print file sizes grouped and separated by thousands using the non-monetary separator returned +by localeconv(3), typically a comma or period. If no locale is set, or the locale does not have a non-monetary separator, this +option has no effect. This option is not defined in IEEE Std 1003.1-2001 (“POSIX.1”)`, + dependsOn: ["-l"], + }, + { + name: "--color", + description: `Output colored escape sequences based on when, which may be set to either always, auto, or never`, + requiresSeparator: true, + args: { + name: "when", + suggestions: [ + { + name: ["always", "yes", "force"], + description: "Will make ls always output color", + }, + { + name: "auto", + description: + "Will make ls output escape sequences based on termcap(5), but only if stdout is a tty and either the -G flag is specified or the COLORTERM environment variable is set and not empty", + }, + { + name: ["never", "no", "none"], + description: + "Will disable color regardless of environment variables", + }, + ], + }, + }, + ], +}; + +export default completionSpec; diff --git a/code/extensions/terminal-suggest/src/completions/upstream/mkdir.ts b/code/extensions/terminal-suggest/src/completions/upstream/mkdir.ts new file mode 100644 index 00000000000..90a6530c3bd --- /dev/null +++ b/code/extensions/terminal-suggest/src/completions/upstream/mkdir.ts @@ -0,0 +1,37 @@ +const completionSpec: Fig.Spec = { + name: "mkdir", + description: "Make directories", + args: { + name: "directory name", + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: ["-m", "--mode"], + description: "Set file mode (as in chmod), not a=rwx - umask", + args: { name: "MODE" }, + }, + { + name: ["-p", "--parents"], + description: "No error if existing, make parent directories as needed", + }, + { + name: ["-v", "--verbose"], + description: "Print a message for each created directory", + }, + { + name: ["-Z", "--context"], + description: + "Set the SELinux security context of each created directory to CTX", + args: { name: "CTX" }, + }, + { name: "--help", description: "Display this help and exit" }, + { + name: "--version", + description: "Output version information and exit", + }, + ], +}; + +export default completionSpec; diff --git a/code/extensions/terminal-suggest/src/completions/upstream/rm.ts b/code/extensions/terminal-suggest/src/completions/upstream/rm.ts new file mode 100644 index 00000000000..7b52f909527 --- /dev/null +++ b/code/extensions/terminal-suggest/src/completions/upstream/rm.ts @@ -0,0 +1,43 @@ +const completionSpec: Fig.Spec = { + name: "rm", + description: "Remove directory entries", + args: { + isVariadic: true, + template: ["folders", "filepaths"], + }, + + options: [ + { + name: ["-r", "-R"], + description: + "Recursive. Attempt to remove the file hierarchy rooted in each file argument", + isDangerous: true, + }, + { + name: "-P", + description: "Overwrite regular files before deleting them", + isDangerous: true, + }, + { + name: "-d", + description: + "Attempt to remove directories as well as other types of files", + }, + { + name: "-f", + description: + "⚠️ Attempt to remove the files without prompting for confirmation", + isDangerous: true, + }, + { + name: "-i", + description: "Request confirmation before attempting to remove each file", + }, + { + name: "-v", + description: "Be verbose when deleting files", + }, + ], +}; + +export default completionSpec; diff --git a/code/extensions/terminal-suggest/src/completions/upstream/rmdir.ts b/code/extensions/terminal-suggest/src/completions/upstream/rmdir.ts new file mode 100644 index 00000000000..92790e75d0d --- /dev/null +++ b/code/extensions/terminal-suggest/src/completions/upstream/rmdir.ts @@ -0,0 +1,18 @@ +const completionSpec: Fig.Spec = { + name: "rmdir", + description: "Remove directories", + args: { + isVariadic: true, + template: "folders", + }, + + options: [ + { + name: "-p", + description: "Remove each directory of path", + isDangerous: true, + }, + ], +}; + +export default completionSpec; diff --git a/code/extensions/terminal-suggest/src/completions/upstream/touch.ts b/code/extensions/terminal-suggest/src/completions/upstream/touch.ts new file mode 100644 index 00000000000..45208878313 --- /dev/null +++ b/code/extensions/terminal-suggest/src/completions/upstream/touch.ts @@ -0,0 +1,59 @@ +const completionSpec: Fig.Spec = { + name: "touch", + description: "Change file access and modification times", + args: { + name: "file", + isVariadic: true, + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: "-A", + description: + "Adjust the access and modification time stamps for the file by the specified value", + args: { + name: "time", + description: "[-][[hh]mm]SS", + }, + }, + { name: "-a", description: "Change the access time of the file" }, + { + name: "-c", + description: "Do not create the file if it does not exist", + }, + { + name: "-f", + description: + "Attempt to force the update, even if the file permissions do not currently permit it", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the times of the link itself rather than the file that the link points to", + }, + { + name: "-m", + description: "Change the modification time of the file", + }, + { + name: "-r", + description: + "Use the access and modifications times from the specified file instead of the current time of day", + args: { + name: "file", + }, + }, + { + name: "-t", + description: + "Change the access and modification times to the specified time instead of the current time of day", + args: { + name: "timestamp", + description: "[[CC]YY]MMDDhhmm[.SS]", + }, + }, + ], +}; + +export default completionSpec; diff --git a/code/extensions/terminal-suggest/src/constants.ts b/code/extensions/terminal-suggest/src/constants.ts new file mode 100644 index 00000000000..37d08189da6 --- /dev/null +++ b/code/extensions/terminal-suggest/src/constants.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. + *--------------------------------------------------------------------------------------------*/ + +export const upstreamSpecs = [ + 'echo', + 'ls', + 'mkdir', + 'rm', + 'rmdir', + 'touch', +]; diff --git a/code/extensions/terminal-suggest/src/helpers/executable.ts b/code/extensions/terminal-suggest/src/helpers/executable.ts new file mode 100644 index 00000000000..cd8fce4f8dc --- /dev/null +++ b/code/extensions/terminal-suggest/src/helpers/executable.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { osIsWindows } from './os'; +import * as fs from 'fs/promises'; + +export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined } | undefined): Promise { + if (osIsWindows()) { + const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions); + return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; + } + try { + const stats = await fs.stat(filePath); + // On macOS/Linux, check if the executable bit is set + return (stats.mode & 0o100) !== 0; + } catch (error) { + // If the file does not exist or cannot be accessed, it's not executable + return false; + } +} + + +function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): string[] { + const resolvedWindowsExecutableExtensions: string[] = windowsDefaultExecutableExtensions; + const excluded = new Set(); + if (configuredWindowsExecutableExtensions) { + for (const [key, value] of Object.entries(configuredWindowsExecutableExtensions)) { + if (value === true) { + resolvedWindowsExecutableExtensions.push(key); + } else { + excluded.add(key); + } + } + } + return Array.from(new Set(resolvedWindowsExecutableExtensions)).filter(ext => !excluded.has(ext)); +} + +export const windowsDefaultExecutableExtensions: string[] = [ + '.exe', // Executable file + '.bat', // Batch file + '.cmd', // Command script + '.com', // Command file + + '.msi', // Windows Installer package + + '.ps1', // PowerShell script + + '.vbs', // VBScript file + '.js', // JScript file + '.jar', // Java Archive (requires Java runtime) + '.py', // Python script (requires Python interpreter) + '.rb', // Ruby script (requires Ruby interpreter) + '.pl', // Perl script (requires Perl interpreter) + '.sh', // Shell script (via WSL or third-party tools) +]; diff --git a/code/extensions/terminal-suggest/src/helpers/os.ts b/code/extensions/terminal-suggest/src/helpers/os.ts new file mode 100644 index 00000000000..3c2a3498719 --- /dev/null +++ b/code/extensions/terminal-suggest/src/helpers/os.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; + +export function osIsWindows(): boolean { + return os.platform() === 'win32'; +} diff --git a/code/extensions/terminal-suggest/src/terminalSuggestMain.ts b/code/extensions/terminal-suggest/src/terminalSuggestMain.ts index 13ff032037a..6824fbb2e24 100644 --- a/code/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/code/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -3,49 +3,79 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as os from 'os'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { ExecOptionsWithStringEncoding, execSync } from 'child_process'; -import codeInsidersCompletionSpec from './completions/code-insiders'; +import { exec, ExecOptionsWithStringEncoding, execSync } from 'child_process'; +import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; +import codeInsidersCompletionSpec from './completions/code-insiders'; +import { osIsWindows } from './helpers/os'; +import { isExecutable } from './helpers/executable'; -let cachedAvailableCommands: Set | undefined; -const cachedBuiltinCommands: Map = new Map(); +const enum PwshCommandType { + Alias = 1 +} -export const availableSpecs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; +const isWindows = osIsWindows(); +let cachedAvailableCommandsPath: string | undefined; +let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; +const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; +let cachedAvailableCommands: Set | undefined; +const cachedBuiltinCommands: Map = new Map(); + +export const availableSpecs: Fig.Spec[] = [ + cdSpec, + codeInsidersCompletionSpec, + codeCompletionSpec, +]; +for (const spec of upstreamSpecs) { + availableSpecs.push(require(`./completions/upstream/${spec}`).default); +} -function getBuiltinCommands(shell: string): string[] | undefined { +async function getBuiltinCommands(shellType: TerminalShellType, existingCommands?: Set): Promise { try { - const shellType = path.basename(shell, path.extname(shell)); const cachedCommands = cachedBuiltinCommands.get(shellType); if (cachedCommands) { return cachedCommands; } - const filter = (cmd: string) => cmd; + const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); + const shell = getShell(shellType); + if (!shell) { + return; + } const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; let commands: string[] | undefined; switch (shellType) { - case 'bash': { + case TerminalShellType.Bash: { const bashOutput = execSync('compgen -b', options); commands = bashOutput.split('\n').filter(filter); break; } - case 'zsh': { + case TerminalShellType.Zsh: { const zshOutput = execSync('printf "%s\\n" ${(k)builtins}', options); commands = zshOutput.split('\n').filter(filter); break; } - case 'fish': { + case TerminalShellType.Fish: { // TODO: Ghost text in the command line prevents completions from working ATM for fish const fishOutput = execSync('functions -n', options); commands = fishOutput.split(', ').filter(filter); break; } - case 'pwsh': { - // TODO: Select `CommandType, DisplayName` and map to a rich type with kind and detail - const output = execSync('Get-Command -All | Select-Object Name | ConvertTo-Json', options); + case TerminalShellType.PowerShell: { + const output = await new Promise((resolve, reject) => { + exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { + ...options, + maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size + }, (error, stdout) => { + if (error) { + reject(error); + return; + } + resolve(stdout); + }); + }); let json: any; try { json = JSON.parse(output); @@ -53,12 +83,30 @@ function getBuiltinCommands(shell: string): string[] | undefined { console.error('Error parsing pwsh output:', e); return []; } - commands = (json as any[]).map(e => e.Name); - break; + const commandResources = (json as any[]).map(e => { + switch (e.CommandType) { + case PwshCommandType.Alias: { + return { + label: e.Name, + detail: e.DisplayName, + }; + } + default: { + return { + label: e.Name, + detail: e.Definition, + }; + } + } + }); + cachedBuiltinCommands.set(shellType, commandResources); + return commandResources; } } - cachedBuiltinCommands.set(shellType, commands); - return commands; + + const commandResources = commands?.map(command => ({ label: command })); + cachedBuiltinCommands.set(shellType, commandResources); + return commandResources; } catch (error) { console.error('Error fetching builtin commands:', error); @@ -74,31 +122,47 @@ export async function activate(context: vscode.ExtensionContext) { return; } - // TODO: Leverage shellType when available https://github.com/microsoft/vscode/issues/230165 - const shellPath = ('shellPath' in terminal.creationOptions ? terminal.creationOptions.shellPath : undefined) ?? vscode.env.shell; - if (!shellPath) { + const shellType: TerminalShellType | undefined = 'shellType' in terminal.state ? terminal.state.shellType as TerminalShellType : undefined; + if (!shellType) { return; } - const commandsInPath = await getCommandsInPath(); - const builtinCommands = getBuiltinCommands(shellPath); - if (!commandsInPath || !builtinCommands) { + const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); + const builtinCommands = await getBuiltinCommands(shellType, commandsInPath?.labels) ?? []; + if (!commandsInPath?.completionResources) { return; } - const commands = [...commandsInPath, ...builtinCommands]; + const commands = [...commandsInPath.completionResources, ...builtinCommands]; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); - + const pathSeparator = isWindows ? '\\' : '/'; const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); + if (terminal.shellIntegration?.env) { + const homeDirCompletion = result.items.find(i => i.label === '~'); + if (homeDirCompletion && terminal.shellIntegration.env.HOME) { + homeDirCompletion.documentation = getFriendlyResourcePath(vscode.Uri.file(terminal.shellIntegration.env.HOME), pathSeparator, vscode.TerminalCompletionItemKind.Folder); + homeDirCompletion.kind = vscode.TerminalCompletionItemKind.Folder; + } + } + if (result.cwd && (result.filesRequested || result.foldersRequested)) { - // const cwd = resolveCwdFromPrefix(prefix, terminal.shellIntegration?.cwd) ?? terminal.shellIntegration?.cwd; - return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: osIsWindows() ? '\\' : '/' }); + return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: isWindows ? '\\' : '/', env: terminal.shellIntegration?.env }); } return result.items; } }, '/', '\\')); -} + if (isWindows) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); + cachedAvailableCommands = undefined; + cachedAvailableCommandsPath = undefined; + } + })); + } +} /** * Adjusts the current working directory based on a given prefix if it is a folder. @@ -114,7 +178,7 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // Get the nearest folder path from the prefix. This ignores everything after the `/` as // they are what triggers changes in the directory. let lastSlashIndex: number; - if (osIsWindows()) { + if (isWindows) { // TODO: This support is very basic, ideally the slashes supported would depend upon the // shell type. For example git bash under Windows does not allow using \ as a path // separator. @@ -129,6 +193,7 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // Resolve the absolute path of the prefix const resolvedPath = path.resolve(currentCwd?.fsPath, relativeFolder); + const stat = await fs.stat(resolvedPath); // Check if the resolved path exists and is a directory @@ -142,7 +207,12 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // If the prefix is not a folder, return the current cwd return currentCwd; } - +function getDescription(spec: Fig.Spec): string { + if ('description' in spec) { + return spec.description ?? ''; + } + return ''; +} function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] | undefined { if (typeof spec === 'string') { @@ -157,57 +227,60 @@ function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] return spec.name; } -function createCompletionItem(cursorPosition: number, prefix: string, label: string, description?: string, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { +function createCompletionItem(cursorPosition: number, prefix: string, commandResource: ICompletionResource, detail?: string, documentation?: string | vscode.MarkdownString, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { const endsWithSpace = prefix.endsWith(' '); const lastWord = endsWithSpace ? '' : prefix.split(' ').at(-1) ?? ''; return { - label, - detail: description ?? '', + label: commandResource.label, + detail: detail ?? commandResource.detail ?? '', + documentation, replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length > 0 ? lastWord.length : cursorPosition, + replacementLength: lastWord.length, kind: kind ?? vscode.TerminalCompletionItemKind.Method }; } -async function isExecutable(filePath: string): Promise { - // Windows doesn't have the concept of an executable bit and running any - // file is possible. We considered using $PATHEXT here but since it's mostly - // there for legacy reasons and it would be easier and more intuitive to add - // a setting if needed instead. - if (osIsWindows()) { - return true; - } - try { - const stats = await fs.stat(filePath); - // On macOS/Linux, check if the executable bit is set - return (stats.mode & 0o100) !== 0; - } catch (error) { - // If the file does not exist or cannot be accessed, it's not executable - return false; - } +interface ICompletionResource { + label: string; + detail?: string; } - -async function getCommandsInPath(): Promise | undefined> { - if (cachedAvailableCommands) { - return cachedAvailableCommands; +async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + const labels: Set = new Set(); + let pathValue: string | undefined; + if (isWindows) { + const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); + if (caseSensitivePathKey) { + pathValue = env[caseSensitivePathKey]; + } + } else { + pathValue = env.PATH; } - const paths = osIsWindows() ? process.env.PATH?.split(';') : process.env.PATH?.split(':'); - if (!paths) { + if (pathValue === undefined) { return; } - const pathSeparator = osIsWindows() ? '\\' : '/'; - const executables = new Set(); + + // Check cache + if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { + return { completionResources: cachedAvailableCommands, labels }; + } + + // Extract executables from PATH + const paths = pathValue.split(isWindows ? ';' : ':'); + const pathSeparator = isWindows ? '\\' : '/'; + const executables = new Set(); for (const path of paths) { try { const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); if (!dirExists) { continue; } - const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path)); - + const fileResource = vscode.Uri.file(path); + const files = await vscode.workspace.fs.readDirectory(fileResource); for (const [file, fileType] of files) { - if (fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(path + pathSeparator + file)) { - executables.add(file); + const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { + executables.add({ label: file, detail: formattedPath }); + labels.add(file); } } } catch (e) { @@ -216,7 +289,7 @@ async function getCommandsInPath(): Promise | undefined> { } } cachedAvailableCommands = executables; - return executables; + return { completionResources: executables, labels }; } function getPrefix(commandLine: string, cursorPosition: number): string { @@ -246,21 +319,34 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: string[], prefix: string, shellIntegrationCwd?: vscode.Uri, token?: vscode.CancellationToken): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { +export async function getCompletionItemsFromSpecs( + specs: Fig.Spec[], + terminalContext: { commandLine: string; cursorPosition: number }, + availableCommands: ICompletionResource[], + prefix: string, + shellIntegrationCwd?: vscode.Uri, + token?: vscode.CancellationToken +): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { const items: vscode.TerminalCompletionItem[] = []; let filesRequested = false; let foldersRequested = false; + const firstCommand = getFirstCommand(terminalContext.commandLine); + const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + for (const spec of specs) { const specLabels = getLabel(spec); + if (!specLabels) { continue; } + for (const specLabel of specLabels) { - if (!availableCommands.includes(specLabel) || (token && token?.isCancellationRequested)) { + const availableCommand = availableCommands.find(command => command.label === specLabel); + if (!availableCommand || (token && token.isCancellationRequested)) { continue; } - // + if ( // If the prompt is empty !terminalContext.commandLine @@ -268,81 +354,43 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon || !!firstCommand && specLabel.startsWith(firstCommand) ) { // push it to the completion items - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, specLabel)); + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); } + if (!terminalContext.commandLine.startsWith(specLabel)) { // the spec label is not the first word in the command line, so do not provide options or args continue; } - const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); - if ('options' in spec && spec.options) { - for (const option of spec.options) { - const optionLabels = getLabel(option); - if (!optionLabels) { - continue; - } - for (const optionLabel of optionLabels) { - if (!items.find(i => i.label === optionLabel) && optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, vscode.TerminalCompletionItemKind.Flag)); - } - const expectedText = `${specLabel} ${optionLabel} `; - if (!precedingText.includes(expectedText)) { - continue; - } - const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); - const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); - const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext); - if (!argsCompletions) { - continue; - } - const argCompletions = argsCompletions.items; - foldersRequested = foldersRequested || argsCompletions.foldersRequested; - filesRequested = filesRequested || argsCompletions.filesRequested; - let cwd: vscode.Uri | undefined; - if (shellIntegrationCwd && (filesRequested || foldersRequested)) { - cwd = await resolveCwdFromPrefix(prefix, shellIntegrationCwd) ?? shellIntegrationCwd; - } - return { items: argCompletions, filesRequested, foldersRequested, cwd }; - } - } + + const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText); + if (argsCompletionResult) { + items.push(...argsCompletionResult.items); + filesRequested ||= argsCompletionResult.filesRequested; + foldersRequested ||= argsCompletionResult.foldersRequested; } - if ('args' in spec && asArray(spec.args)) { - const expectedText = `${specLabel} `; - if (!precedingText.includes(expectedText)) { - continue; - } - const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); - const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); - const argsCompletions = getCompletionItemsFromArgs(spec.args, currentPrefix, terminalContext); - if (!argsCompletions) { - continue; - } - items.push(...argsCompletions.items); - filesRequested = filesRequested || argsCompletions.filesRequested; - foldersRequested = foldersRequested || argsCompletions.foldersRequested; + + const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); + if (optionsCompletionResult) { + items.push(...optionsCompletionResult.items); + filesRequested ||= optionsCompletionResult.filesRequested; + foldersRequested ||= optionsCompletionResult.foldersRequested; } } } const shouldShowResourceCompletions = - ( - // If the command line is empty - terminalContext.commandLine.trim().length === 0 - // or no completions are found and the prefix is empty - || !items?.length - // or all of the items are '.' or '..' IE file paths - || items.length && items.every(i => ['.', '..'].includes(i.label)) - ) - // and neither files nor folders are going to be requested (for a specific spec's argument) - && (!filesRequested && !foldersRequested); + (!terminalContext.commandLine.trim() || !items.length) && + !filesRequested && + !foldersRequested; const shouldShowCommands = !terminalContext.commandLine.substring(0, terminalContext.cursorPosition).trimStart().includes(' '); - if (shouldShowCommands && (filesRequested === foldersRequested)) { + + if (shouldShowCommands && !filesRequested && !foldersRequested) { // Include builitin/available commands in the results - const labels = new Set(items.map(i => i.label)); + const labels = new Set(items.map((i) => i.label)); for (const command of availableCommands) { - if (!labels.has(command)) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); + if (!labels.has(command.label)) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command, command.detail)); } } } @@ -351,13 +399,91 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon filesRequested = true; foldersRequested = true; } + let cwd: vscode.Uri | undefined; if (shellIntegrationCwd && (filesRequested || foldersRequested)) { cwd = await resolveCwdFromPrefix(prefix, shellIntegrationCwd) ?? shellIntegrationCwd; } + return { items, filesRequested, foldersRequested, cwd }; } +function handleArguments(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { + let args; + if ('args' in spec && spec.args && asArray(spec.args)) { + args = asArray(spec.args); + } + const expectedText = `${specLabel} `; + + if (!precedingText.includes(expectedText)) { + return; + } + + const currentPrefix = precedingText.slice(precedingText.lastIndexOf(expectedText) + expectedText.length); + const argsCompletions = getCompletionItemsFromArgs(args, currentPrefix, terminalContext); + + if (!argsCompletions) { + return; + } + + return argsCompletions; +} + +function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string, prefix: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { + let options; + if ('options' in spec && spec.options) { + options = spec.options; + } + if (!options) { + return; + } + + const optionItems: vscode.TerminalCompletionItem[] = []; + + for (const option of options) { + const optionLabels = getLabel(option); + + if (!optionLabels) { + continue; + } + + for (const optionLabel of optionLabels) { + if ( + // Already includes this option + optionItems.find((i) => i.label === optionLabel) + ) { + continue; + } + + optionItems.push( + createCompletionItem( + terminalContext.cursorPosition, + prefix, + { label: optionLabel }, + option.description, + undefined, + vscode.TerminalCompletionItemKind.Flag + ) + ); + + const expectedText = `${specLabel} ${optionLabel} `; + if (!precedingText.includes(expectedText)) { + continue; + } + + const currentPrefix = precedingText.slice(precedingText.lastIndexOf(expectedText) + expectedText.length); + const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext); + + if (argsCompletions) { + return { items: argsCompletions.items, filesRequested: argsCompletions.filesRequested, foldersRequested: argsCompletions.foldersRequested }; + } + } + } + + return { items: optionItems, filesRequested: false, foldersRequested: false }; +} + + function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined, currentPrefix: string, terminalContext: { commandLine: string; cursorPosition: number }): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { if (!args) { return; @@ -396,7 +522,7 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined } if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { const description = typeof suggestion !== 'string' ? suggestion.description : ''; - items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', suggestionLabel, description, vscode.TerminalCompletionItemKind.Argument)); + items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', { label: suggestionLabel }, description, undefined, vscode.TerminalCompletionItemKind.Argument)); } } } @@ -408,9 +534,7 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined return { items, filesRequested, foldersRequested }; } -function osIsWindows(): boolean { - return os.platform() === 'win32'; -} + function getFirstCommand(commandLine: string): string | undefined { const wordsOnLine = commandLine.split(' '); @@ -422,3 +546,52 @@ function getFirstCommand(commandLine: string): string | undefined { } return firstCommand; } + +function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { + let path = uri.fsPath; + // Ensure drive is capitalized on Windows + if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { + path = `${path[0].toUpperCase()}:${path.slice(2)}`; + } + if (kind === vscode.TerminalCompletionItemKind.Folder) { + if (!path.endsWith(pathSeparator)) { + path += pathSeparator; + } + } + return path; +} + +// TODO: remove once API is finalized +export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 +} + + +function getShell(shellType: TerminalShellType): string | undefined { + switch (shellType) { + case TerminalShellType.Bash: + return 'bash'; + case TerminalShellType.Fish: + return 'fish'; + case TerminalShellType.Zsh: + return 'zsh'; + case TerminalShellType.PowerShell: + return 'pwsh'; + default: { + return undefined; + } + } +} + diff --git a/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 70694606d99..d72996b8f70 100644 --- a/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -62,9 +62,9 @@ function createCodeTestSpecs(executable: string): ITestSpec2[] { { input: `${executable} --merge ./file1 ./file2 ./base |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, { input: `${executable} --goto |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, { input: `${executable} --user-data-dir |`, expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: `${executable} --profile |` }, - { input: `${executable} --install-extension |` }, - { input: `${executable} --uninstall-extension |` }, + { input: `${executable} --profile |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: `${executable} --install-extension |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: `${executable} --uninstall-extension |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, { input: `${executable} --log |`, expectedCompletions: logOptions }, { input: `${executable} --sync |`, expectedCompletions: syncOptions }, { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testCwd } }, @@ -150,7 +150,7 @@ suite('Terminal Suggest', () => { const prefix = commandLine.slice(0, cursorPosition).split(' ').at(-1) || ''; const filesRequested = testSpec.expectedResourceRequests?.type === 'files' || testSpec.expectedResourceRequests?.type === 'both'; const foldersRequested = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; - const result = await getCompletionItemsFromSpecs(completionSpecs, { commandLine, cursorPosition }, availableCommands, prefix, testCwd); + const result = await getCompletionItemsFromSpecs(completionSpecs, { commandLine, cursorPosition }, availableCommands.map(c => { return { label: c }; }), prefix, testCwd); deepStrictEqual(result.items.map(i => i.label).sort(), (testSpec.expectedCompletions ?? []).sort()); strictEqual(result.filesRequested, filesRequested); strictEqual(result.foldersRequested, foldersRequested); diff --git a/code/extensions/terminal-suggest/tsconfig.json b/code/extensions/terminal-suggest/tsconfig.json index 151a29616bb..f3d3aa73975 100644 --- a/code/extensions/terminal-suggest/tsconfig.json +++ b/code/extensions/terminal-suggest/tsconfig.json @@ -11,11 +11,16 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true, + + // Needed to suppress warnings in upstream completions + "noImplicitReturns": false, + "noUnusedParameters": false }, "include": [ "src/**/*", "src/completions/index.d.ts", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts" + "../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts" ] } diff --git a/code/extensions/typescript-language-features/package.json b/code/extensions/typescript-language-features/package.json index ac6d0487a4c..5a1f52a9b25 100644 --- a/code/extensions/typescript-language-features/package.json +++ b/code/extensions/typescript-language-features/package.json @@ -10,7 +10,6 @@ "enabledApiProposals": [ "workspaceTrust", "multiDocumentHighlightProvider", - "mappedEditsProvider", "codeActionAI", "codeActionRanges", "editorHoverVerbosityLevel" @@ -531,6 +530,12 @@ "description": "%format.indentSwitchCase%", "scope": "resource" }, + "javascript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "scope": "resource" + }, "javascript.validate.enable": { "type": "boolean", "default": true, diff --git a/code/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts b/code/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts deleted file mode 100644 index 06ce5557b6c..00000000000 --- a/code/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts +++ /dev/null @@ -1,66 +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 vscode from 'vscode'; -import { API } from '../tsServer/api'; -import { FileSpan } from '../tsServer/protocol/protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireMinVersion } from './util/dependentRegistration'; -import { Range, WorkspaceEdit } from '../typeConverters'; -import { DocumentSelector } from '../configuration/documentSelector'; - -class TsMappedEditsProvider implements vscode.MappedEditsProvider { - constructor( - private readonly client: ITypeScriptServiceClient - ) { } - - async provideMappedEdits(document: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, token: vscode.CancellationToken): Promise { - if (!this.isEnabled()) { - return; - } - - const file = this.client.toOpenTsFilePath(document); - if (!file) { - return; - } - - const response = await this.client.execute('mapCode', { - file, - mapping: { - contents: codeBlocks, - focusLocations: context.documents.map(documents => { - return documents.flatMap((contextItem): FileSpan[] => { - const file = this.client.toTsFilePath(contextItem.uri); - if (!file) { - return []; - } - return contextItem.ranges.map((range): FileSpan => ({ file, ...Range.toTextSpan(range) })); - }); - }), - } - }, token); - if (response.type !== 'response' || !response.body) { - return; - } - - return WorkspaceEdit.fromFileCodeEdits(this.client, response.body); - } - - private isEnabled(): boolean { - return vscode.workspace.getConfiguration('typescript').get('experimental.mappedCodeEdits.enabled', false); - } -} - -export function register( - selector: DocumentSelector, - client: ITypeScriptServiceClient, -) { - return conditionalRegistration([ - requireMinVersion(client, API.v540) - ], () => { - const provider = new TsMappedEditsProvider(client); - return vscode.chat.registerMappedEditsProvider(selector.semantic, provider); - }); -} diff --git a/code/extensions/typescript-language-features/src/languageProvider.ts b/code/extensions/typescript-language-features/src/languageProvider.ts index feb47f09683..f2d52f21fa7 100644 --- a/code/extensions/typescript-language-features/src/languageProvider.ts +++ b/code/extensions/typescript-language-features/src/languageProvider.ts @@ -79,7 +79,6 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/inlayHints').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager, this.telemetryReporter))), import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))), import('./languageFeatures/linkedEditing').then(provider => this._register(provider.register(selector, this.client))), - import('./languageFeatures/mappedCodeEditProvider').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))), import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))), import('./languageFeatures/refactor').then(provider => this._register(provider.register(selector, this.client, cachedNavTreeResponse, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))), diff --git a/code/extensions/typescript-language-features/src/lazyClientHost.ts b/code/extensions/typescript-language-features/src/lazyClientHost.ts index be832b3e169..6d2bb34604f 100644 --- a/code/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/code/extensions/typescript-language-features/src/lazyClientHost.ts @@ -15,7 +15,7 @@ import { ActiveJsTsEditorTracker } from './ui/activeJsTsEditorTracker'; import ManagedFileContextManager from './ui/managedFileContext'; import { ServiceConfigurationProvider } from './configuration/configuration'; import * as fileSchemes from './configuration/fileSchemes'; -import { standardLanguageDescriptions } from './configuration/languageDescription'; +import { standardLanguageDescriptions, isJsConfigOrTsConfigFileName } from './configuration/languageDescription'; import { Lazy, lazy } from './utils/lazy'; import { Logger } from './logging/logger'; import { PluginManager } from './tsServer/plugins'; @@ -97,6 +97,6 @@ function isSupportedDocument( supportedLanguage: readonly string[], document: vscode.TextDocument ): boolean { - return supportedLanguage.indexOf(document.languageId) >= 0 + return (supportedLanguage.indexOf(document.languageId) >= 0 || isJsConfigOrTsConfigFileName(document.fileName)) && !fileSchemes.disabledSchemes.has(document.uri.scheme); } diff --git a/code/extensions/typescript-language-features/tsconfig.json b/code/extensions/typescript-language-features/tsconfig.json index 0b599aecc60..8ac3a8735b1 100644 --- a/code/extensions/typescript-language-features/tsconfig.json +++ b/code/extensions/typescript-language-features/tsconfig.json @@ -13,7 +13,6 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", - "../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", diff --git a/code/extensions/vscode-api-tests/package.json b/code/extensions/vscode-api-tests/package.json index 60b35b4dd9e..d67a7cf4dac 100644 --- a/code/extensions/vscode-api-tests/package.json +++ b/code/extensions/vscode-api-tests/package.json @@ -28,7 +28,6 @@ "fsChunks", "interactive", "languageStatusText", - "mappedEditsProvider", "nativeWindowHandle", "notebookCellExecutionState", "notebookDeprecated", diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts deleted file mode 100644 index 2fa19a2bd0a..00000000000 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts +++ /dev/null @@ -1,105 +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 'path'; -import * as vscode from 'vscode'; -import assert from 'assert'; - -suite('mapped edits provider', () => { - - test('mapped edits does not provide edits for unregistered langs', async function () { - - const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts')); - - const tsDocFilter = [{ language: 'json' }]; - - const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, { - provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => { - - assert((context as any).selections.length === 1); // context.selections is for backward compat - assert(context.documents.length === 1); - - const edit = new vscode.WorkspaceEdit(); - const text = codeBlocks.join('\n//----\n'); - edit.replace(uri, context.documents[0][0].ranges[0], text); - return edit; - } - }); - await vscode.workspace.openTextDocument(uri); - const result = await vscode.commands.executeCommand>( - 'vscode.executeMappedEditsProvider', - uri, - [ - '// hello', - `function foo() {\n\treturn 1;\n}`, - ], - { - documents: [ - [ - { - uri, - version: 1, - ranges: [ - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)) - ] - } - ] - ] - } - ); - r1.dispose(); - - assert(result === null, 'returned null'); - }); - - test('mapped edits provides a single edit replacing the selection', async function () { - - const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts')); - - const tsDocFilter = [{ language: 'typescript' }]; - - const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, { - provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => { - - const edit = new vscode.WorkspaceEdit(); - const text = codeBlocks.join('\n//----\n'); - edit.replace(uri, context.documents[0][0].ranges[0], text); - return edit; - } - }); - - await vscode.workspace.openTextDocument(uri); - const result = await vscode.commands.executeCommand>( - 'vscode.executeMappedEditsProvider', - uri, - [ - '// hello', - `function foo() {\n\treturn 1;\n}`, - ], - { - documents: [ - [ - { - uri, - version: 1, - ranges: [ - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)) - ] - } - ] - ] - } - ); - r1.dispose(); - - assert(result, 'non null response'); - const edits = result.get(uri); - assert(edits.length === 1); - assert(edits[0].range.start.line === 0); - assert(edits[0].range.start.character === 0); - assert(edits[0].range.end.line === 1); - assert(edits[0].range.end.character === 0); - }); -}); diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts index ccda85b442a..7845e3113cc 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -5,12 +5,13 @@ import * as https from 'https'; import 'mocha'; -import { assertNoRpc, delay } from '../utils'; +import { assertNoRpc } from '../utils'; import { pki } from 'node-forge'; import { AddressInfo } from 'net'; import { resetCaches } from '@vscode/proxy-agent'; import * as vscode from 'vscode'; -import { middleware, Straightforward } from 'straightforward'; +import { Straightforward, Middleware, RequestContext, ConnectContext, isRequest, isConnect } from 'straightforward'; +import assert from 'assert'; (vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => { @@ -84,7 +85,8 @@ import { middleware, Straightforward } from 'straightforward'; const sf = new Straightforward(); let authEnabled = false; - const auth = middleware.auth({ user, pass }); + const authOpts: AuthOpts = { user, pass }; + const auth = middlewareAuth(authOpts); sf.onConnect.use(async (context, next) => { if (authEnabled) { return auth(context, next); @@ -105,8 +107,9 @@ import { middleware, Straightforward } from 'straightforward'; await proxyListen; const proxyPort = (sf.server.address() as AddressInfo).port; - await vscode.workspace.getConfiguration().update('integration-test.http.proxy', `PROXY 127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); - await delay(1000); // Wait for the configuration change to propagate. + const change = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', `http://127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); + await change; await new Promise((resolve, reject) => { https.get(url, res => { if (res.statusCode === 418) { @@ -130,8 +133,7 @@ import { middleware, Straightforward } from 'straightforward'; .on('error', reject); }); - await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', `${user}:${pass}`, vscode.ConfigurationTarget.Global); - await delay(1000); // Wait for the configuration change to propagate. + authOpts.realm = Buffer.from(JSON.stringify({ username: user, password: pass })).toString('base64'); await new Promise((resolve, reject) => { https.get(url, res => { if (res.statusCode === 204) { @@ -144,8 +146,125 @@ import { middleware, Straightforward } from 'straightforward'; }); } finally { sf.close(); - await vscode.workspace.getConfiguration().update('integration-test.http.proxy', undefined, vscode.ConfigurationTarget.Global); + const change = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global); + await change; await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', undefined, vscode.ConfigurationTarget.Global); } }); + + (vscode.env.remoteName ? test : test.skip)('separate local / remote proxy settings', async () => { + // Assumes test resolver runs with `--use-host-proxy`. + const localProxy = 'http://localhost:1234'; + const remoteProxy = 'http://localhost:4321'; + + const actualLocalProxy1 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p1 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', localProxy, vscode.ConfigurationTarget.Global); + await p1; + const actualLocalProxy2 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p2 = waitForConfigChange('http.useLocalProxyConfiguration'); + await vscode.workspace.getConfiguration().update('http.useLocalProxyConfiguration', false, vscode.ConfigurationTarget.Global); + await p2; + const actualRemoteProxy1 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p3 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', remoteProxy, vscode.ConfigurationTarget.Global); + await p3; + const actualRemoteProxy2 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p4 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global); + await p4; + const actualRemoteProxy3 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p5 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.useLocalProxyConfiguration', true, vscode.ConfigurationTarget.Global); + await p5; + const actualLocalProxy3 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p6 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global); + await p6; + const actualLocalProxy4 = vscode.workspace.getConfiguration().get('http.proxy'); + + assert.strictEqual(actualLocalProxy1, ''); + assert.strictEqual(actualLocalProxy2, localProxy); + assert.strictEqual(actualRemoteProxy1, ''); + assert.strictEqual(actualRemoteProxy2, remoteProxy); + assert.strictEqual(actualRemoteProxy3, ''); + assert.strictEqual(actualLocalProxy3, localProxy); + assert.strictEqual(actualLocalProxy4, ''); + }); + + function waitForConfigChange(key: string) { + return new Promise(resolve => { + const s = vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + s.dispose(); + resolve(); + } + }); + }); + } }); + +// Added 'realm'. From MIT licensed https://github.com/berstend/straightforward/blob/84a4cb88024cffce37a05870da7d9d0aba7dcca8/src/middleware/auth.ts + +export interface AuthOpts { + realm?: string; + user?: string; + pass?: string; + dynamic?: boolean; +} + +export interface RequestAdditionsAuth { + locals: { proxyUser: string; proxyPass: string }; +} + +/** + * Authenticate an incoming proxy request + * Supports static `user` and `pass` or `dynamic`, + * in which case `ctx.req.locals` will be populated with `proxyUser` and `proxyPass` + * This middleware supports both onRequest and onConnect + */ +export const middlewareAuth = + (opts: AuthOpts): Middleware< + RequestContext | ConnectContext + > => + async (ctx, next) => { + const { realm, user, pass, dynamic } = opts; + const sendAuthRequired = () => { + const realmStr = realm ? ` realm="${realm}"` : ''; + if (isRequest(ctx)) { + ctx.res.writeHead(407, { 'Proxy-Authenticate': `Basic${realmStr}` }); + ctx.res.end(); + } else if (isConnect(ctx)) { + ctx.clientSocket.end( + 'HTTP/1.1 407\r\n' + `Proxy-Authenticate: basic${realmStr}\r\n` + '\r\n' + ); + } + }; + const proxyAuth = ctx.req.headers['proxy-authorization']; + if (!proxyAuth) { + return sendAuthRequired(); + } + const [proxyUser, proxyPass] = Buffer.from( + proxyAuth.replace('Basic ', ''), + 'base64' + ) + .toString() + .split(':'); + + if (!dynamic && !!(!!user && !!pass)) { + if (user !== proxyUser || pass !== proxyPass) { + return sendAuthRequired(); + } + } + ctx.req.locals.proxyUser = proxyUser; + ctx.req.locals.proxyPass = proxyPass; + + return next(); + }; diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts index f10edc51b3b..cbe58948d86 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { ExtensionContext, extensions } from 'vscode'; +import { ExtensionContext, extensions, Uri } from 'vscode'; suite('vscode API - globalState / workspaceState', () => { @@ -16,7 +16,7 @@ suite('vscode API - globalState / workspaceState', () => { extensionContext = (global as any).testExtensionContext; }); - test('state', async () => { + test('state basics', async () => { for (const state of [extensionContext.globalState, extensionContext.workspaceState]) { let keys = state.keys(); assert.strictEqual(keys.length, 0); @@ -42,4 +42,39 @@ suite('vscode API - globalState / workspaceState', () => { assert.strictEqual(res, 'default'); } }); + + test('state - handling of objects', async () => { + for (const state of [extensionContext.globalState, extensionContext.workspaceState]) { + const keys = state.keys(); + assert.strictEqual(keys.length, 0); + + state.update('state.test.date', new Date()); + const date = state.get('state.test.date'); + assert.ok(typeof date === 'string'); + + state.update('state.test.regex', /foo/); + const regex = state.get('state.test.regex'); + assert.ok(typeof regex === 'object' && !(regex instanceof RegExp)); + + class Foo { } + state.update('state.test.class', new Foo()); + const clazz = state.get('state.test.class'); + assert.ok(typeof clazz === 'object' && !(clazz instanceof Foo)); + + const cycle: any = { self: null }; + cycle.self = cycle; + assert.throws(() => state.update('state.test.cycle', cycle)); + + const uriIn = Uri.parse('/foo/bar'); + state.update('state.test.uri', uriIn); + const uriOut = state.get('state.test.uri') as Uri; + assert.ok(uriIn.toString() === Uri.from(uriOut).toString()); + + state.update('state.test.null', null); + assert.strictEqual(state.get('state.test.null'), null); + + state.update('state.test.undefined', undefined); + assert.strictEqual(state.get('state.test.undefined'), undefined); + } + }); }); diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index db48c2593e0..8f094091f96 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -87,6 +87,23 @@ import { assertNoRpc } from '../utils'; await closeTerminalAsync(terminal); }); + if (platform() === 'darwin' || platform() === 'linux') { + // TODO: Enable when this is enabled in stable, otherwise it will break the stable product builds only + test.skip('Test if env is set', async () => { + const { shellIntegration } = await createTerminalAndWaitForShellIntegration(); + await new Promise(r => { + disposables.push(window.onDidChangeTerminalShellIntegration(e => { + if (e.shellIntegration.env) { + r(); + } + })); + }); + ok(shellIntegration.env); + ok(shellIntegration.env.PATH); + ok(shellIntegration.env.PATH.length > 0, 'env.PATH should have a length greater than 0'); + }); + } + test('execution events should fire in order when a command runs', async () => { const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const events: string[] = []; diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 7411d9c094a..9c1c097aab2 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -26,6 +26,8 @@ import { assertNoRpc, poll } from '../utils'; await config.update('gpuAcceleration', 'off', ConfigurationTarget.Global); // Disable env var relaunch for tests to prevent terminals relaunching themselves await config.update('environmentChangesRelaunch', false, ConfigurationTarget.Global); + // Disable local echo in case it causes any problems in remote tests + await config.update('localEchoEnabled', "off", ConfigurationTarget.Global); await config.update('shellIntegration.enabled', false); }); @@ -231,19 +233,40 @@ import { assertNoRpc, poll } from '../utils'; }); }); - test('onDidChangeTerminalState should fire after writing to a terminal', async () => { + test('onDidChangeTerminalState should fire with isInteractedWith after writing to a terminal', async () => { const terminal = window.createTerminal(); - deepStrictEqual(terminal.state, { isInteractedWith: false }); + strictEqual(terminal.state.isInteractedWith, false); const eventState = await new Promise(r => { disposables.push(window.onDidChangeTerminalState(e => { - if (e === terminal) { + if (e === terminal && e.state.isInteractedWith) { r(e.state); } })); terminal.sendText('test'); }); - deepStrictEqual(eventState, { isInteractedWith: true }); - deepStrictEqual(terminal.state, { isInteractedWith: true }); + strictEqual(eventState.isInteractedWith, true); + await new Promise(r => { + disposables.push(window.onDidCloseTerminal(t => { + if (t === terminal) { + r(); + } + })); + terminal.dispose(); + }); + }); + + test('onDidChangeTerminalState should fire with shellType when created', async () => { + const terminal = window.createTerminal(); + if (terminal.state.shellType) { + return; + } + await new Promise(r => { + disposables.push(window.onDidChangeTerminalState(e => { + if (e === terminal && e.state.shellType) { + r(); + } + })); + }); await new Promise(r => { disposables.push(window.onDidCloseTerminal(t => { if (t === terminal) { @@ -740,19 +763,19 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); - terminal.sendText('$env:C'); - terminal.sendText('echo $C'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B $C"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('b1~b2~'), 'b1~b2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~c2~c1'), '~c2~c1 should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('b1~b2~'), 'b1~b2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~c2~c1'), '~c2~c1 should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { @@ -785,19 +808,19 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); - terminal.sendText('$env:C'); - terminal.sendText('echo $C'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B $C"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~c2~'), '~c2~ should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~c2~'), '~c2~ should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { @@ -829,16 +852,18 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~b1~'), '~b1~ should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b1~'), '~b1~ should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { @@ -870,16 +895,18 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index b871093df39..90baf6d2870 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -924,7 +924,8 @@ suite('vscode API - workspace', () => { } }); - test('workspace.applyEdit drops the TextEdit if there is a RenameFile later #77735 (with opened editor)', async function () { + // TODO: below test is flaky and commented out, see https://github.com/microsoft/vscode/issues/238837 + test.skip('workspace.applyEdit drops the TextEdit if there is a RenameFile later #77735 (with opened editor)', async function () { await test77735(true); }); diff --git a/code/package-lock.json b/code/package-lock.json index c594c19d073..4d509908faf 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -17,8 +17,8 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.28.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/proxy-agent": "0.28.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -27,15 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "js-yaml": "^4.1.0", @@ -51,7 +52,7 @@ "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "ws": "8.2.3", "yauzl": "^3.0.0", "yazl": "^2.4.3" @@ -60,7 +61,7 @@ "che-code": "out/vs/server/main.js" }, "devDependencies": { - "@playwright/test": "^1.46.1", + "@playwright/test": "1.46.1", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", @@ -2024,6 +2025,36 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2838,10 +2869,11 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.9.tgz", - "integrity": "sha512-4q2PXRvUvr3bF+LsfrifmUZgSPmCNcUZo6SbEAZgArIChchkezaxLoIeQMJe/z3CCKStvaVKpBXLxN3Z8lQjFQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", + "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", @@ -3125,25 +3157,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/test-web/node_modules/playwright": { - "version": "1.47.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", - "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.47.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, "node_modules/@vscode/tree-sitter-wasm": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", @@ -3430,30 +3443,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.68", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", - "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", + "version": "0.2.0-beta.79", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.79.tgz", + "integrity": "sha512-zpwf43fzBG01fA8E75JQ/jsm/85bHCtFMOcURNAEbCoj0RSpZaq4rE5cPoy49IhYnctPZdYNxEU/+PzgtsSQ6Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", - "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.96.tgz", + "integrity": "sha512-LDA03uA5gTBfFeEIUdHXoCpxGHuPIke03aSXZc6cllRRp9jCaOK2kbWwTOoXfzRvjreKK6pxD0vJcepwdKaNcw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", - "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", + "version": "0.10.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.96.tgz", + "integrity": "sha512-uI86EDxCab345YR/Piwzs3R82DptKp/PC+sMKSX24hehhvWVAkpZus0toGhYwLZxF1r0U6yw4Oq0aCEzW362dQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3463,55 +3476,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" + } + }, + "node_modules/@xterm/addon-progress": { + "version": "0.2.0-beta.2", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.2.tgz", + "integrity": "sha512-5BcRexNJanTMG2N9TjZpDcaemDqQuvNkGN8HK3509+QNmqBF9sinJXSBWWNwn+o/f4OheBtBmlmC5mDanh4kig==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", - "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", + "version": "0.16.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.96.tgz", + "integrity": "sha512-AEEXqxkT6OgDOII4SxQJFKn7NhM7PnFeMLxQwrRQzZyrtFPg5peCesA07xHYvYzh0aKQ1PVvgycfrPgbPEeQDQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", - "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", + "version": "0.14.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.96.tgz", + "integrity": "sha512-+00J2K3WsbMPy02UlehNumen//Opmr5B0MBrS4KFqhRwVaO8RD0hMPfr8IuhY3YqPaVny4HMOU88yaWtscxl7A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", - "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.96.tgz", + "integrity": "sha512-jBUSErJtrf6Jhz3itT5JXLeIIl5BkJ1ii8/YdzZQMh9602ZLYvqwS+7ih2FzYYjk0pZ+E1y4sYbT5FpBlVOM5w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", - "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", + "version": "0.19.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.96.tgz", + "integrity": "sha512-iAno2CMYRBHasr5aE3I7WN6DZ0mlHsEAIYAhvdhH+be1YxX4/DceBAtN0Ak7nFcQ70ZbtYzs1aNLTVKtli2TKA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.85.tgz", - "integrity": "sha512-GzIULvPPz+I9tvdpMM5k5GeURz7bHBQyVQyZE6d6UUSyVVVd0NWgJXwF25t62y9cZQt1Bfp8i+1eCJ7p2+8ZSQ==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.96.tgz", + "integrity": "sha512-lLumuO7sQbPNIxQkXa3CxwQWNlO4C5XF80VRgQ4PzjUcwPWGkGtkJJG9jcucHAKymQnH6AokCW171mcveU8cgg==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", - "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.96.tgz", + "integrity": "sha512-XdOZyAaqOW67J3kJGJDgVb5skTD2nDQLmBICyhQ0cEqThTKrX5CzB11RswG6rZE8dI+nDE+pc93NjAMXSrQgFg==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { @@ -13676,12 +13698,13 @@ } }, "node_modules/playwright": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", - "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.46.1" + "playwright-core": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -13707,10 +13730,11 @@ } }, "node_modules/playwright/node_modules/playwright-core": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", - "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -18065,9 +18089,10 @@ } }, "node_modules/vscode-textmate": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.1.0.tgz", - "integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.8", diff --git a/code/package.json b/code/package.json index 85f6ec30f52..7d67f352c30 100644 --- a/code/package.json +++ b/code/package.json @@ -1,7 +1,7 @@ { "name": "che-code", "version": "1.97.0", - "distro": "baf3347105f6082ae1df942fd1c052cd06f7a7f0", + "distro": "c504aa66a94bcd910e273dbbf66b2c3a9f0343f8", "author": { "name": "Microsoft Corporation" }, @@ -75,8 +75,8 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.28.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/proxy-agent": "0.28.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -85,15 +85,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -108,14 +109,14 @@ "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "yauzl": "^3.0.0", "yazl": "^2.4.3", "ws": "8.2.3", "js-yaml": "^4.1.0" }, "devDependencies": { - "@playwright/test": "^1.46.1", + "@playwright/test": "1.46.1", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", diff --git a/code/product.json b/code/product.json index 485eb51c852..75181b97368 100644 --- a/code/product.json +++ b/code/product.json @@ -51,8 +51,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.96.0", - "sha256": "278cd8b129c133d834a8105d0e0699f2f940c5c159fa5c821c7b9a4f7ffd3581", + "version": "1.97.0", + "sha256": "8442c0578c80976ef17031868fd6dcc42206be75b0ca1b0e9b5d6ff965cfdf23", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/code/remote/package-lock.json b/code/remote/package-lock.json index 90e6455ad02..72f55ad0745 100644 --- a/code/remote/package-lock.json +++ b/code/remote/package-lock.json @@ -14,22 +14,23 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.28.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/proxy-agent": "0.28.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -42,7 +43,7 @@ "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "ws": "8.2.3", "yauzl": "^3.0.0", "yazl": "^2.4.3" @@ -529,7 +530,6 @@ "version": "0.28.0", "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.28.0.tgz", "integrity": "sha512-7rYF8ju0dP/ASpjjnuOCvzRosGLoKz0WOyNohREUskRdrvMEnYuEUXy84lHlH+4+MD8CZZjw2SUzhjHaJK1hxg==", - "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", "agent-base": "^7.0.1", @@ -544,10 +544,11 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.9.tgz", - "integrity": "sha512-4q2PXRvUvr3bF+LsfrifmUZgSPmCNcUZo6SbEAZgArIChchkezaxLoIeQMJe/z3CCKStvaVKpBXLxN3Z8lQjFQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", + "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", @@ -629,30 +630,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.68", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", - "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", + "version": "0.2.0-beta.79", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.79.tgz", + "integrity": "sha512-zpwf43fzBG01fA8E75JQ/jsm/85bHCtFMOcURNAEbCoj0RSpZaq4rE5cPoy49IhYnctPZdYNxEU/+PzgtsSQ6Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", - "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.96.tgz", + "integrity": "sha512-LDA03uA5gTBfFeEIUdHXoCpxGHuPIke03aSXZc6cllRRp9jCaOK2kbWwTOoXfzRvjreKK6pxD0vJcepwdKaNcw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", - "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", + "version": "0.10.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.96.tgz", + "integrity": "sha512-uI86EDxCab345YR/Piwzs3R82DptKp/PC+sMKSX24hehhvWVAkpZus0toGhYwLZxF1r0U6yw4Oq0aCEzW362dQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -662,55 +663,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" + } + }, + "node_modules/@xterm/addon-progress": { + "version": "0.2.0-beta.2", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.2.tgz", + "integrity": "sha512-5BcRexNJanTMG2N9TjZpDcaemDqQuvNkGN8HK3509+QNmqBF9sinJXSBWWNwn+o/f4OheBtBmlmC5mDanh4kig==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", - "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", + "version": "0.16.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.96.tgz", + "integrity": "sha512-AEEXqxkT6OgDOII4SxQJFKn7NhM7PnFeMLxQwrRQzZyrtFPg5peCesA07xHYvYzh0aKQ1PVvgycfrPgbPEeQDQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", - "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", + "version": "0.14.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.96.tgz", + "integrity": "sha512-+00J2K3WsbMPy02UlehNumen//Opmr5B0MBrS4KFqhRwVaO8RD0hMPfr8IuhY3YqPaVny4HMOU88yaWtscxl7A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", - "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.96.tgz", + "integrity": "sha512-jBUSErJtrf6Jhz3itT5JXLeIIl5BkJ1ii8/YdzZQMh9602ZLYvqwS+7ih2FzYYjk0pZ+E1y4sYbT5FpBlVOM5w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", - "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", + "version": "0.19.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.96.tgz", + "integrity": "sha512-iAno2CMYRBHasr5aE3I7WN6DZ0mlHsEAIYAhvdhH+be1YxX4/DceBAtN0Ak7nFcQ70ZbtYzs1aNLTVKtli2TKA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.85.tgz", - "integrity": "sha512-GzIULvPPz+I9tvdpMM5k5GeURz7bHBQyVQyZE6d6UUSyVVVd0NWgJXwF25t62y9cZQt1Bfp8i+1eCJ7p2+8ZSQ==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.96.tgz", + "integrity": "sha512-lLumuO7sQbPNIxQkXa3CxwQWNlO4C5XF80VRgQ4PzjUcwPWGkGtkJJG9jcucHAKymQnH6AokCW171mcveU8cgg==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", - "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.96.tgz", + "integrity": "sha512-XdOZyAaqOW67J3kJGJDgVb5skTD2nDQLmBICyhQ0cEqThTKrX5CzB11RswG6rZE8dI+nDE+pc93NjAMXSrQgFg==", "license": "MIT" }, "node_modules/agent-base": { @@ -2467,10 +2477,9 @@ "license": "Unlicense" }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", - "license": "MIT", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", "engines": { "node": ">=18.17" } @@ -2540,9 +2549,10 @@ } }, "node_modules/vscode-textmate": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.1.0.tgz", - "integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "license": "MIT" }, "node_modules/which": { "version": "2.0.2", diff --git a/code/remote/package.json b/code/remote/package.json index ef6cff7e750..b8530eca649 100644 --- a/code/remote/package.json +++ b/code/remote/package.json @@ -9,22 +9,23 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.28.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/proxy-agent": "0.28.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -36,7 +37,7 @@ "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "yauzl": "^3.0.0", "yazl": "^2.4.3", "ws": "8.2.3", diff --git a/code/remote/web/package-lock.json b/code/remote/web/package-lock.json index 628d03e23cb..45f7724805d 100644 --- a/code/remote/web/package-lock.json +++ b/code/remote/web/package-lock.json @@ -13,18 +13,19 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", - "vscode-textmate": "9.1.0" + "vscode-textmate": "9.2.0" } }, "node_modules/@microsoft/1ds-core-js": { @@ -89,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.68", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", - "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", + "version": "0.2.0-beta.79", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.79.tgz", + "integrity": "sha512-zpwf43fzBG01fA8E75JQ/jsm/85bHCtFMOcURNAEbCoj0RSpZaq4rE5cPoy49IhYnctPZdYNxEU/+PzgtsSQ6Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", - "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.96.tgz", + "integrity": "sha512-LDA03uA5gTBfFeEIUdHXoCpxGHuPIke03aSXZc6cllRRp9jCaOK2kbWwTOoXfzRvjreKK6pxD0vJcepwdKaNcw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", - "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", + "version": "0.10.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.96.tgz", + "integrity": "sha512-uI86EDxCab345YR/Piwzs3R82DptKp/PC+sMKSX24hehhvWVAkpZus0toGhYwLZxF1r0U6yw4Oq0aCEzW362dQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -122,49 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" + } + }, + "node_modules/@xterm/addon-progress": { + "version": "0.2.0-beta.2", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.2.tgz", + "integrity": "sha512-5BcRexNJanTMG2N9TjZpDcaemDqQuvNkGN8HK3509+QNmqBF9sinJXSBWWNwn+o/f4OheBtBmlmC5mDanh4kig==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", - "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", + "version": "0.16.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.96.tgz", + "integrity": "sha512-AEEXqxkT6OgDOII4SxQJFKn7NhM7PnFeMLxQwrRQzZyrtFPg5peCesA07xHYvYzh0aKQ1PVvgycfrPgbPEeQDQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", - "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", + "version": "0.14.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.96.tgz", + "integrity": "sha512-+00J2K3WsbMPy02UlehNumen//Opmr5B0MBrS4KFqhRwVaO8RD0hMPfr8IuhY3YqPaVny4HMOU88yaWtscxl7A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", - "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.96.tgz", + "integrity": "sha512-jBUSErJtrf6Jhz3itT5JXLeIIl5BkJ1ii8/YdzZQMh9602ZLYvqwS+7ih2FzYYjk0pZ+E1y4sYbT5FpBlVOM5w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", - "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", + "version": "0.19.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.96.tgz", + "integrity": "sha512-iAno2CMYRBHasr5aE3I7WN6DZ0mlHsEAIYAhvdhH+be1YxX4/DceBAtN0Ak7nFcQ70ZbtYzs1aNLTVKtli2TKA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", - "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.96.tgz", + "integrity": "sha512-XdOZyAaqOW67J3kJGJDgVb5skTD2nDQLmBICyhQ0cEqThTKrX5CzB11RswG6rZE8dI+nDE+pc93NjAMXSrQgFg==", "license": "MIT" }, "node_modules/font-finder": { @@ -267,9 +277,10 @@ "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" }, "node_modules/vscode-textmate": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.1.0.tgz", - "integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "license": "MIT" }, "node_modules/yallist": { "version": "4.0.0", diff --git a/code/remote/web/package.json b/code/remote/web/package.json index 5b0a3a2a72c..f1f5ce1ecb9 100644 --- a/code/remote/web/package.json +++ b/code/remote/web/package.json @@ -8,17 +8,18 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", - "vscode-textmate": "9.1.0" + "vscode-textmate": "9.2.0" } } diff --git a/code/resources/linux/rpm/code.spec.template b/code/resources/linux/rpm/code.spec.template index a73bc02c3e7..5691bb6a956 100644 --- a/code/resources/linux/rpm/code.spec.template +++ b/code/resources/linux/rpm/code.spec.template @@ -12,6 +12,9 @@ Requires: @@DEPENDENCIES@@ AutoReq: 0 %global __provides_exclude_from ^%{_datadir}/%{name}/.*\\.so.*$ +# Disable elf stripping, refer https://github.com/microsoft/vscode/issues/223455#issuecomment-2610001754 +%global __brp_strip %{nil} +%global __brp_strip_comment_note %{nil} %description Visual Studio Code is a new choice of tool that combines the simplicity of a code editor with what developers need for the core edit-build-debug cycle. See https://code.visualstudio.com/docs/setup/linux for installation instructions and FAQ. diff --git a/code/scripts/xterm-update.js b/code/scripts/xterm-update.js index 5a7db71abac..35c2084f794 100644 --- a/code/scripts/xterm-update.js +++ b/code/scripts/xterm-update.js @@ -11,6 +11,7 @@ const moduleNames = [ '@xterm/addon-clipboard', '@xterm/addon-image', '@xterm/addon-ligatures', + '@xterm/addon-progress', '@xterm/addon-search', '@xterm/addon-serialize', '@xterm/addon-unicode11', diff --git a/code/src/bootstrap-node.ts b/code/src/bootstrap-node.ts index c0d5cd3693c..1f4b3a2f6a6 100644 --- a/code/src/bootstrap-node.ts +++ b/code/src/bootstrap-node.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; import { createRequire } from 'node:module'; -import type { IProductConfiguration } from './vs/base/common/product'; +import type { IProductConfiguration } from './vs/base/common/product.js'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/code/src/bootstrap-window.ts b/code/src/bootstrap-window.ts index 3c127002603..2f6c296a3ae 100644 --- a/code/src/bootstrap-window.ts +++ b/code/src/bootstrap-window.ts @@ -5,9 +5,9 @@ (function () { - type ISandboxConfiguration = import('vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; - type ILoadResult = import('vs/platform/window/electron-sandbox/window.js').ILoadResult; - type ILoadOptions = import('vs/platform/window/electron-sandbox/window.js').ILoadOptions; + type ISandboxConfiguration = import('./vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; + type ILoadResult = import('./vs/platform/window/electron-sandbox/window.js').ILoadResult; + type ILoadOptions = import('./vs/platform/window/electron-sandbox/window.js').ILoadOptions; type IMainWindowSandboxGlobals = import('./vs/base/parts/sandbox/electron-sandbox/globals.js').IMainWindowSandboxGlobals; const preloadGlobals: IMainWindowSandboxGlobals = (window as any).vscode; // defined by preload.ts diff --git a/code/src/tsconfig.base.json b/code/src/tsconfig.base.json index 9c7aacd4f11..e354b0ed463 100644 --- a/code/src/tsconfig.base.json +++ b/code/src/tsconfig.base.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "module": "es2022", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", + "moduleDetection": "legacy", "experimentalDecorators": true, "noImplicitReturns": true, "noImplicitOverride": true, @@ -11,12 +12,6 @@ "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "vs/*": [ - "./vs/*" - ] - }, "target": "es2022", "useDefineForClassFields": false, "lib": [ @@ -27,4 +22,4 @@ ], "allowSyntheticDefaultImports": true } -} \ No newline at end of file +} diff --git a/code/src/tsconfig.tsec.json b/code/src/tsconfig.tsec.json index d822b0a4e89..64550458ab7 100644 --- a/code/src/tsconfig.tsec.json +++ b/code/src/tsconfig.tsec.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true, + "skipLibCheck": true, "plugins": [ { "name": "tsec", diff --git a/code/src/typings/editContext.d.ts b/code/src/typings/editContext.d.ts index 5b5da0ac7e9..09585848667 100644 --- a/code/src/typings/editContext.d.ts +++ b/code/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -interface TextUpdateEvent extends Event { - new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; +declare class TextUpdateEvent extends Event { + constructor(type: DOMString, options?: TextUpdateEventInit); readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/code/src/vs/base/browser/markdownRenderer.ts b/code/src/vs/base/browser/markdownRenderer.ts index 5d58485ddd8..71af77b8f25 100644 --- a/code/src/vs/base/browser/markdownRenderer.ts +++ b/code/src/vs/base/browser/markdownRenderer.ts @@ -772,9 +772,25 @@ function completeListItemPattern(list: marked.Tokens.List): marked.Tokens.List | codespan */ + const listEndsInHeading = (list: marked.Tokens.List): boolean => { + // A list item can be rendered as a heading for some reason when it has a subitem where we haven't rendered the text yet like this: + // 1. list item + // - + const lastItem = list.items.at(-1); + const lastToken = lastItem?.tokens.at(-1); + return lastToken?.type === 'heading' || lastToken?.type === 'list' && listEndsInHeading(lastToken as marked.Tokens.List); + }; + let newToken: marked.Token | undefined; if (lastListSubToken?.type === 'text' && !('inRawBlock' in lastListItem)) { // Why does Tag have a type of 'text' newToken = completeSingleLinePattern(lastListSubToken as marked.Tokens.Text); + } else if (listEndsInHeading(list)) { + const newList = marked.lexer(list.raw.trim() + '  ')[0] as marked.Tokens.List; + if (newList.type !== 'list') { + // Something went wrong + return; + } + return newList; } if (!newToken || newToken.type !== 'paragraph') { // 'text' item inside the list item turns into paragraph diff --git a/code/src/vs/base/browser/ui/findinput/replaceInput.ts b/code/src/vs/base/browser/ui/findinput/replaceInput.ts index 22a81e211c9..11c3c04a8cc 100644 --- a/code/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/code/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -17,6 +17,7 @@ import { KeyCode } from '../../../common/keyCodes.js'; import './findInput.css'; import * as nls from '../../../../nls.js'; import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; +import { IHistory } from '../../../common/history.js'; export interface IReplaceInputOptions { @@ -29,7 +30,7 @@ export interface IReplaceInputOptions { readonly flexibleMaxHeight?: number; readonly appendPreserveCaseLabel?: string; - readonly history?: string[]; + readonly history?: IHistory; readonly showHistoryHint?: () => boolean; readonly inputBoxStyles: IInputBoxStyles; readonly toggleStyles: IToggleStyles; @@ -94,7 +95,7 @@ export class ReplaceInput extends Widget { this.label = options.label || NLS_DEFAULT_LABEL; const appendPreserveCaseLabel = options.appendPreserveCaseLabel || ''; - const history = options.history || []; + const history = options.history || new Set([]); const flexibleHeight = !!options.flexibleHeight; const flexibleWidth = !!options.flexibleWidth; const flexibleMaxHeight = options.flexibleMaxHeight; @@ -108,7 +109,7 @@ export class ReplaceInput extends Widget { validationOptions: { validation: this.validation }, - history: new Set(history), + history, showHistoryHint: options.showHistoryHint, flexibleHeight, flexibleWidth, diff --git a/code/src/vs/base/browser/ui/grid/grid.ts b/code/src/vs/base/browser/ui/grid/grid.ts index 60d79ed6155..cd7aabf6cfb 100644 --- a/code/src/vs/base/browser/ui/grid/grid.ts +++ b/code/src/vs/base/browser/ui/grid/grid.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IBoundarySashes, Orientation } from '../sash/sash.js'; -import { equals, tail2 as tail } from '../../../common/arrays.js'; +import { equals, tail } from '../../../common/arrays.js'; import { Event } from '../../../common/event.js'; import { Disposable } from '../../../common/lifecycle.js'; import './gridview.css'; diff --git a/code/src/vs/base/browser/ui/grid/gridview.ts b/code/src/vs/base/browser/ui/grid/gridview.ts index 3dc5d8f6a56..09452c14992 100644 --- a/code/src/vs/base/browser/ui/grid/gridview.ts +++ b/code/src/vs/base/browser/ui/grid/gridview.ts @@ -6,7 +6,7 @@ import { $ } from '../../dom.js'; import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js'; import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js'; -import { equals as arrayEquals, tail2 as tail } from '../../../common/arrays.js'; +import { equals as arrayEquals, tail } from '../../../common/arrays.js'; import { Color } from '../../../common/color.js'; import { Emitter, Event, Relay } from '../../../common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js'; diff --git a/code/src/vs/base/browser/ui/list/listView.ts b/code/src/vs/base/browser/ui/list/listView.ts index e305b7de4b9..4c8af3e908e 100644 --- a/code/src/vs/base/browser/ui/list/listView.ts +++ b/code/src/vs/base/browser/ui/list/listView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DataTransfers, IDragAndDropData } from '../../dnd.js'; -import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; +import { $, addDisposableListener, animate, Dimension, getActiveElement, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { IMouseWheelEvent } from '../../mouseEvent.js'; import { EventType as TouchEventType, Gesture, GestureEvent } from '../../touch.js'; @@ -87,6 +87,7 @@ export interface IListViewOptions extends IListViewOptionsUpdate { readonly transformOptimization?: boolean; readonly alwaysConsumeMouseWheel?: boolean; readonly initialSize?: Dimension; + readonly scrollToActiveElement?: boolean; } const DefaultOptions = { @@ -324,6 +325,7 @@ export class ListView implements IListView { private onDragLeaveTimeout: IDisposable = Disposable.None; private currentSelectionDisposable: IDisposable = Disposable.None; private currentSelectionBounds: IRange | undefined; + private activeElement: HTMLElement | undefined; private readonly disposables: DisposableStore = new DisposableStore(); @@ -439,9 +441,15 @@ export class ListView implements IListView { this.scrollableElement.onScroll(this.onScroll, this, this.disposables); this.disposables.add(addDisposableListener(this.rowsContainer, TouchEventType.Change, e => this.onTouchChange(e as GestureEvent))); - // Prevent the monaco-scrollable-element from scrolling - // https://github.com/microsoft/vscode/issues/44181 - this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => (e.target as HTMLElement).scrollTop = 0)); + this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => { + // Make sure the active element is scrolled into view + const element = (e.target as HTMLElement); + const scrollValue = element.scrollTop; + element.scrollTop = 0; + if (options.scrollToActiveElement) { + this.setScrollTop(this.scrollTop + scrollValue); + } + })); this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e)))); @@ -460,6 +468,33 @@ export class ListView implements IListView { this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd); this.layout(options.initialSize?.height, options.initialSize?.width); + if (options.scrollToActiveElement) { + this._setupFocusObserver(container); + } + } + + private _setupFocusObserver(container: HTMLElement): void { + this.disposables.add(addDisposableListener(container, 'focus', () => { + const element = getActiveElement() as HTMLElement | null; + if (this.activeElement !== element && element !== null) { + this.activeElement = element; + this._scrollToActiveElement(this.activeElement, container); + } + }, true)); + } + + private _scrollToActiveElement(element: HTMLElement, container: HTMLElement) { + // The scroll event on the list only fires when scrolling down. + // If the active element is above the viewport, we need to scroll up. + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + + const topOffset = elementRect.top - containerRect.top; + + if (topOffset < 0) { + // Scroll up + this.setScrollTop(this.scrollTop + topOffset); + } } updateOptions(options: IListViewOptionsUpdate) { @@ -1193,9 +1228,11 @@ export class ListView implements IListView { // Selection events also don't tell us where the input doing the selection is. So, make a poor // assumption that a user is using the mouse, and base our events on that. movementStore.add(addDisposableListener(this.domNode, 'selectstart', () => { - this.setupDragAndDropScrollTopAnimation(e); - - movementStore.add(addDisposableListener(doc, 'mousemove', e => this.setupDragAndDropScrollTopAnimation(e))); + movementStore.add(addDisposableListener(doc, 'mousemove', e => { + if (doc.getSelection()?.isCollapsed === false) { + this.setupDragAndDropScrollTopAnimation(e); + } + })); // The selection is cleared either on mouseup if there's no selection, or on next mousedown // when `this.currentSelectionDisposable` is reset. @@ -1223,6 +1260,7 @@ export class ListView implements IListView { movementStore.add(addDisposableListener(doc, 'mouseup', () => { movementStore.dispose(); + this.teardownDragAndDropScrollTopAnimation(); if (doc.getSelection()?.isCollapsed !== false) { selectionStore.dispose(); diff --git a/code/src/vs/base/browser/ui/tree/indexTreeModel.ts b/code/src/vs/base/browser/ui/tree/indexTreeModel.ts index c263eecddf6..adbab9de3f3 100644 --- a/code/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/code/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -5,7 +5,7 @@ import { IIdentityProvider } from '../list/list.js'; import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeListSpliceData, ITreeModel, ITreeModelSpliceEvent, ITreeNode, TreeError, TreeVisibility } from './tree.js'; -import { splice, tail2 } from '../../../common/arrays.js'; +import { splice, tail } from '../../../common/arrays.js'; import { Delayer } from '../../../common/async.js'; import { MicrotaskDelay } from '../../../common/symbols.js'; import { LcsDiff } from '../../../common/diff/diff.js'; @@ -762,7 +762,7 @@ export class IndexTreeModel, TFilterData = voi } else if (location.length === 1) { return []; } else { - return tail2(location)[0]; + return tail(location)[0]; } } diff --git a/code/src/vs/base/common/actions.ts b/code/src/vs/base/common/actions.ts index 5a271b2bdc8..fd2605942f3 100644 --- a/code/src/vs/base/common/actions.ts +++ b/code/src/vs/base/common/actions.ts @@ -17,7 +17,7 @@ export type WorkbenchActionExecutedClassification = { id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run.' }; from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the component the action was run from.' }; detail?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Optional details about how the action was run, e.g which keybinding was used.' }; - owner: 'bpasero'; + owner: 'isidorn'; comment: 'Provides insight into actions that are executed within the workbench.'; }; diff --git a/code/src/vs/base/common/arrays.ts b/code/src/vs/base/common/arrays.ts index 9035ffd9f18..dcc6aa5406d 100644 --- a/code/src/vs/base/common/arrays.ts +++ b/code/src/vs/base/common/arrays.ts @@ -9,15 +9,15 @@ import { CancellationError } from './errors.js'; import { ISplice } from './sequence.js'; /** - * Returns the last element of an array. - * @param array The array. - * @param n Which element from the end (default is zero). + * Returns the last entry and the initial N-1 entries of the array, as a tuple of [rest, last]. + * + * The array must have at least one element. + * + * @param arr The input array + * @returns A tuple of [rest, last] where rest is all but the last element and last is the last element + * @throws Error if the array is empty */ -export function tail(array: ArrayLike, n: number = 0): T | undefined { - return array[array.length - (1 + n)]; -} - -export function tail2(arr: T[]): [T[], T] { +export function tail(arr: T[]): [T[], T] { if (arr.length === 0) { throw new Error('Invalid tail call'); } diff --git a/code/src/vs/base/common/assert.ts b/code/src/vs/base/common/assert.ts index b185897f614..56b74b3a3b7 100644 --- a/code/src/vs/base/common/assert.ts +++ b/code/src/vs/base/common/assert.ts @@ -29,9 +29,25 @@ export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } -export function assert(condition: boolean, message = 'unexpected state'): asserts condition { +/** + * Asserts that a condition is `truthy`. + * + * @throws provided {@linkcode messageOrError} if the {@linkcode condition} is `falsy`. + * + * @param condition The condition to assert. + * @param messageOrError An error message or error object to throw if condition is `falsy`. + */ +export function assert( + condition: boolean, + messageOrError: string | Error = 'unexpected state', +): asserts condition { if (!condition) { - throw new BugIndicatingError(`Assertion Failed: ${message}`); + // if error instance is provided, use it, otherwise create a new one + const errorToThrow = typeof messageOrError === 'string' + ? new BugIndicatingError(`Assertion Failed: ${messageOrError}`) + : messageOrError; + + throw errorToThrow; } } diff --git a/code/src/vs/base/common/codecs/asyncDecoder.ts b/code/src/vs/base/common/codecs/asyncDecoder.ts index efbcf9a7fe4..e35daa77317 100644 --- a/code/src/vs/base/common/codecs/asyncDecoder.ts +++ b/code/src/vs/base/common/codecs/asyncDecoder.ts @@ -69,7 +69,7 @@ export class AsyncDecoder, K extends NonNullable< } // if no data available and stream ended, we're done - if (this.decoder.isEnded) { + if (this.decoder.ended) { this.dispose(); return null; diff --git a/code/src/vs/base/common/codecs/baseDecoder.ts b/code/src/vs/base/common/codecs/baseDecoder.ts index 5404e3df0bf..586ea61f56a 100644 --- a/code/src/vs/base/common/codecs/baseDecoder.ts +++ b/code/src/vs/base/common/codecs/baseDecoder.ts @@ -5,9 +5,11 @@ import { assert } from '../assert.js'; import { Emitter } from '../event.js'; +import { IDisposable } from '../lifecycle.js'; import { ReadableStream } from '../stream.js'; +import { DeferredPromise } from '../async.js'; import { AsyncDecoder } from './asyncDecoder.js'; -import { Disposable, IDisposable } from '../lifecycle.js'; +import { ObservableDisposable } from '../observableDisposable.js'; /** * Event names of {@link ReadableStream} stream. @@ -23,21 +25,27 @@ export type TStreamListenerNames = 'data' | 'error' | 'end'; export abstract class BaseDecoder< T extends NonNullable, K extends NonNullable = NonNullable, -> extends Disposable implements ReadableStream { +> extends ObservableDisposable implements ReadableStream { /** - * Flag that indicates if the decoder stream has ended. + * Private attribute to track if the stream has ended. */ - protected ended = false; + private _ended = false; protected readonly _onData = this._register(new Emitter()); - protected readonly _onEnd = this._register(new Emitter()); - protected readonly _onError = this._register(new Emitter()); + private readonly _onEnd = this._register(new Emitter()); + private readonly _onError = this._register(new Emitter()); /** * A store of currently registered event listeners. */ private readonly _listeners: Map> = new Map(); + /** + * This method is called when a new incomming data + * is received from the input stream. + */ + protected abstract onStreamData(data: K): void; + /** * @param stream The input stream to decode. */ @@ -52,10 +60,41 @@ export abstract class BaseDecoder< } /** - * This method is called when a new incomming data - * is received from the input stream. + * Private attribute to track if the stream has started. */ - protected abstract onStreamData(data: K): void; + private started = false; + + /** + * Promise that resolves when the stream has ended, either by + * receiving the `end` event or by a disposal, but not when + * the `error` event is received alone. + */ + private settledPromise = new DeferredPromise(); + + /** + * Promise that resolves when the stream has ended, either by + * receiving the `end` event or by a disposal, but not when + * the `error` event is received alone. + * + * @throws If the stream was not yet started to prevent this + * promise to block the consumer calls indefinitely. + */ + public get settled(): Promise { + // if the stream has not started yet, the promise might + // block the consumer calls indefinitely if they forget + // to call the `start()` method, or if the call happens + // after await on the `settled` promise; to forbid this + // confusion, we require the stream to be started first + assert( + this.started, + [ + 'Cannot get `settled` promise of a stream that has not been started.', + 'Please call `start()` first.', + ].join(' '), + ); + + return this.settledPromise.p; + } /** * Start receiveing data from the stream. @@ -63,9 +102,19 @@ export abstract class BaseDecoder< */ public start(): this { assert( - !this.ended, + !this._ended, 'Cannot start stream that has already ended.', ); + assert( + !this.disposed, + 'Cannot start stream that has already disposed.', + ); + + // if already started, nothing to do + if (this.started) { + return this; + } + this.started = true; this.stream.on('data', this.tryOnStreamData); this.stream.on('error', this.onStreamError); @@ -85,8 +134,8 @@ export abstract class BaseDecoder< * Check if the decoder has been ended hence has * no more data to produce. */ - public get isEnded(): boolean { - return this.ended; + public get ended(): boolean { + return this._ended; } /** @@ -257,12 +306,13 @@ export abstract class BaseDecoder< * This method is called when the input stream ends. */ protected onStreamEnd(): void { - if (this.ended) { + if (this._ended) { return; } - this.ended = true; + this._ended = true; this._onEnd.fire(); + this.settledPromise.complete(); } /** @@ -280,7 +330,7 @@ export abstract class BaseDecoder< */ public async consumeAll(): Promise { assert( - !this.ended, + !this._ended, 'Cannot consume all messages of the stream that has already ended.', ); @@ -303,7 +353,7 @@ export abstract class BaseDecoder< */ [Symbol.asyncIterator](): AsyncIterator { assert( - !this.ended, + !this._ended, 'Cannot iterate on messages of the stream that has already ended.', ); @@ -313,6 +363,10 @@ export abstract class BaseDecoder< } public override dispose(): void { + if (this.disposed) { + return; + } + this.onStreamEnd(); this.stream.destroy(); diff --git a/code/src/vs/base/common/color.ts b/code/src/vs/base/common/color.ts index 8b1a68294a5..9f20f7080ab 100644 --- a/code/src/vs/base/common/color.ts +++ b/code/src/vs/base/common/color.ts @@ -535,17 +535,17 @@ export class Color { return this._toString; } - private _toNumber24Bit?: number; - toNumber24Bit(): number { - if (!this._toNumber24Bit) { - this._toNumber24Bit = ( + private _toNumber32Bit?: number; + toNumber32Bit(): number { + if (!this._toNumber32Bit) { + this._toNumber32Bit = ( this.rgba.r /* */ << 24 | this.rgba.g /* */ << 16 | this.rgba.b /* */ << 8 | this.rgba.a * 0xFF << 0 ) >>> 0; } - return this._toNumber24Bit; + return this._toNumber32Bit; } static getLighterColor(of: Color, relative: Color, factor?: number): Color { diff --git a/code/src/vs/base/common/decorators.ts b/code/src/vs/base/common/decorators.ts index 9a40158d9e6..0f02b3ede2c 100644 --- a/code/src/vs/base/common/decorators.ts +++ b/code/src/vs/base/common/decorators.ts @@ -127,3 +127,5 @@ export function throttle(delay: number, reducer?: IDebounceReducer, initia }; }); } + +export { cancelPreviousCalls } from './decorators/cancelPreviousCalls.js'; diff --git a/code/src/vs/base/common/decorators/cancelPreviousCalls.ts b/code/src/vs/base/common/decorators/cancelPreviousCalls.ts new file mode 100644 index 00000000000..b0a3089bc31 --- /dev/null +++ b/code/src/vs/base/common/decorators/cancelPreviousCalls.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertDefined } from '../types.js'; +import { Disposable, DisposableMap } from '../lifecycle.js'; +import { CancellationTokenSource, CancellationToken } from '../cancellation.js'; + +/** + * Helper type that represents a function that has an optional {@linkcode CancellationToken} + * argument argument at the end of the arguments list. + * + * @typeparam `TFunction` - Type of the function arguments list of which will be extended + * with an optional {@linkcode CancellationToken} argument. + */ +type TWithOptionalCancellationToken = TFunction extends (...args: infer TArgs) => infer TReturn + ? (...args: [...TArgs, cancellatioNToken?: CancellationToken]) => TReturn + : never; + +/** + * Decorator that provides a mechanism to cancel previous calls of the decorated method + * by providing a `cancellation token` as the last argument of the method, which gets + * cancelled immediately on subsequent call of the decorated method. + * + * Therefore to use this decorator, the two conditions must be met: + * + * - the decorated method must have an *optional* {@linkcode CancellationToken} argument at + * the end of the arguments list + * - the object that the decorated method belongs to must implement the {@linkcode Disposable}; + * this requirement comes from the internal implementation of the decorator that + * creates new resources that need to be eventually disposed by someone + * + * @typeparam `TObject` - Object type that the decorated method belongs to. + * @typeparam `TArgs` - Argument list of the decorated method. + * @typeparam `TReturn` - Return value type of the decorated method. + * + * ### Examples + * + * ```typescript + * // let's say we have a class that implements the `Disposable` interface that we want + * // to use the decorator on + * class Example extends Disposable { + * async doSomethingAsync(arg1: number, arg2: string): Promise { + * // do something async.. + * await new Promise(resolve => setTimeout(resolve, 1000)); + * } + * } + * ``` + * + * ```typescript + * // to do that we need to add the `CancellationToken` argument to the end of args list + * class Example extends Disposable { + * @cancelPreviousCalls + * async doSomethingAsync(arg1: number, arg2: string, cancellationToken?: CancellationToken): Promise { + * console.log(`call with args ${arg1} and ${arg2} initiated`); + * + * // the decorator will create the cancellation token automatically + * assertDefined( + * cancellationToken, + * `The method must now have the `CancellationToken` passed to it.`, + * ); + * + * cancellationToken.onCancellationRequested(() => { + * console.log(`call with args ${arg1} and ${arg2} was cancelled`); + * }); + * + * // do something async.. + * await new Promise(resolve => setTimeout(resolve, 1000)); + * + * // check cancellation token state after the async operations + * console.log( + * `call with args ${arg1} and ${arg2} completed, canceled?: ${cancellationToken.isCancellationRequested}`, + * ); + * } + * } + * + * const example = new Example(); + * // call the decorate method first time + * example.doSomethingAsync(1, 'foo'); + * // wait for 500ms which is less than 1000ms of the async operation in the first call + * await new Promise(resolve => setTimeout(resolve, 500)); + * // calling the decorate method second time cancels the token passed to the first call + * example.doSomethingAsync(2, 'bar'); + * ``` + */ +export function cancelPreviousCalls< + TObject extends Disposable, + TArgs extends unknown[], + TReturn extends unknown, +>( + _proto: TObject, + methodName: string, + descriptor: TypedPropertyDescriptor TReturn>>, +) { + const originalMethod = descriptor.value; + + assertDefined( + originalMethod, + `Method '${methodName}' is not defined.`, + ); + + // we create the global map that contains `TObjectRecord` for each object instance that + // uses this decorator, which itself contains a `{method name} -> TMethodRecord` mapping + // for each decorated method on the object; the `TMethodRecord` record stores current + // `cancellationTokenSource`, token of which was passed to the previous call of the method + const objectRecords = new WeakMap>(); + + // decorate the original method with the following logic that upon a new invocation + // of the method cancels the cancellation token that was passed to a previous call + descriptor.value = function ( + this: TObject, + ...args: Parameters + ): TReturn { + // get or create a record for the current object instance + // the creation is done once per each object instance + let record = objectRecords.get(this); + if (!record) { + record = new DisposableMap(); + objectRecords.set(this, record); + + this._register({ + dispose: () => { + objectRecords.get(this)?.dispose(); + objectRecords.delete(this); + }, + }); + } + + // when the decorated method is called again and there is a cancellation token + // source exists from a previous call, cancel and dispose it, then remove it + record.get(methodName)?.dispose(true); + + // now we need to provide a cancellation token to the original method + // as the last argument, there are two cases to consider: + // - (common case) the arguments list does not have a cancellation token + // as the last argument, - in this case we need to add a new one + // - (possible case) - the arguments list already has a cancellation token + // as the last argument, - in this case we need to reuse the token when + // we create ours, and replace the old token with the new one + // therefore, + + // get the last argument of the arguments list and if it is present, + // reuse it as the token for the new cancellation token source + const lastArgument = (args.length > 0) + ? args[args.length - 1] + : undefined; + const token = CancellationToken.isCancellationToken(lastArgument) + ? lastArgument + : undefined; + + const cancellationSource = new CancellationTokenSource(token); + record.set(methodName, cancellationSource); + + // then update or add cancelaltion token at the end of the arguments list + if (CancellationToken.isCancellationToken(lastArgument)) { + args[args.length - 1] = cancellationSource.token; + } else { + args.push(cancellationSource.token); + } + + // finally invoke the original method passing original arguments and + // the new cancellation token at the end of the arguments list + return originalMethod.call(this, ...args); + }; + + return descriptor; +} diff --git a/code/src/vs/base/common/errors.ts b/code/src/vs/base/common/errors.ts index a6930ef51cf..888aa3b0630 100644 --- a/code/src/vs/base/common/errors.ts +++ b/code/src/vs/base/common/errors.ts @@ -126,9 +126,14 @@ export interface SerializedError { readonly message: string; readonly stack: string; readonly noTelemetry: boolean; + readonly code?: string; readonly cause?: SerializedError; } +type ErrorWithCode = Error & { + code: string | undefined; +}; + export function transformErrorForSerialization(error: Error): SerializedError; export function transformErrorForSerialization(error: any): any; export function transformErrorForSerialization(error: any): any { @@ -141,7 +146,8 @@ export function transformErrorForSerialization(error: any): any { message, stack, noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error), - cause: cause ? transformErrorForSerialization(cause) : undefined + cause: cause ? transformErrorForSerialization(cause) : undefined, + code: (error).code }; } @@ -159,6 +165,9 @@ export function transformErrorFromSerialization(data: SerializedError): Error { } error.message = data.message; error.stack = data.stack; + if (data.code) { + (error).code = data.code; + } if (data.cause) { error.cause = transformErrorFromSerialization(data.cause); } diff --git a/code/src/vs/base/common/event.ts b/code/src/vs/base/common/event.ts index d49f2ef276f..6603fa22c39 100644 --- a/code/src/vs/base/common/event.ts +++ b/code/src/vs/base/common/event.ts @@ -13,12 +13,6 @@ import { IObservable, IObservableWithChange, IObserver } from './observable.js'; import { StopWatch } from './stopwatch.js'; import { MicrotaskDelay } from './symbols.js'; -// ----------------------------------------------------------------------------------------------------------------------- -// Uncomment the next line to print warnings whenever a listener is GC'ed without having been disposed. This is a LEAK. -// ----------------------------------------------------------------------------------------------------------------------- -const _enableListenerGCedWarning = false - // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed - ; // ----------------------------------------------------------------------------------------------------------------------- // Uncomment the next line to print warnings whenever an emitter with listeners is disposed. That is a sign of code smell. @@ -602,8 +596,8 @@ export namespace Event { /** * Creates a promise out of an event, using the {@link Event.once} helper. */ - export function toPromise(event: Event): Promise { - return new Promise(resolve => once(event)(resolve)); + export function toPromise(event: Event, disposables?: IDisposable[] | DisposableStore): Promise { + return new Promise(resolve => once(event)(resolve, null, disposables)); } /** @@ -984,28 +978,6 @@ const forEachListener = (listeners: ListenerOrListeners, fn: (c: ListenerC } }; - -let _listenerFinalizers: FinalizationRegistry | undefined; - -if (_enableListenerGCedWarning) { - const leaks: string[] = []; - - setInterval(() => { - if (leaks.length === 0) { - return; - } - console.warn('[LEAKING LISTENERS] GC\'ed these listeners that were NOT yet disposed:'); - console.warn(leaks.join('\n')); - leaks.length = 0; - }, 3000); - - _listenerFinalizers = new FinalizationRegistry(heldValue => { - if (typeof heldValue === 'string') { - leaks.push(heldValue); - } - }); -} - /** * The Emitter can be used to expose an Event to the public * to fire it from the insides. @@ -1161,7 +1133,6 @@ export class Emitter { const result = toDisposable(() => { - _listenerFinalizers?.unregister(result); removeMonitor?.(); this._removeListener(contained); }); @@ -1171,12 +1142,6 @@ export class Emitter { disposables.push(result); } - if (_listenerFinalizers) { - const stack = new Error().stack!.split('\n').slice(2, 3).join('\n').trim(); - const match = /(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(stack); - _listenerFinalizers.register(result, match?.[2] ?? stack, result); - } - return result; }; diff --git a/code/src/vs/base/common/iterator.ts b/code/src/vs/base/common/iterator.ts index ca55068751d..fedcfe7edef 100644 --- a/code/src/vs/base/common/iterator.ts +++ b/code/src/vs/base/common/iterator.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isIterable } from './types.js'; + export namespace Iterable { export function is(thing: any): thing is Iterable { @@ -90,9 +92,13 @@ export namespace Iterable { } } - export function* concat(...iterables: Iterable[]): Iterable { - for (const iterable of iterables) { - yield* iterable; + export function* concat(...iterables: (Iterable | T)[]): Iterable { + for (const item of iterables) { + if (isIterable(item)) { + yield* item; + } else { + yield item; + } } } diff --git a/code/src/vs/base/common/map.ts b/code/src/vs/base/common/map.ts index bc06aafaec2..c8f9de67964 100644 --- a/code/src/vs/base/common/map.ts +++ b/code/src/vs/base/common/map.ts @@ -877,91 +877,76 @@ export function mapsStrictEqualIgnoreOrder(a: Map, b: Map { - private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; +export class NKeyMap { + private _data: Map = new Map(); - public set(first: TFirst, second: TSecond, value: TValue): void { - if (!this._data[first]) { - this._data[first] = {}; - } - this._data[first as string | number]![second] = value; - } - - public get(first: TFirst, second: TSecond): TValue | undefined { - return this._data[first as string | number]?.[second]; - } - - public clear(): void { - this._data = {}; - } - - public *values(): IterableIterator { - for (const first in this._data) { - for (const second in this._data[first]) { - const value = this._data[first]![second]; - if (value) { - yield value; - } + /** + * Sets a value on the map. Note that unlike a standard `Map`, the first argument is the value. + * This is because the spread operator is used for the keys and must be last.. + * @param value The value to set. + * @param keys The keys for the value. + */ + public set(value: TValue, ...keys: [...TKeys]): void { + let currentMap = this._data; + for (let i = 0; i < keys.length - 1; i++) { + if (!currentMap.has(keys[i])) { + currentMap.set(keys[i], new Map()); } + currentMap = currentMap.get(keys[i]); } + currentMap.set(keys[keys.length - 1], value); } -} -/** - * A map that is addressable with 3 separate keys. This is useful in high performance scenarios - * where creating a composite key whenever the data is accessed is too expensive. - */ -export class ThreeKeyMap { - private _data: { [key: string | number]: TwoKeyMap | undefined } = {}; - - public set(first: TFirst, second: TSecond, third: TThird, value: TValue): void { - if (!this._data[first]) { - this._data[first] = new TwoKeyMap(); + public get(...keys: [...TKeys]): TValue | undefined { + let currentMap = this._data; + for (let i = 0; i < keys.length - 1; i++) { + if (!currentMap.has(keys[i])) { + return undefined; + } + currentMap = currentMap.get(keys[i]); } - this._data[first as string | number]!.set(second, third, value); - } - - public get(first: TFirst, second: TSecond, third: TThird): TValue | undefined { - return this._data[first as string | number]?.get(second, third); + return currentMap.get(keys[keys.length - 1]); } public clear(): void { - this._data = {}; + this._data.clear(); } public *values(): IterableIterator { - for (const first in this._data) { - for (const value of this._data[first]!.values()) { - if (value) { + function* iterate(map: Map): IterableIterator { + for (const value of map.values()) { + if (value instanceof Map) { + yield* iterate(value); + } else { yield value; } } } - } -} - -/** - * A map that is addressable with 4 separate keys. This is useful in high performance scenarios - * where creating a composite key whenever the data is accessed is too expensive. - */ -export class FourKeyMap { - private _data: TwoKeyMap> = new TwoKeyMap(); - - public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { - if (!this._data.get(first, second)) { - this._data.set(first, second, new TwoKeyMap()); - } - this._data.get(first, second)!.set(third, fourth, value); + yield* iterate(this._data); } - public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { - return this._data.get(first, second)?.get(third, fourth); - } + /** + * Get a textual representation of the map for debugging purposes. + */ + public toString(): string { + const printMap = (map: Map, depth: number): string => { + let result = ''; + for (const [key, value] of map) { + result += `${' '.repeat(depth)}${key}: `; + if (value instanceof Map) { + result += '\n' + printMap(value, depth + 1); + } else { + result += `${value}\n`; + } + } + return result; + }; - public clear(): void { - this._data.clear(); + return printMap(this._data, 0); } } diff --git a/code/src/vs/base/common/objectCache.ts b/code/src/vs/base/common/objectCache.ts new file mode 100644 index 00000000000..e7be0b161e9 --- /dev/null +++ b/code/src/vs/base/common/objectCache.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../base/common/lifecycle.js'; +import { ObservableDisposable, assertNotDisposed } from './observableDisposable.js'; + +/** + * Generic cache for object instances. Guarantees to return only non-disposed + * objects from the {@linkcode get} method. If a requested object is not yet + * in the cache or is disposed already, the {@linkcode factory} callback is + * called to create a new object. + * + * @throws if {@linkcode factory} callback returns a disposed object. + * + * ## Examples + * + * ```typescript + * // a class that will be used as a cache key; the key can be of any + * // non-nullable type, including primitives like `string` or `number`, + * // but in this case we use an object pointer as a key + * class KeyObject {} + * + * // a class for testing purposes + * class TestObject extends ObservableDisposable { + * constructor( + * public readonly id: KeyObject, + * ) {} + * }; + * + * // create an object cache instance providing it a factory function that + * // is responsible for creating new objects based on the provided key if + * // the cache does not contain the requested object yet or an existing + * // object is already disposed + * const cache = new ObjectCache((key) => { + * // create a new test object based on the provided key + * return new TestObject(key); + * }); + * + * // create two keys + * const key1 = new KeyObject(); + * const key2 = new KeyObject(); + * + * // get an object from the cache by its key + * const object1 = cache.get(key1); // returns a new test object + * + * // validate that the new object has the correct key + * assert( + * object1.id === key1, + * 'Object 1 must have correct ID.', + * ); + * + * // returns the same cached test object + * const object2 = cache.get(key1); + * + * // validate that the same exact object is returned from the cache + * assert( + * object1 === object2, + * 'Object 2 the same cached object as object 1.', + * ); + * + * // returns a new test object + * const object3 = cache.get(key2); + * + * // validate that the new object has the correct key + * assert( + * object3.id === key2, + * 'Object 3 must have correct ID.', + * ); + * + * assert( + * object3 !== object1, + * 'Object 3 must be a new object.', + * ); + * ``` + */ +export class ObjectCache< + TValue extends ObservableDisposable, + TKey extends NonNullable = string, +> extends Disposable { + private readonly cache: DisposableMap = + this._register(new DisposableMap()); + + constructor( + private readonly factory: (key: TKey) => TValue & { disposed: false }, + ) { + super(); + } + + /** + * Get an existing object from the cache. If a requested object is not yet + * in the cache or is disposed already, the {@linkcode factory} callback is + * called to create a new object. + * + * @throws if {@linkcode factory} callback returns a disposed object. + * @param key - ID of the object in the cache + */ + public get(key: TKey): TValue & { disposed: false } { + let object = this.cache.get(key); + + // if object is already disposed, remove it from the cache + if (object?.disposed) { + this.cache.deleteAndLeak(key); + object = undefined; + } + + // if object exists and is not disposed, return it + if (object) { + // must always hold true due to the check above + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + + return object; + } + + // create a new object by calling the factory + object = this.factory(key); + + // newly created object must not be disposed + assertNotDisposed( + object, + 'Newly created object must not be disposed.', + ); + + // remove it from the cache automatically on dispose + object.onDispose(() => { + this.cache.deleteAndLeak(key); + }); + this.cache.set(key, object); + + return object; + } + + /** + * Remove an object from the cache by its key. + * + * @param key ID of the object to remove. + * @param dispose Whether the removed object must be disposed. + */ + public remove(key: TKey, dispose: boolean): this { + if (dispose) { + this.cache.deleteAndDispose(key); + return this; + } + + this.cache.deleteAndLeak(key); + return this; + } +} diff --git a/code/src/vs/base/common/observableDisposable.ts b/code/src/vs/base/common/observableDisposable.ts new file mode 100644 index 00000000000..3bce45ffa5a --- /dev/null +++ b/code/src/vs/base/common/observableDisposable.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './event.js'; +import { Disposable } from './lifecycle.js'; + +/** + * Disposable object that tracks its {@linkcode disposed} state + * as a public attribute and provides the {@linkcode onDispose} + * event to subscribe to. + */ +export abstract class ObservableDisposable extends Disposable { + /** + * Private emitter for the `onDispose` event. + */ + private readonly _onDispose = this._register(new Emitter()); + + /** + * The event is fired when this object is disposed. + * Note! Executes the callback immediately if already disposed. + * + * @param callback The callback function to be called on updates. + */ + public onDispose(callback: () => void): this { + // if already disposed, execute the callback immediately + if (this.disposed) { + callback(); + + return this; + } + + // otherwise subscribe to the event + this._register(this._onDispose.event(callback)); + return this; + } + + /** + * Tracks disposed state of this object. + */ + private _disposed = false; + + /** + * Check if the current object was already disposed. + */ + public get disposed(): boolean { + return this._disposed; + } + + /** + * Dispose current object if not already disposed. + * @returns + */ + public override dispose(): void { + if (this.disposed) { + return; + } + + this._disposed = true; + this._onDispose.fire(); + super.dispose(); + } + + /** + * Assert that the current object was not yet disposed. + * + * @throws If the current object was already disposed. + * @param error Error message or error object to throw if assertion fails. + */ + public assertNotDisposed( + error: string | Error, + ): asserts this is TNotDisposed { + assertNotDisposed(this, error); + } +} + +/** + * Type for a non-disposed object `TObject`. + */ +type TNotDisposed = TObject & { disposed: false }; + +/** + * Asserts that a provided `object` is not `disposed` yet, + * e.g., its `disposed` property is `false`. + * + * @throws if the provided `object.disposed` equal to `false`. + * @param error Error message or error object to throw if assertion fails. + */ +export function assertNotDisposed( + object: TObject, + error: string | Error, +): asserts object is TNotDisposed { + if (!object.disposed) { + return; + } + + const errorToThrow = typeof error === 'string' + ? new Error(error) + : error; + + throw errorToThrow; +} diff --git a/code/src/vs/base/common/observableInternal/utils.ts b/code/src/vs/base/common/observableInternal/utils.ts index c42f12f7b8e..56d3e692b49 100644 --- a/code/src/vs/base/common/observableInternal/utils.ts +++ b/code/src/vs/base/common/observableInternal/utils.ts @@ -202,20 +202,24 @@ export namespace observableFromEvent { } export function observableSignalFromEvent( - debugName: string, + owner: DebugOwner | string, event: Event ): IObservable { - return new FromEventObservableSignal(debugName, event); + return new FromEventObservableSignal(typeof owner === 'string' ? owner : new DebugNameData(owner, undefined, undefined), event); } class FromEventObservableSignal extends BaseObservable { private subscription: IDisposable | undefined; + public readonly debugName: string; constructor( - public readonly debugName: string, + debugNameDataOrName: DebugNameData | string, private readonly event: Event, ) { super(); + this.debugName = typeof debugNameDataOrName === 'string' + ? debugNameDataOrName + : debugNameDataOrName.getDebugName(this) ?? 'Observable Signal From Event'; } protected override onFirstObserverAdded(): void { diff --git a/code/src/vs/base/common/product.ts b/code/src/vs/base/common/product.ts index 2a7bdc7825f..675702c6b15 100644 --- a/code/src/vs/base/common/product.ts +++ b/code/src/vs/base/common/product.ts @@ -108,6 +108,7 @@ export interface IProductConfiguration { }; readonly extensionPublisherOrgs?: readonly string[]; + readonly trustedExtensionPublishers?: readonly string[]; readonly extensionRecommendations?: IStringDictionary; readonly configBasedExtensionTips?: IStringDictionary; @@ -308,6 +309,7 @@ export interface IAiGeneratedWorkspaceTrust { export interface IDefaultChatAgent { readonly extensionId: string; readonly chatExtensionId: string; + readonly documentationUrl: string; readonly termsStatementUrl: string; readonly privacyStatementUrl: string; @@ -316,9 +318,15 @@ export interface IDefaultChatAgent { readonly manageSettingsUrl: string; readonly managePlanUrl: string; readonly upgradePlanUrl: string; + readonly providerId: string; readonly providerName: string; + readonly enterpriseProviderId: string; + readonly enterpriseProviderName: string; + readonly providerSetting: string; + readonly providerUriSetting: string; readonly providerScopes: string[][]; + readonly entitlementUrl: string; readonly entitlementSignupLimitedUrl: string; } diff --git a/code/src/vs/base/common/strings.ts b/code/src/vs/base/common/strings.ts index be89ea49299..44dc32027cc 100644 --- a/code/src/vs/base/common/strings.ts +++ b/code/src/vs/base/common/strings.ts @@ -749,7 +749,7 @@ export function isEmojiImprecise(x: number): boolean { * happens at favorable positions - such as whitespace or punctuation characters. * The return value can be longer than the given value of `n`. Leading whitespace is always trimmed. */ -export function lcut(text: string, n: number, prefix = '') { +export function lcut(text: string, n: number, prefix = ''): string { const trimmed = text.trimStart(); if (trimmed.length < n) { @@ -774,6 +774,34 @@ export function lcut(text: string, n: number, prefix = '') { return prefix + trimmed.substring(i).trimStart(); } +/** + * Given a string and a max length returns a shorted version. Shorting + * happens at favorable positions - such as whitespace or punctuation characters. + * The return value can be longer than the given value of `n`. Trailing whitespace is always trimmed. + */ +export function rcut(text: string, n: number, suffix = ''): string { + const trimmed = text.trimEnd(); + + if (trimmed.length < n) { + return trimmed; + } + + const parts = text.split(/\b/); + let result = ''; + for (const part of parts) { + if (result.length > 0 && result.length + part.length > n) { + break; + } + result += part; + } + + if (result === trimmed) { + return result; + } + + return result.trim().replace(/b$/, '') + suffix; +} + // Escape codes, compiled from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ // Plus additional markers for custom `\x1b]...\x07` instructions. const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; diff --git a/code/src/vs/base/node/extpath.ts b/code/src/vs/base/node/extpath.ts index 4fa8d7a0b2b..60fbdd6e4c7 100644 --- a/code/src/vs/base/node/extpath.ts +++ b/code/src/vs/base/node/extpath.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../common/cancellation.js'; import { basename, dirname, join, normalize, sep } from '../common/path.js'; import { isLinux } from '../common/platform.js'; import { rtrim } from '../common/strings.js'; -import { Promises, readdirSync } from './pfs.js'; +import { Promises } from './pfs.js'; /** * Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 @@ -17,48 +17,8 @@ import { Promises, readdirSync } from './pfs.js'; * On a case insensitive file system, the returned path might differ from the original path by character casing. * On a case sensitive file system, the returned path will always be identical to the original path. * In case of errors, null is returned. But you cannot use this function to verify that a path exists. - * realcaseSync does not handle '..' or '.' path segments and it does not take the locale into account. + * realcase does not handle '..' or '.' path segments and it does not take the locale into account. */ -export function realcaseSync(path: string): string | null { - if (isLinux) { - // This method is unsupported on OS that have case sensitive - // file system where the same path can exist in different forms - // (see also https://github.com/microsoft/vscode/issues/139709) - return path; - } - - const dir = dirname(path); - if (path === dir) { // end recursion - return path; - } - - const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase(); - try { - const entries = readdirSync(dir); - const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search - if (found.length === 1) { - // on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition - const prefix = realcaseSync(dir); // recurse - if (prefix) { - return join(prefix, found[0]); - } - } else if (found.length > 1) { - // must be a case sensitive $filesystem - const ix = found.indexOf(name); - if (ix >= 0) { // case sensitive - const prefix = realcaseSync(dir); // recurse - if (prefix) { - return join(prefix, found[ix]); - } - } - } - } catch (error) { - // silently ignore error - } - - return null; -} - export async function realcase(path: string, token?: CancellationToken): Promise { if (isLinux) { // This method is unsupported on OS that have case sensitive diff --git a/code/src/vs/base/parts/ipc/common/ipc.ts b/code/src/vs/base/parts/ipc/common/ipc.ts index 5b51cd2ae90..92bb92c78c6 100644 --- a/code/src/vs/base/parts/ipc/common/ipc.ts +++ b/code/src/vs/base/parts/ipc/common/ipc.ts @@ -649,7 +649,7 @@ export class ChannelClient implements IChannelClient, IDisposable { }); return result.finally(() => { - disposable.dispose(); + disposable?.dispose(); // Seen as undefined in tests. this.activeRequests.delete(disposableWithRequestCancel); }); } diff --git a/code/src/vs/base/parts/sandbox/electron-sandbox/preload.ts b/code/src/vs/base/parts/sandbox/electron-sandbox/preload.ts index ba5476ffb2c..7ff6c24d2c7 100644 --- a/code/src/vs/base/parts/sandbox/electron-sandbox/preload.ts +++ b/code/src/vs/base/parts/sandbox/electron-sandbox/preload.ts @@ -9,7 +9,7 @@ const { ipcRenderer, webFrame, contextBridge, webUtils } = require('electron'); - type ISandboxConfiguration = import('vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; + type ISandboxConfiguration = import('../common/sandboxTypes.js').ISandboxConfiguration; //#region Utilities diff --git a/code/src/vs/base/test/browser/markdownRenderer.test.ts b/code/src/vs/base/test/browser/markdownRenderer.test.ts index 6ec296beb4a..3263774598e 100644 --- a/code/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/code/src/vs/base/test/browser/markdownRenderer.test.ts @@ -692,6 +692,27 @@ suite('MarkdownRenderer', () => { const completeTokens = marked.marked.lexer(incomplete + '\`](https://microsoft.com)'); assert.deepStrictEqual(newTokens, completeTokens); }); + + test('list with incomplete subitem', () => { + const incomplete = `1. list item one + - `; + const tokens = marked.marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.marked.lexer(incomplete + ' '); + assert.deepStrictEqual(newTokens, completeTokens); + }); + + test('list with incomplete nested subitem', () => { + const incomplete = `1. list item one + - item 2 + - `; + const tokens = marked.marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.marked.lexer(incomplete + ' '); + assert.deepStrictEqual(newTokens, completeTokens); + }); }); suite('codespan', () => { diff --git a/code/src/vs/base/test/common/assert.test.ts b/code/src/vs/base/test/common/assert.test.ts index 11d99e8f2b3..bc5b7781510 100644 --- a/code/src/vs/base/test/common/assert.test.ts +++ b/code/src/vs/base/test/common/assert.test.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ok } from '../../common/assert.js'; +import { ok, assert as commonAssert } from '../../common/assert.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { CancellationError, ReadonlyError } from '../../common/errors.js'; suite('Assert', () => { test('ok', () => { @@ -33,5 +34,70 @@ suite('Assert', () => { ok(5); }); + suite('throws a provided error object', () => { + test('generic error', () => { + const originalError = new Error('Oh no!'); + + try { + commonAssert( + false, + originalError, + ); + } catch (thrownError) { + assert.strictEqual( + thrownError, + originalError, + 'Must throw the provided error instance.', + ); + + assert.strictEqual( + thrownError.message, + 'Oh no!', + 'Must throw the provided error instance.', + ); + } + }); + + test('cancellation error', () => { + const originalError = new CancellationError(); + + try { + commonAssert( + false, + originalError, + ); + } catch (thrownError) { + assert.strictEqual( + thrownError, + originalError, + 'Must throw the provided error instance.', + ); + } + }); + + test('readonly error', () => { + const originalError = new ReadonlyError('World'); + + try { + commonAssert( + false, + originalError, + ); + } catch (thrownError) { + assert.strictEqual( + thrownError, + originalError, + 'Must throw the provided error instance.', + ); + + assert.strictEqual( + thrownError.message, + 'World is read-only and cannot be changed', + 'Must throw the provided error instance.', + ); + } + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/code/src/vs/base/test/common/cancelPreviousCalls.test.ts b/code/src/vs/base/test/common/cancelPreviousCalls.test.ts new file mode 100644 index 00000000000..d5432efe313 --- /dev/null +++ b/code/src/vs/base/test/common/cancelPreviousCalls.test.ts @@ -0,0 +1,303 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Disposable } from '../../common/lifecycle.js'; +import { CancellationToken } from '../../common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { cancelPreviousCalls } from '../../common/decorators/cancelPreviousCalls.js'; + +suite('cancelPreviousCalls decorator', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + class MockDisposable extends Disposable { + /** + * Arguments that the {@linkcode doSomethingAsync} method was called with. + */ + private readonly callArgs1: ([number, string, CancellationToken | undefined])[] = []; + + /** + * Arguments that the {@linkcode doSomethingElseAsync} method was called with. + */ + private readonly callArgs2: ([number, string, CancellationToken | undefined])[] = []; + + /** + * Returns the arguments that the {@linkcode doSomethingAsync} method was called with. + */ + public get callArguments1() { + return this.callArgs1; + } + + /** + * Returns the arguments that the {@linkcode doSomethingElseAsync} method was called with. + */ + public get callArguments2() { + return this.callArgs2; + } + + @cancelPreviousCalls + async doSomethingAsync(arg1: number, arg2: string, cancellationToken?: CancellationToken): Promise { + this.callArgs1.push([arg1, arg2, cancellationToken]); + + await new Promise(resolve => setTimeout(resolve, 25)); + } + + @cancelPreviousCalls + async doSomethingElseAsync(arg1: number, arg2: string, cancellationToken?: CancellationToken): Promise { + this.callArgs2.push([arg1, arg2, cancellationToken]); + + await new Promise(resolve => setTimeout(resolve, 25)); + } + } + + test('should call method with CancellationToken', async () => { + const instance = disposables.add(new MockDisposable()); + + await instance.doSomethingAsync(1, 'foo'); + + const callArguments = instance.callArguments1; + assert.strictEqual( + callArguments.length, + 1, + `The 'doSomethingAsync' method must be called just once.`, + ); + + const args = callArguments[0]; + assert( + args.length === 3, + `The 'doSomethingAsync' method must be called with '3' arguments, got '${args.length}'.`, + ); + + const arg1 = args[0]; + const arg2 = args[1]; + const arg3 = args[2]; + + assert.strictEqual( + arg1, + 1, + `The 'doSomethingAsync' method call must have the correct 1st argument.`, + ); + + assert.strictEqual( + arg2, + 'foo', + `The 'doSomethingAsync' method call must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(arg3), + `The last argument of the 'doSomethingAsync' method must be a 'CancellationToken', got '${arg3}'.`, + ); + + assert( + arg3.isCancellationRequested === false, + `The 'CancellationToken' argument must not yet be cancelled.`, + ); + + assert( + instance.callArguments2.length === 0, + `The 'doSomethingElseAsync' method must not be called.`, + ); + }); + + test('cancel token of the previous call when method is called again', async () => { + const instance = disposables.add(new MockDisposable()); + + instance.doSomethingAsync(1, 'foo'); + await new Promise(resolve => setTimeout(resolve, 10)); + instance.doSomethingAsync(2, 'bar'); + + const callArguments = instance.callArguments1; + assert.strictEqual( + callArguments.length, + 2, + `The 'doSomethingAsync' method must be called twice.`, + ); + + const call1Args = callArguments[0]; + assert( + call1Args.length === 3, + `The first call of the 'doSomethingAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call1Args[0], + 1, + `The first call of the 'doSomethingAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call1Args[1], + 'foo', + `The first call of the 'doSomethingAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call1Args[2]), + `The first call of the 'doSomethingAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call1Args[2].isCancellationRequested === true, + `The 'CancellationToken' of the first call must be cancelled.`, + ); + + const call2Args = callArguments[1]; + assert( + call2Args.length === 3, + `The second call of the 'doSomethingAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call2Args[0], + 2, + `The second call of the 'doSomethingAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call2Args[1], + 'bar', + `The second call of the 'doSomethingAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call2Args[2]), + `The second call of the 'doSomethingAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call2Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the second call must be cancelled.`, + ); + + assert( + instance.callArguments2.length === 0, + `The 'doSomethingElseAsync' method must not be called.`, + ); + }); + + test('different method calls must not interfere with each other', async () => { + const instance = disposables.add(new MockDisposable()); + + instance.doSomethingAsync(10, 'baz'); + await new Promise(resolve => setTimeout(resolve, 10)); + instance.doSomethingElseAsync(25, 'qux'); + + assert.strictEqual( + instance.callArguments1.length, + 1, + `The 'doSomethingAsync' method must be called once.`, + ); + + const call1Args = instance.callArguments1[0]; + assert( + call1Args.length === 3, + `The first call of the 'doSomethingAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call1Args[0], + 10, + `The first call of the 'doSomethingAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call1Args[1], + 'baz', + `The first call of the 'doSomethingAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call1Args[2]), + `The first call of the 'doSomethingAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call1Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the first call must not be cancelled.`, + ); + + assert.strictEqual( + instance.callArguments2.length, + 1, + `The 'doSomethingElseAsync' method must be called once.`, + ); + + const call2Args = instance.callArguments2[0]; + assert( + call2Args.length === 3, + `The first call of the 'doSomethingElseAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call2Args[0], + 25, + `The first call of the 'doSomethingElseAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call2Args[1], + 'qux', + `The first call of the 'doSomethingElseAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call2Args[2]), + `The first call of the 'doSomethingElseAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call2Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the second call must be cancelled.`, + ); + + instance.doSomethingElseAsync(105, 'uxi'); + + assert.strictEqual( + instance.callArguments1.length, + 1, + `The 'doSomethingAsync' method must be called once.`, + ); + + assert.strictEqual( + instance.callArguments2.length, + 2, + `The 'doSomethingElseAsync' method must be called twice.`, + ); + + assert( + call1Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the first call must not be cancelled.`, + ); + + const call3Args = instance.callArguments2[1]; + assert( + CancellationToken.isCancellationToken(call3Args[2]), + `The last argument of the second call of the 'doSomethingElseAsync' method must be a 'CancellationToken'.`, + ); + + assert( + call2Args[2].isCancellationRequested, + `The 'CancellationToken' of the first call must be cancelled.`, + ); + + assert( + call3Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the second call must not be cancelled.`, + ); + + assert.strictEqual( + call3Args[0], + 105, + `The second call of the 'doSomethingElseAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call3Args[1], + 'uxi', + `The second call of the 'doSomethingElseAsync' method must have the correct 2nd argument.`, + ); + }); +}); diff --git a/code/src/vs/base/test/common/color.test.ts b/code/src/vs/base/test/common/color.test.ts index c0f439d744e..116ebba4119 100644 --- a/code/src/vs/base/test/common/color.test.ts +++ b/code/src/vs/base/test/common/color.test.ts @@ -118,40 +118,40 @@ suite('Color', () => { }); }); - suite('toNumber24Bit', () => { + suite('toNumber32Bit', () => { test('alpha channel', () => { - assert.deepStrictEqual(Color.fromHex('#00000000').toNumber24Bit(), 0x00000000); - assert.deepStrictEqual(Color.fromHex('#00000080').toNumber24Bit(), 0x00000080); - assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber24Bit(), 0x000000FF); + assert.deepStrictEqual(Color.fromHex('#00000000').toNumber32Bit(), 0x00000000); + assert.deepStrictEqual(Color.fromHex('#00000080').toNumber32Bit(), 0x00000080); + assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber32Bit(), 0x000000FF); }); test('opaque', () => { - assert.deepStrictEqual(Color.fromHex('#000000').toNumber24Bit(), 0x000000FF); - assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber24Bit(), 0xFFFFFFFF); - assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber24Bit(), 0xFF0000FF); - assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber24Bit(), 0x00FF00FF); - assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber24Bit(), 0x0000FFFF); - assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber24Bit(), 0xFFFF00FF); - assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber24Bit(), 0x00FFFFFF); - assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber24Bit(), 0xFF00FFFF); - assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber24Bit(), 0xC0C0C0FF); - assert.deepStrictEqual(Color.fromHex('#808080').toNumber24Bit(), 0x808080FF); - assert.deepStrictEqual(Color.fromHex('#800000').toNumber24Bit(), 0x800000FF); - assert.deepStrictEqual(Color.fromHex('#808000').toNumber24Bit(), 0x808000FF); - assert.deepStrictEqual(Color.fromHex('#008000').toNumber24Bit(), 0x008000FF); - assert.deepStrictEqual(Color.fromHex('#800080').toNumber24Bit(), 0x800080FF); - assert.deepStrictEqual(Color.fromHex('#008080').toNumber24Bit(), 0x008080FF); - assert.deepStrictEqual(Color.fromHex('#000080').toNumber24Bit(), 0x000080FF); - assert.deepStrictEqual(Color.fromHex('#010203').toNumber24Bit(), 0x010203FF); - assert.deepStrictEqual(Color.fromHex('#040506').toNumber24Bit(), 0x040506FF); - assert.deepStrictEqual(Color.fromHex('#070809').toNumber24Bit(), 0x070809FF); - assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber24Bit(), 0x0a0A0aFF); - assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber24Bit(), 0x0b0B0bFF); - assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber24Bit(), 0x0c0C0cFF); - assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber24Bit(), 0x0d0D0dFF); - assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber24Bit(), 0x0e0E0eFF); - assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber24Bit(), 0x0f0F0fFF); - assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber24Bit(), 0xa0A0a0FF); + assert.deepStrictEqual(Color.fromHex('#000000').toNumber32Bit(), 0x000000FF); + assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber32Bit(), 0xFFFFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber32Bit(), 0xFF0000FF); + assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber32Bit(), 0x00FF00FF); + assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber32Bit(), 0x0000FFFF); + assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber32Bit(), 0xFFFF00FF); + assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber32Bit(), 0x00FFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber32Bit(), 0xFF00FFFF); + assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber32Bit(), 0xC0C0C0FF); + assert.deepStrictEqual(Color.fromHex('#808080').toNumber32Bit(), 0x808080FF); + assert.deepStrictEqual(Color.fromHex('#800000').toNumber32Bit(), 0x800000FF); + assert.deepStrictEqual(Color.fromHex('#808000').toNumber32Bit(), 0x808000FF); + assert.deepStrictEqual(Color.fromHex('#008000').toNumber32Bit(), 0x008000FF); + assert.deepStrictEqual(Color.fromHex('#800080').toNumber32Bit(), 0x800080FF); + assert.deepStrictEqual(Color.fromHex('#008080').toNumber32Bit(), 0x008080FF); + assert.deepStrictEqual(Color.fromHex('#000080').toNumber32Bit(), 0x000080FF); + assert.deepStrictEqual(Color.fromHex('#010203').toNumber32Bit(), 0x010203FF); + assert.deepStrictEqual(Color.fromHex('#040506').toNumber32Bit(), 0x040506FF); + assert.deepStrictEqual(Color.fromHex('#070809').toNumber32Bit(), 0x070809FF); + assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber32Bit(), 0x0a0A0aFF); + assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber32Bit(), 0x0b0B0bFF); + assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber32Bit(), 0x0c0C0cFF); + assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber32Bit(), 0x0d0D0dFF); + assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber32Bit(), 0x0e0E0eFF); + assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber32Bit(), 0x0f0F0fFF); + assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber32Bit(), 0xa0A0a0FF); }); }); diff --git a/code/src/vs/base/test/common/event.test.ts b/code/src/vs/base/test/common/event.test.ts index 9e754ca833b..3e398675272 100644 --- a/code/src/vs/base/test/common/event.test.ts +++ b/code/src/vs/base/test/common/event.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { stub } from 'sinon'; -import { tail2 } from '../../common/arrays.js'; import { DeferredPromise, timeout } from '../../common/async.js'; import { CancellationToken } from '../../common/cancellation.js'; import { errorHandler, setUnexpectedErrorHandler } from '../../common/errors.js'; @@ -14,6 +13,7 @@ import { observableValue, transaction } from '../../common/observable.js'; import { MicrotaskDelay } from '../../common/symbols.js'; import { runWithFakedTimers } from './timeTravelScheduler.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { tail } from '../../common/arrays.js'; namespace Samples { @@ -405,8 +405,8 @@ suite('Event', function () { } assert.deepStrictEqual(allError.length, 5); - const [start, tail] = tail2(allError); - assert.ok(tail instanceof ListenerRefusalError); + const [start, rest] = tail(allError); + assert.ok(rest instanceof ListenerRefusalError); for (const item of start) { assert.ok(item instanceof ListenerLeakError); diff --git a/code/src/vs/base/test/common/map.test.ts b/code/src/vs/base/test/common/map.test.ts index aa424023885..895726ab312 100644 --- a/code/src/vs/base/test/common/map.test.ts +++ b/code/src/vs/base/test/common/map.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { BidirectionalMap, FourKeyMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, ThreeKeyMap, Touch, TwoKeyMap } from '../../common/map.js'; +import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, NKeyMap, ResourceMap, SetMap, Touch } from '../../common/map.js'; import { extUriIgnorePathCase } from '../../common/resources.js'; import { URI } from '../../common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; @@ -683,82 +683,14 @@ suite('SetMap', () => { }); }); -suite('TwoKeyMap', () => { +suite('NKeyMap', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('set and get', () => { - const map = new TwoKeyMap(); - map.set('a', 'b', 1); - map.set('a', 'c', 2); - map.set('b', 'c', 3); - assert.strictEqual(map.get('a', 'b'), 1); - assert.strictEqual(map.get('a', 'c'), 2); - assert.strictEqual(map.get('b', 'c'), 3); - assert.strictEqual(map.get('a', 'd'), undefined); - }); - - test('clear', () => { - const map = new TwoKeyMap(); - map.set('a', 'b', 1); - map.set('a', 'c', 2); - map.set('b', 'c', 3); - map.clear(); - assert.strictEqual(map.get('a', 'b'), undefined); - assert.strictEqual(map.get('a', 'c'), undefined); - assert.strictEqual(map.get('b', 'c'), undefined); - }); - - test('values', () => { - const map = new TwoKeyMap(); - map.set('a', 'b', 1); - map.set('a', 'c', 2); - map.set('b', 'c', 3); - assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); - }); -}); - -suite('ThreeKeyMap', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('set and get', () => { - const map = new ThreeKeyMap(); - map.set('a', 'b', 'c', 1); - map.set('a', 'c', 'd', 2); - map.set('b', 'c', 'e', 3); - assert.strictEqual(map.get('a', 'b', 'c'), 1); - assert.strictEqual(map.get('a', 'c', 'd'), 2); - assert.strictEqual(map.get('b', 'c', 'e'), 3); - assert.strictEqual(map.get('a', 'd', 'e'), undefined); - }); - - test('clear', () => { - const map = new ThreeKeyMap(); - map.set('a', 'b', 'c', 1); - map.set('a', 'c', 'd', 2); - map.set('b', 'c', 'e', 3); - map.clear(); - assert.strictEqual(map.get('a', 'b', 'c'), undefined); - assert.strictEqual(map.get('a', 'c', 'd'), undefined); - assert.strictEqual(map.get('b', 'c', 'e'), undefined); - }); - - test('values', () => { - const map = new ThreeKeyMap(); - map.set('a', 'b', 'c', 1); - map.set('a', 'c', 'd', 2); - map.set('b', 'c', 'e', 3); - assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); - }); -}); - -suite('FourKeyMap', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('set and get', () => { - const map = new FourKeyMap(); - map.set('a', 'b', 'c', 'd', 1); - map.set('a', 'c', 'c', 'd', 2); - map.set('b', 'e', 'f', 'g', 3); + const map = new NKeyMap(); + map.set(1, 'a', 'b', 'c', 'd'); + map.set(2, 'a', 'c', 'c', 'd'); + map.set(3, 'b', 'e', 'f', 'g'); assert.strictEqual(map.get('a', 'b', 'c', 'd'), 1); assert.strictEqual(map.get('a', 'c', 'c', 'd'), 2); assert.strictEqual(map.get('b', 'e', 'f', 'g'), 3); @@ -766,13 +698,41 @@ suite('FourKeyMap', () => { }); test('clear', () => { - const map = new FourKeyMap(); - map.set('a', 'b', 'c', 'd', 1); - map.set('a', 'c', 'c', 'd', 2); - map.set('b', 'e', 'f', 'g', 3); + const map = new NKeyMap(); + map.set(1, 'a', 'b', 'c', 'd'); + map.set(2, 'a', 'c', 'c', 'd'); + map.set(3, 'b', 'e', 'f', 'g'); map.clear(); assert.strictEqual(map.get('a', 'b', 'c', 'd'), undefined); assert.strictEqual(map.get('a', 'c', 'c', 'd'), undefined); assert.strictEqual(map.get('b', 'e', 'f', 'g'), undefined); }); + + test('values', () => { + const map = new NKeyMap(); + map.set(1, 'a', 'b', 'c', 'd'); + map.set(2, 'a', 'c', 'c', 'd'); + map.set(3, 'b', 'e', 'f', 'g'); + assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); + }); + + test('toString', () => { + const map = new NKeyMap(); + map.set(1, 'f', 'o', 'o'); + map.set(2, 'b', 'a', 'r'); + map.set(3, 'b', 'a', 'z'); + map.set(3, 'b', 'o', 'o'); + assert.strictEqual(map.toString(), [ + 'f: ', + ' o: ', + ' o: 1', + 'b: ', + ' a: ', + ' r: 2', + ' z: 3', + ' o: ', + ' o: 3', + '', + ].join('\n')); + }); }); diff --git a/code/src/vs/base/test/common/objectCache.test.ts b/code/src/vs/base/test/common/objectCache.test.ts new file mode 100644 index 00000000000..b4736890248 --- /dev/null +++ b/code/src/vs/base/test/common/objectCache.test.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { spy } from 'sinon'; +import { ObjectCache } from '../../common/objectCache.js'; +import { wait } from '../../../base/test/common/testUtils.js'; +import { ObservableDisposable } from '../../common/observableDisposable.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; + +/** + * Test object class. + */ +class TestObject = string> extends ObservableDisposable { + constructor( + public readonly ID: TKey, + ) { + super(); + } + + /** + * Check if this object is equal to another one. + */ + public equal(other: TestObject>): boolean { + return this.ID === other.ID; + } +} + +suite('ObjectCache', function () { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('get', () => { + /** + * Common test funtion to test core logic of the cache + * with provider test ID keys of some specific type. + * + * @param key1 Test key1. + * @param key2 Test key2. + */ + const testCoreLogic = async >(key1: TKey, key2: TKey) => { + const factory = spy(( + key: TKey, + ) => { + const result: TestObject = new TestObject(key); + + result.assertNotDisposed( + 'Object must not be disposed.', + ); + + return result; + }); + + const cache = disposables.add(new ObjectCache(factory)); + + /** + * Test the core logic of the cache using 2 objects. + */ + + const obj1 = cache.get(key1); + assert( + factory.calledOnceWithExactly(key1), + '[obj1] Must be called once with the correct arguments.', + ); + + assert( + obj1.ID === key1, + '[obj1] Returned object must have the correct ID.', + ); + + const obj2 = cache.get(key1); + assert( + factory.calledOnceWithExactly(key1), + '[obj2] Must be called once with the correct arguments.', + ); + + assert( + obj2.ID === key1, + '[obj2] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj2 && obj1.equal(obj2), + '[obj2] Returned object must be the same instance.', + ); + + factory.resetHistory(); + + const obj3 = cache.get(key2); + assert( + factory.calledOnceWithExactly(key2), + '[obj3] Must be called once with the correct arguments.', + ); + + assert( + obj3.ID === key2, + '[obj3] Returned object must have the correct ID.', + ); + + factory.resetHistory(); + + const obj4 = cache.get(key1); + assert( + factory.notCalled, + '[obj4] Factory must not be called.', + ); + + assert( + obj4.ID === key1, + '[obj4] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj4 && obj1.equal(obj4), + '[obj4] Returned object must be the same instance.', + ); + + factory.resetHistory(); + + /** + * Now test that the object is removed automatically from + * the cache when it is disposed. + */ + + obj3.dispose(); + // the object is removed from the cache asynchronously + // so add a small delay to ensure the object is removed + await wait(5); + + const obj5 = cache.get(key1); + assert( + factory.notCalled, + '[obj5] Factory must not be called.', + ); + + assert( + obj5.ID === key1, + '[obj5] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj5 && obj1.equal(obj5), + '[obj5] Returned object must be the same instance.', + ); + + factory.resetHistory(); + + /** + * Test that the previously disposed object is recreated + * on the new retrieval call. + */ + + const obj6 = cache.get(key2); + assert( + factory.calledOnceWithExactly(key2), + '[obj6] Must be called once with the correct arguments.', + ); + + assert( + obj6.ID === key2, + '[obj6] Returned object must have the correct ID.', + ); + }; + + test('strings as keys', async function () { + await testCoreLogic('key1', 'key2'); + }); + + test('numbers as keys', async function () { + await testCoreLogic(10, 17065); + }); + + test('objects as keys', async function () { + await testCoreLogic( + disposables.add(new TestObject({})), + disposables.add(new TestObject({})), + ); + }); + }); + + suite('remove', () => { + /** + * Common test funtion to test remove logic of the cache + * with provider test ID keys of some specific type. + * + * @param key1 Test key1. + * @param key2 Test key2. + */ + const testRemoveLogic = async >( + key1: TKey, + key2: TKey, + disposeOnRemove: boolean, + ) => { + const factory = spy(( + key: TKey, + ) => { + const result: TestObject = new TestObject(key); + + result.assertNotDisposed( + 'Object must not be disposed.', + ); + + return result; + }); + + // ObjectCache, TKey> + const cache = disposables.add(new ObjectCache(factory)); + + /** + * Test the core logic of the cache. + */ + + const obj1 = cache.get(key1); + assert( + factory.calledOnceWithExactly(key1), + '[obj1] Must be called once with the correct arguments.', + ); + + assert( + obj1.ID === key1, + '[obj1] Returned object must have the correct ID.', + ); + + factory.resetHistory(); + + const obj2 = cache.get(key2); + assert( + factory.calledOnceWithExactly(key2), + '[obj2] Must be called once with the correct arguments.', + ); + + assert( + obj2.ID === key2, + '[obj2] Returned object must have the correct ID.', + ); + + cache.remove(key2, disposeOnRemove); + + const object2Disposed = obj2.disposed; + + // ensure we don't leak undisposed object in the tests + if (!obj2.disposed) { + obj2.dispose(); + } + + assert( + object2Disposed === disposeOnRemove, + `[obj2] Removed object must be disposed: ${disposeOnRemove}.`, + ); + + factory.resetHistory(); + + /** + * Validate that another object is not disposed. + */ + + assert( + !obj1.disposed, + '[obj1] Object must not be disposed.', + ); + + const obj3 = cache.get(key1); + assert( + factory.notCalled, + '[obj3] Factory must not be called.', + ); + + assert( + obj3.ID === key1, + '[obj3] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj3 && obj1.equal(obj3), + '[obj3] Returned object must be the same instance.', + ); + + factory.resetHistory(); + }; + + test('strings as keys', async function () { + await testRemoveLogic('key1', 'key2', false); + await testRemoveLogic('some-key', 'another-key', true); + }); + + test('numbers as keys', async function () { + await testRemoveLogic(7, 2400700, false); + await testRemoveLogic(1090, 2654, true); + }); + + test('objects as keys', async function () { + await testRemoveLogic( + disposables.add(new TestObject(1)), + disposables.add(new TestObject(1)), + false, + ); + + await testRemoveLogic( + disposables.add(new TestObject(2)), + disposables.add(new TestObject(2)), + true, + ); + }); + }); + + test('throws if factory returns a disposed object', async function () { + const factory = ( + key: string, + ) => { + const result = new TestObject(key); + + if (key === 'key2') { + result.dispose(); + } + + // caution! explicit type casting below! + return result as TestObject & { disposed: false }; + }; + + // ObjectCache + const cache = disposables.add(new ObjectCache(factory)); + + assert.doesNotThrow(() => { + cache.get('key1'); + }); + + assert.throws(() => { + cache.get('key2'); + }); + }); +}); diff --git a/code/src/vs/base/test/common/observableDisposable.test.ts b/code/src/vs/base/test/common/observableDisposable.test.ts new file mode 100644 index 00000000000..24de6cc3ecc --- /dev/null +++ b/code/src/vs/base/test/common/observableDisposable.test.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { spy } from 'sinon'; +import { wait, waitRandom } from './testUtils.js'; +import { Disposable } from '../../common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { assertNotDisposed, ObservableDisposable } from '../../common/observableDisposable.js'; + +suite('ObservableDisposable', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('tracks `disposed` state', () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert( + object instanceof ObservableDisposable, + 'Object must be instance of ObservableDisposable.', + ); + + assert( + object instanceof Disposable, + 'Object must be instance of Disposable.', + ); + + assert( + !object.disposed, + 'Object must not be disposed yet.', + ); + + object.dispose(); + + assert( + object.disposed, + 'Object must be disposed.', + ); + }); + + suite('onDispose', () => { + test('fires the event on dispose', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert( + !object.disposed, + 'Object must not be disposed yet.', + ); + + const onDisposeSpy = spy(() => { }); + object.onDispose(onDisposeSpy); + + assert( + onDisposeSpy.notCalled, + '`onDispose` callback must not be called yet.', + ); + + await waitRandom(10); + + assert( + onDisposeSpy.notCalled, + '`onDispose` callback must not be called yet.', + ); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + /** + * Validate that the callback was called. + */ + + assert( + object.disposed, + 'Object must be disposed.', + ); + + assert( + onDisposeSpy.calledOnce, + '`onDispose` callback must be called.', + ); + + /** + * Validate that the callback is not called again. + */ + + object.dispose(); + object.dispose(); + await waitRandom(10); + object.dispose(); + + assert( + onDisposeSpy.calledOnce, + '`onDispose` callback must not be called again.', + ); + + assert( + object.disposed, + 'Object must be disposed.', + ); + }); + + test('executes callback immediately if already disposed', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object = new class extends ObservableDisposable { }(); + disposables.add(object); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + const onDisposeSpy = spy(() => { }); + object.onDispose(onDisposeSpy); + + assert( + onDisposeSpy.calledOnce, + '`onDispose` callback must be called immediately.', + ); + + await waitRandom(10); + + object.onDispose(onDisposeSpy); + + assert( + onDisposeSpy.calledTwice, + '`onDispose` callback must be called immediately the second time.', + ); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + assert( + onDisposeSpy.calledTwice, + '`onDispose` callback must not be called again on dispose.', + ); + }); + }); + + suite('asserts', () => { + test('not disposed (method)', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object: ObservableDisposable = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert.doesNotThrow(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + + await waitRandom(10); + + assert.doesNotThrow(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + assert.throws(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + + await waitRandom(10); + + assert.throws(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + }); + + test('not disposed (function)', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object: ObservableDisposable = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert.doesNotThrow(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + + await waitRandom(10); + + assert.doesNotThrow(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + assert.throws(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + + await waitRandom(10); + + assert.throws(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + }); + }); +}); diff --git a/code/src/vs/base/test/common/strings.test.ts b/code/src/vs/base/test/common/strings.test.ts index b075808571e..7c567df4b6f 100644 --- a/code/src/vs/base/test/common/strings.test.ts +++ b/code/src/vs/base/test/common/strings.test.ts @@ -155,6 +155,24 @@ suite('Strings', () => { assert.strictEqual(strings.lcut('............a', 10, '…'), '............a'); }); + suite('rcut', () => { + test('basic truncation', () => { + assert.strictEqual(strings.rcut('foo bar', 0), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 1), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 4), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 7), 'foo bar'); + assert.strictEqual(strings.rcut('test string 0.1.2.3', 3), 'test'); + }); + + test('truncation with suffix', () => { + assert.strictEqual(strings.rcut('foo bar', 0, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 1, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 4, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 7, '…'), 'foo bar'); + assert.strictEqual(strings.rcut('test string 0.1.2.3', 3, '…'), 'test…'); + }); + }); + test('escape', () => { assert.strictEqual(strings.escape(''), ''); assert.strictEqual(strings.escape('foo'), 'foo'); diff --git a/code/src/vs/base/test/common/testUtils.ts b/code/src/vs/base/test/common/testUtils.ts index 1e291422f41..8c059122f8c 100644 --- a/code/src/vs/base/test/common/testUtils.ts +++ b/code/src/vs/base/test/common/testUtils.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { randomInt } from '../../common/numbers.js'; + export function flakySuite(title: string, fn: () => void) /* Suite */ { return suite(title, function () { @@ -17,3 +19,34 @@ export function flakySuite(title: string, fn: () => void) /* Suite */ { fn.call(this); }); } + +/** + * Helper function that allows to await for a specified amount of time. + * @param ms The amount of time to wait in milliseconds. + */ +export const wait = (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +/** + * Helper function that allows to await for a random amount of time. + * @param maxMs The `maximum` amount of time to wait, in milliseconds. + * @param minMs [`optional`] The `minimum` amount of time to wait, in milliseconds. + */ +export const waitRandom = (maxMs: number, minMs: number = 0): Promise => { + return wait(randomInt(maxMs, minMs)); +}; + +/** + * (pseudo)Random boolean generator. + * + * ## Examples + * + * ```typsecript + * randomBoolean(); // generates either `true` or `false` + * ``` + * + */ +export const randomBoolean = (): boolean => { + return Math.random() > 0.5; +}; diff --git a/code/src/vs/base/test/node/extpath.test.ts b/code/src/vs/base/test/node/extpath.test.ts index 9dbdf1ac87e..c86f3cea0d1 100644 --- a/code/src/vs/base/test/node/extpath.test.ts +++ b/code/src/vs/base/test/node/extpath.test.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import assert from 'assert'; import { tmpdir } from 'os'; -import { realcase, realcaseSync, realpath, realpathSync } from '../../node/extpath.js'; +import { realcase, realpath, realpathSync } from '../../node/extpath.js'; import { Promises } from '../../node/pfs.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; import { flakySuite, getRandomTestPath } from './testUtils.js'; @@ -24,30 +24,6 @@ flakySuite('Extpath', () => { return Promises.rm(testDir); }); - test('realcaseSync', async () => { - - // assume case insensitive file system - if (process.platform === 'win32' || process.platform === 'darwin') { - const upper = testDir.toUpperCase(); - const real = realcaseSync(upper); - - if (real) { // can be null in case of permission errors - assert.notStrictEqual(real, upper); - assert.strictEqual(real.toUpperCase(), upper); - assert.strictEqual(real, testDir); - } - } - - // linux, unix, etc. -> assume case sensitive file system - else { - let real = realcaseSync(testDir); - assert.strictEqual(real, testDir); - - real = realcaseSync(testDir.toUpperCase()); - assert.strictEqual(real, testDir.toUpperCase()); - } - }); - test('realcase', async () => { // assume case insensitive file system diff --git a/code/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts b/code/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts index 35db4812515..17be24c02f0 100644 --- a/code/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts +++ b/code/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts @@ -7,9 +7,9 @@ (async function () { - type IBootstrapWindow = import('vs/platform/window/electron-sandbox/window.js').IBootstrapWindow; - type IProcessExplorerMain = import('vs/code/electron-sandbox/processExplorer/processExplorerMain.js').IProcessExplorerMain; - type ProcessExplorerWindowConfiguration = import('vs/platform/process/common/process.js').ProcessExplorerWindowConfiguration; + type IBootstrapWindow = import('../../../platform/window/electron-sandbox/window.js').IBootstrapWindow; + type IProcessExplorerMain = import('./processExplorerMain.js').IProcessExplorerMain; + type ProcessExplorerWindowConfiguration = import('../../../platform/process/common/process.js').ProcessExplorerWindowConfiguration; const bootstrapWindow: IBootstrapWindow = (window as any).MonacoBootstrapWindow; // defined by bootstrap-window.ts diff --git a/code/src/vs/code/electron-sandbox/workbench/workbench.ts b/code/src/vs/code/electron-sandbox/workbench/workbench.ts index daddd15eb07..47c3d28a6fc 100644 --- a/code/src/vs/code/electron-sandbox/workbench/workbench.ts +++ b/code/src/vs/code/electron-sandbox/workbench/workbench.ts @@ -10,10 +10,10 @@ // Add a perf entry right from the top performance.mark('code/didStartRenderer'); - type INativeWindowConfiguration = import('vs/platform/window/common/window.ts').INativeWindowConfiguration; - type IBootstrapWindow = import('vs/platform/window/electron-sandbox/window.js').IBootstrapWindow; - type IMainWindowSandboxGlobals = import('vs/base/parts/sandbox/electron-sandbox/globals.js').IMainWindowSandboxGlobals; - type IDesktopMain = import('vs/workbench/electron-sandbox/desktop.main.js').IDesktopMain; + type INativeWindowConfiguration = import('../../../platform/window/common/window.ts').INativeWindowConfiguration; + type IBootstrapWindow = import('../../../platform/window/electron-sandbox/window.js').IBootstrapWindow; + type IMainWindowSandboxGlobals = import('../../../base/parts/sandbox/electron-sandbox/globals.js').IMainWindowSandboxGlobals; + type IDesktopMain = import('../../../workbench/electron-sandbox/desktop.main.js').IDesktopMain; const bootstrapWindow: IBootstrapWindow = (window as any).MonacoBootstrapWindow; // defined by bootstrap-window.ts const preloadGlobals: IMainWindowSandboxGlobals = (window as any).vscode; // defined by preload.ts @@ -102,63 +102,67 @@ } // ensure there is enough space - layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth)); + layoutInfo.auxiliarySideBarWidth = Math.min(layoutInfo.auxiliarySideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth + layoutInfo.sideBarWidth)); + layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth + layoutInfo.auxiliarySideBarWidth)); // part: title - const titleDiv = document.createElement('div'); - titleDiv.style.position = 'absolute'; - titleDiv.style.width = '100%'; - titleDiv.style.height = `${layoutInfo.titleBarHeight}px`; - titleDiv.style.left = '0'; - titleDiv.style.top = '0'; - titleDiv.style.backgroundColor = `${colorInfo.titleBarBackground}`; - (titleDiv.style as any)['-webkit-app-region'] = 'drag'; - splash.appendChild(titleDiv); - - if (colorInfo.titleBarBorder && layoutInfo.titleBarHeight > 0) { - const titleBorder = document.createElement('div'); - titleBorder.style.position = 'absolute'; - titleBorder.style.width = '100%'; - titleBorder.style.height = '1px'; - titleBorder.style.left = '0'; - titleBorder.style.bottom = '0'; - titleBorder.style.borderBottom = `1px solid ${colorInfo.titleBarBorder}`; - titleDiv.appendChild(titleBorder); + if (layoutInfo.titleBarHeight > 0) { + const titleDiv = document.createElement('div'); + titleDiv.style.position = 'absolute'; + titleDiv.style.width = '100%'; + titleDiv.style.height = `${layoutInfo.titleBarHeight}px`; + titleDiv.style.left = '0'; + titleDiv.style.top = '0'; + titleDiv.style.backgroundColor = `${colorInfo.titleBarBackground}`; + (titleDiv.style as any)['-webkit-app-region'] = 'drag'; + splash.appendChild(titleDiv); + + if (colorInfo.titleBarBorder) { + const titleBorder = document.createElement('div'); + titleBorder.style.position = 'absolute'; + titleBorder.style.width = '100%'; + titleBorder.style.height = '1px'; + titleBorder.style.left = '0'; + titleBorder.style.bottom = '0'; + titleBorder.style.borderBottom = `1px solid ${colorInfo.titleBarBorder}`; + titleDiv.appendChild(titleBorder); + } } // part: activity bar - const activityDiv = document.createElement('div'); - activityDiv.style.position = 'absolute'; - activityDiv.style.width = `${layoutInfo.activityBarWidth}px`; - activityDiv.style.height = `calc(100% - ${layoutInfo.titleBarHeight + layoutInfo.statusBarHeight}px)`; - activityDiv.style.top = `${layoutInfo.titleBarHeight}px`; - if (layoutInfo.sideBarSide === 'left') { - activityDiv.style.left = '0'; - } else { - activityDiv.style.right = '0'; - } - activityDiv.style.backgroundColor = `${colorInfo.activityBarBackground}`; - splash.appendChild(activityDiv); - - if (colorInfo.activityBarBorder && layoutInfo.activityBarWidth > 0) { - const activityBorderDiv = document.createElement('div'); - activityBorderDiv.style.position = 'absolute'; - activityBorderDiv.style.width = '1px'; - activityBorderDiv.style.height = '100%'; - activityBorderDiv.style.top = '0'; + if (layoutInfo.activityBarWidth > 0) { + const activityDiv = document.createElement('div'); + activityDiv.style.position = 'absolute'; + activityDiv.style.width = `${layoutInfo.activityBarWidth}px`; + activityDiv.style.height = `calc(100% - ${layoutInfo.titleBarHeight + layoutInfo.statusBarHeight}px)`; + activityDiv.style.top = `${layoutInfo.titleBarHeight}px`; if (layoutInfo.sideBarSide === 'left') { - activityBorderDiv.style.right = '0'; - activityBorderDiv.style.borderRight = `1px solid ${colorInfo.activityBarBorder}`; + activityDiv.style.left = '0'; } else { - activityBorderDiv.style.left = '0'; - activityBorderDiv.style.borderLeft = `1px solid ${colorInfo.activityBarBorder}`; + activityDiv.style.right = '0'; + } + activityDiv.style.backgroundColor = `${colorInfo.activityBarBackground}`; + splash.appendChild(activityDiv); + + if (colorInfo.activityBarBorder) { + const activityBorderDiv = document.createElement('div'); + activityBorderDiv.style.position = 'absolute'; + activityBorderDiv.style.width = '1px'; + activityBorderDiv.style.height = '100%'; + activityBorderDiv.style.top = '0'; + if (layoutInfo.sideBarSide === 'left') { + activityBorderDiv.style.right = '0'; + activityBorderDiv.style.borderRight = `1px solid ${colorInfo.activityBarBorder}`; + } else { + activityBorderDiv.style.left = '0'; + activityBorderDiv.style.borderLeft = `1px solid ${colorInfo.activityBarBorder}`; + } + activityDiv.appendChild(activityBorderDiv); } - activityDiv.appendChild(activityBorderDiv); } // part: side bar (only when opening workspace/folder) - // folder or workspace -> status bar color, sidebar - if (configuration.workspace) { + if (configuration.workspace && layoutInfo.sideBarWidth > 0) { const sideDiv = document.createElement('div'); sideDiv.style.position = 'absolute'; sideDiv.style.width = `${layoutInfo.sideBarWidth}px`; @@ -172,7 +176,7 @@ sideDiv.style.backgroundColor = `${colorInfo.sideBarBackground}`; splash.appendChild(sideDiv); - if (colorInfo.sideBarBorder && layoutInfo.sideBarWidth > 0) { + if (colorInfo.sideBarBorder) { const sideBorderDiv = document.createElement('div'); sideBorderDiv.style.position = 'absolute'; sideBorderDiv.style.width = '1px'; @@ -189,28 +193,62 @@ } } - // part: statusbar - const statusDiv = document.createElement('div'); - statusDiv.style.position = 'absolute'; - statusDiv.style.width = '100%'; - statusDiv.style.height = `${layoutInfo.statusBarHeight}px`; - statusDiv.style.bottom = '0'; - statusDiv.style.left = '0'; - if (configuration.workspace && colorInfo.statusBarBackground) { - statusDiv.style.backgroundColor = colorInfo.statusBarBackground; - } else if (!configuration.workspace && colorInfo.statusBarNoFolderBackground) { - statusDiv.style.backgroundColor = colorInfo.statusBarNoFolderBackground; + // part: auxiliary sidebar + if (layoutInfo.auxiliarySideBarWidth > 0) { + const auxSideDiv = document.createElement('div'); + auxSideDiv.style.position = 'absolute'; + auxSideDiv.style.width = `${layoutInfo.auxiliarySideBarWidth}px`; + auxSideDiv.style.height = `calc(100% - ${layoutInfo.titleBarHeight + layoutInfo.statusBarHeight}px)`; + auxSideDiv.style.top = `${layoutInfo.titleBarHeight}px`; + if (layoutInfo.sideBarSide === 'left') { + auxSideDiv.style.right = '0'; + } else { + auxSideDiv.style.left = '0'; + } + auxSideDiv.style.backgroundColor = `${colorInfo.sideBarBackground}`; + splash.appendChild(auxSideDiv); + + if (colorInfo.sideBarBorder) { + const auxSideBorderDiv = document.createElement('div'); + auxSideBorderDiv.style.position = 'absolute'; + auxSideBorderDiv.style.width = '1px'; + auxSideBorderDiv.style.height = '100%'; + auxSideBorderDiv.style.top = '0'; + if (layoutInfo.sideBarSide === 'left') { + auxSideBorderDiv.style.left = '0'; + auxSideBorderDiv.style.borderLeft = `1px solid ${colorInfo.sideBarBorder}`; + } else { + auxSideBorderDiv.style.right = '0'; + auxSideBorderDiv.style.borderRight = `1px solid ${colorInfo.sideBarBorder}`; + } + auxSideDiv.appendChild(auxSideBorderDiv); + } } - splash.appendChild(statusDiv); - - if (colorInfo.statusBarBorder && layoutInfo.statusBarHeight > 0) { - const statusBorderDiv = document.createElement('div'); - statusBorderDiv.style.position = 'absolute'; - statusBorderDiv.style.width = '100%'; - statusBorderDiv.style.height = '1px'; - statusBorderDiv.style.top = '0'; - statusBorderDiv.style.borderTop = `1px solid ${colorInfo.statusBarBorder}`; - statusDiv.appendChild(statusBorderDiv); + + // part: statusbar + if (layoutInfo.statusBarHeight > 0) { + const statusDiv = document.createElement('div'); + statusDiv.style.position = 'absolute'; + statusDiv.style.width = '100%'; + statusDiv.style.height = `${layoutInfo.statusBarHeight}px`; + statusDiv.style.bottom = '0'; + statusDiv.style.left = '0'; + if (configuration.workspace && colorInfo.statusBarBackground) { + statusDiv.style.backgroundColor = colorInfo.statusBarBackground; + } else if (!configuration.workspace && colorInfo.statusBarNoFolderBackground) { + statusDiv.style.backgroundColor = colorInfo.statusBarNoFolderBackground; + } + splash.appendChild(statusDiv); + + if (colorInfo.statusBarBorder) { + const statusBorderDiv = document.createElement('div'); + statusBorderDiv.style.position = 'absolute'; + statusBorderDiv.style.width = '100%'; + statusBorderDiv.style.height = '1px'; + statusBorderDiv.style.top = '0'; + statusBorderDiv.style.borderTop = `1px solid ${colorInfo.statusBarBorder}`; + statusDiv.appendChild(statusBorderDiv); + } } window.document.body.appendChild(splash); diff --git a/code/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/code/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 563f563455a..fbbae411cdb 100644 --- a/code/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/code/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -43,7 +43,7 @@ import { InstantiationService } from '../../../platform/instantiation/common/ins import { ServiceCollection } from '../../../platform/instantiation/common/serviceCollection.js'; import { ILanguagePackService } from '../../../platform/languagePacks/common/languagePacks.js'; import { NativeLanguagePackService } from '../../../platform/languagePacks/node/languagePacks.js'; -import { ConsoleLogger, ILoggerService, ILogService } from '../../../platform/log/common/log.js'; +import { ConsoleLogger, ILoggerService, ILogService, LoggerGroup } from '../../../platform/log/common/log.js'; import { LoggerChannelClient } from '../../../platform/log/common/logIpc.js'; import product from '../../../platform/product/common/product.js'; import { IProductService } from '../../../platform/product/common/productService.js'; @@ -162,7 +162,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { instantiationService.invokeFunction(accessor => { const logService = accessor.get(ILogService); const telemetryService = accessor.get(ITelemetryService); - const userDataProfilesService = accessor.get(IUserDataProfilesService); // Log info logService.trace('sharedProcess configuration', JSON.stringify(this.configuration)); @@ -173,10 +172,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { // Error handler this.registerErrorHandler(logService); - // Report Profiles Info - this.reportProfilesInfo(telemetryService, userDataProfilesService); - this._register(userDataProfilesService.onDidChangeProfiles(() => this.reportProfilesInfo(telemetryService, userDataProfilesService))); - // Report Client OS/DE Info this.reportClientOSInfo(telemetryService, logService); }); @@ -219,7 +214,8 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(ILoggerService, loggerService); // Log - const logger = this._register(loggerService.createLogger('sharedprocess', { name: localize('sharedLog', "Shared") })); + const sharedLogGroup: LoggerGroup = { id: 'shared', name: localize('sharedLog', "Shared") }; + const logger = this._register(loggerService.createLogger('sharedprocess', { name: localize('sharedLog', "Shared"), group: sharedLogGroup })); const consoleLogger = this._register(new ConsoleLogger(logger.getLevel())); const logService = this._register(new LogService(logger, [consoleLogger])); services.set(ILogService, logService); @@ -273,7 +269,8 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { ]); // Request - const requestService = new RequestService(configurationService, environmentService, logService); + const networkLogger = this._register(loggerService.createLogger(`network-shared`, { name: localize('networkk', "Network"), group: sharedLogGroup })); + const requestService = new RequestService(configurationService, environmentService, this._register(new LogService(networkLogger))); services.set(IRequestService, requestService); // Checksum @@ -453,20 +450,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { }); } - private reportProfilesInfo(telemetryService: ITelemetryService, userDataProfilesService: IUserDataProfilesService): void { - type ProfilesInfoClassification = { - owner: 'sandy081'; - comment: 'Report profiles information'; - count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of profiles' }; - }; - type ProfilesInfoEvent = { - count: number; - }; - telemetryService.publicLog2('profilesInfo', { - count: userDataProfilesService.profiles.length - }); - } - private async reportClientOSInfo(telemetryService: ITelemetryService, logService: ILogService): Promise { if (isLinux) { const [releaseInfo, displayProtocol] = await Promise.all([ diff --git a/code/src/vs/code/node/cliProcessMain.ts b/code/src/vs/code/node/cliProcessMain.ts index e6f25b20b53..aff3b2da579 100644 --- a/code/src/vs/code/node/cliProcessMain.ts +++ b/code/src/vs/code/node/cliProcessMain.ts @@ -196,7 +196,7 @@ class CliMain extends Disposable { services.set(IUriIdentityService, new UriIdentityService(fileService)); // Request - const requestService = new RequestService(configurationService, environmentService, logService); + const requestService = new RequestService('local', configurationService, environmentService, logService); services.set(IRequestService, requestService); // Download Service diff --git a/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 116364099d0..537c23fec5f 100644 --- a/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -45,13 +45,13 @@ export class NativeEditContext extends AbstractEditContext { public readonly domNode: FastDomNode; private readonly _editContext: EditContext; private readonly _screenReaderSupport: ScreenReaderSupport; + private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1); // Overflow guard container private _parent: HTMLElement | undefined; private _decorations: string[] = []; private _primarySelection: Selection = new Selection(1, 1, 1, 1); - private _textStartPositionWithinEditor: Position = new Position(1, 1); private _targetWindowId: number = -1; private _scrollTop: number = 0; @@ -76,6 +76,7 @@ export class NativeEditContext extends AbstractEditContext { this.domNode.setClassName(`native-edit-context`); this.textArea = new FastDomNode(document.createElement('textarea')); this.textArea.setClassName('native-edit-context-textarea'); + this.textArea.setAttribute('tabindex', '-1'); this._updateDomAttributes(); @@ -290,17 +291,24 @@ export class NativeEditContext extends AbstractEditContext { private _updateEditContext(): void { const editContextState = this._getNewEditContextState(); + if (!editContextState) { + return; + } this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text); this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset); - this._textStartPositionWithinEditor = editContextState.textStartPositionWithinEditor; + this._editContextPrimarySelection = editContextState.editContextPrimarySelection; } private _emitTypeEvent(viewController: ViewController, e: TextUpdateEvent): void { if (!this._editContext) { return; } + if (!this._editContextPrimarySelection.equalsSelection(this._primarySelection)) { + this._updateEditContext(); + } const model = this._context.viewModel.model; - const offsetOfStartOfText = model.getOffsetAt(this._textStartPositionWithinEditor); + const startPositionOfEditContext = this._editContextStartPosition(); + const offsetOfStartOfText = model.getOffsetAt(startPositionOfEditContext); const offsetOfSelectionEnd = model.getOffsetAt(this._primarySelection.getEndPosition()); const offsetOfSelectionStart = model.getOffsetAt(this._primarySelection.getStartPosition()); const selectionEndOffset = offsetOfSelectionEnd - offsetOfStartOfText; @@ -348,34 +356,41 @@ export class NativeEditContext extends AbstractEditContext { } } - private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; textStartPositionWithinEditor: Position } { + private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; editContextPrimarySelection: Selection } | undefined { + const editContextPrimarySelection = this._primarySelection; const model = this._context.viewModel.model; - const primarySelectionStartLine = this._primarySelection.startLineNumber; - const primarySelectionEndLine = this._primarySelection.endLineNumber; + if (!model.isValidRange(editContextPrimarySelection)) { + return; + } + const primarySelectionStartLine = editContextPrimarySelection.startLineNumber; + const primarySelectionEndLine = editContextPrimarySelection.endLineNumber; const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine); const rangeOfText = new Range(primarySelectionStartLine, 1, primarySelectionEndLine, endColumnOfEndLineNumber); const text = model.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined); - const selectionStartOffset = this._primarySelection.startColumn - 1; - const selectionEndOffset = text.length + this._primarySelection.endColumn - endColumnOfEndLineNumber; - const textStartPositionWithinEditor = rangeOfText.getStartPosition(); + const selectionStartOffset = editContextPrimarySelection.startColumn - 1; + const selectionEndOffset = text.length + editContextPrimarySelection.endColumn - endColumnOfEndLineNumber; return { text, selectionStartOffset, selectionEndOffset, - textStartPositionWithinEditor + editContextPrimarySelection }; } + private _editContextStartPosition(): Position { + return new Position(this._editContextPrimarySelection.startLineNumber, 1); + } + private _handleTextFormatUpdate(e: TextFormatUpdateEvent): void { if (!this._editContext) { return; } const formats = e.getTextFormats(); - const textStartPositionWithinEditor = this._textStartPositionWithinEditor; + const editContextStartPosition = this._editContextStartPosition(); const decorations: IModelDeltaDecoration[] = []; formats.forEach(f => { const textModel = this._context.viewModel.model; - const offsetOfEditContextText = textModel.getOffsetAt(textStartPositionWithinEditor); + const offsetOfEditContextText = textModel.getOffsetAt(editContextStartPosition); const startPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeStart); const endPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeEnd); const decorationRange = Range.fromPositions(startPositionOfDecoration, endPositionOfDecoration); @@ -446,7 +461,7 @@ export class NativeEditContext extends AbstractEditContext { const offsetTransformer = new PositionOffsetTransformer(this._editContext.text); for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) { const editContextStartPosition = offsetTransformer.getPosition(offset); - const textStartLineOffsetWithinEditor = this._textStartPositionWithinEditor.lineNumber - 1; + const textStartLineOffsetWithinEditor = this._editContextPrimarySelection.startLineNumber - 1; const characterStartPosition = new Position(textStartLineOffsetWithinEditor + editContextStartPosition.lineNumber, editContextStartPosition.column); const characterEndPosition = characterStartPosition.delta(0, 1); const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition); diff --git a/code/src/vs/editor/browser/gpu/atlas/atlas.ts b/code/src/vs/editor/browser/gpu/atlas/atlas.ts index f4d87f2ae2a..17900d5273c 100644 --- a/code/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/code/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { FourKeyMap } from '../../../../base/common/map.js'; +import type { NKeyMap } from '../../../../base/common/map.js'; import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js'; /** @@ -106,4 +106,9 @@ export const enum UsagePreviewColors { Restricted = '#FF000088', } -export type GlyphMap = FourKeyMap; +export type GlyphMap = NKeyMap; diff --git a/code/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/code/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 9f527081e44..a4e5865c011 100644 --- a/code/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/code/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -8,13 +8,13 @@ import { CharCode } from '../../../../base/common/charCode.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { FourKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; import type { IGlyphRasterizer } from '../raster/raster.js'; -import { IdleTaskQueue } from '../taskQueue.js'; +import { IdleTaskQueue, type ITaskQueue } from '../taskQueue.js'; import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph, GlyphMap } from './atlas.js'; import { AllocatorType, TextureAtlasPage } from './textureAtlasPage.js'; @@ -24,7 +24,7 @@ export interface ITextureAtlasOptions { export class TextureAtlas extends Disposable { private _colorMap?: string[]; - private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); + private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); private readonly _warmedUpRasterizers = new Set(); private readonly _allocatorType: AllocatorType; @@ -50,7 +50,7 @@ export class TextureAtlas extends Disposable { * so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all * pages with a lower index do not contain the glyph. */ - private readonly _glyphPageIndex: GlyphMap = new FourKeyMap(); + private readonly _glyphPageIndex: GlyphMap = new NKeyMap(); private readonly _onDidDeleteGlyphs = this._register(new Emitter()); readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event; @@ -110,11 +110,16 @@ export class TextureAtlas extends Disposable { this._onDidDeleteGlyphs.fire(); } - getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { + getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number, x: number): Readonly { // TODO: Encode font size and family into key // Ignore metadata that doesn't affect the glyph tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + // Add x offset for sub-pixel rendering to the unused portion or tokenMetadata. This + // converts the decimal part of the x to a range from 0 to 9, where 0 = 0.0px x offset, + // 9 = 0.9px x offset + tokenMetadata |= Math.floor((x % 1) * 10); + // Warm up common glyphs if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); @@ -122,27 +127,27 @@ export class TextureAtlas extends Disposable { } // Try get the glyph, overflowing to a new page if necessary - return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, charMetadata); + return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, decorationStyleSetId); } - private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { - this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, pageIndex); + private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { + this._glyphPageIndex.set(pageIndex, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey); return ( - this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, charMetadata) + this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId) ?? (pageIndex + 1 < this._pages.length - ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, charMetadata) + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, decorationStyleSetId) : undefined) - ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, charMetadata) + ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, decorationStyleSetId) ); } - private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { + private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { if (this._pages.length >= TextureAtlas.maximumPageCount) { throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas.maximumPageCount}`); } this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); - this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, this._pages.length - 1); - return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, charMetadata)!; + this._glyphPageIndex.set(this._pages.length - 1, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey); + return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)!; } public getUsagePreview(): Promise { @@ -167,27 +172,33 @@ export class TextureAtlas extends Disposable { // Warm up using roughly the larger glyphs first to help optimize atlas allocation // A-Z for (let code = CharCode.A; code <= CharCode.Z; code++) { - taskQueue.enqueue(() => { - for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); - } - }); + for (const fgColor of colorMap.keys()) { + taskQueue.enqueue(() => { + for (let x = 0; x < 1; x += 0.1) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x); + } + }); + } } // a-z for (let code = CharCode.a; code <= CharCode.z; code++) { - taskQueue.enqueue(() => { - for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); - } - }); + for (const fgColor of colorMap.keys()) { + taskQueue.enqueue(() => { + for (let x = 0; x < 1; x += 0.1) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x); + } + }); + } } // Remaining ascii for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) { - taskQueue.enqueue(() => { - for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); - } - }); + for (const fgColor of colorMap.keys()) { + taskQueue.enqueue(() => { + for (let x = 0; x < 1; x += 0.1) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x); + } + }); + } } } } diff --git a/code/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/code/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 9c09181751a..1548f772e88 100644 --- a/code/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/code/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { FourKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js'; @@ -31,7 +31,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private readonly _canvas: OffscreenCanvas; get source(): OffscreenCanvas { return this._canvas; } - private readonly _glyphMap: GlyphMap = new FourKeyMap(); + private readonly _glyphMap: GlyphMap = new NKeyMap(); private readonly _glyphInOrderSet: Set = new Set(); get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); @@ -65,20 +65,20 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly | undefined { // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime // optimization as it's a very hot function - return this._glyphMap.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, charMetadata); + return this._glyphMap.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId); } - private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { + private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly | undefined { // Ensure the glyph can fit on the page if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { return undefined; } // Rasterize and allocate the glyph - const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, charMetadata, this._colorMap); + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, this._colorMap); const glyph = this._allocator.allocate(rasterizedGlyph); // Ensure the glyph was allocated @@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // Save the glyph - this._glyphMap.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, glyph); + this._glyphMap.set(glyph, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey); this._glyphInOrderSet.add(glyph); // Update page version and it's tracked used area @@ -101,7 +101,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla this._logService.trace('New glyph', { chars, tokenMetadata, - charMetadata, + decorationStyleSetId, rasterizedGlyph, glyph }); diff --git a/code/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/code/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index f9fa20dcfdb..e6340cac982 100644 --- a/code/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/code/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { TwoKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; import { ensureNonNullable } from '../gpuUtils.js'; import type { IRasterizedGlyph } from '../raster/raster.js'; import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; @@ -29,7 +29,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private readonly _ctx: OffscreenCanvasRenderingContext2D; private readonly _slabs: ITextureAtlasSlab[] = []; - private readonly _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + private readonly _activeSlabsByDims: NKeyMap = new NKeyMap(); private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = []; @@ -243,7 +243,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { }); } this._slabs.push(slab); - this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + this._activeSlabsByDims.set(slab, desiredSlabSize.w, desiredSlabSize.h); } const glyphsPerRow = Math.floor(this._slabW / slab.entryW); diff --git a/code/src/vs/editor/browser/gpu/contentSegmenter.ts b/code/src/vs/editor/browser/gpu/contentSegmenter.ts new file mode 100644 index 00000000000..1dab721b53a --- /dev/null +++ b/code/src/vs/editor/browser/gpu/contentSegmenter.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { safeIntl } from '../../../base/common/date.js'; +import type { GraphemeIterator } from '../../../base/common/strings.js'; +import type { ViewLineRenderingData } from '../../common/viewModel.js'; +import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; + +export interface IContentSegmenter { + /** + * Gets the content segment at an index within the line data's contents. This will be undefined + * when the index should not be rendered, ie. when it's part of an earlier segment like the tail + * end of an emoji, or when the line is not that long. + * @param index The index within the line data's content string. + */ + getSegmentAtIndex(index: number): string | undefined; + getSegmentData(index: number): Intl.SegmentData | undefined; +} + +export function createContentSegmenter(lineData: ViewLineRenderingData, options: ViewLineOptions): IContentSegmenter { + if (lineData.isBasicASCII && options.useMonospaceOptimizations) { + return new AsciiContentSegmenter(lineData); + } + return new GraphemeContentSegmenter(lineData); +} + +class AsciiContentSegmenter implements IContentSegmenter { + private readonly _content: string; + + constructor(lineData: ViewLineRenderingData) { + this._content = lineData.content; + } + + getSegmentAtIndex(index: number): string { + return this._content[index]; + } + + getSegmentData(index: number): Intl.SegmentData | undefined { + return undefined; + } +} + +/** + * This is a more modern version of {@link GraphemeIterator}, relying on browser APIs instead of a + * manual table approach. + */ +class GraphemeContentSegmenter implements IContentSegmenter { + private readonly _segments: (Intl.SegmentData | undefined)[] = []; + + constructor(lineData: ViewLineRenderingData) { + const content = lineData.content; + const segmenter = safeIntl.Segmenter(undefined, { granularity: 'grapheme' }); + const segmentedContent = Array.from(segmenter.segment(content)); + let segmenterIndex = 0; + + for (let x = 0; x < content.length; x++) { + const segment = segmentedContent[segmenterIndex]; + + // No more segments in the string (eg. an emoji is the last segment) + if (!segment) { + break; + } + + // The segment isn't renderable (eg. the tail end of an emoji) + if (segment.index !== x) { + this._segments.push(undefined); + continue; + } + + segmenterIndex++; + this._segments.push(segment); + } + } + + getSegmentAtIndex(index: number): string | undefined { + return this._segments[index]?.segment; + } + + getSegmentData(index: number): Intl.SegmentData | undefined { + return this._segments[index]; + } +} diff --git a/code/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/code/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts similarity index 94% rename from code/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts rename to code/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts index ae36165b2e6..43dbd85c87a 100644 --- a/code/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/code/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getActiveDocument } from '../../../base/browser/dom.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { $, getActiveDocument } from '../../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import './media/decorationCssRuleExtractor.css'; /** diff --git a/code/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/code/src/vs/editor/browser/gpu/css/decorationStyleCache.ts new file mode 100644 index 00000000000..1b1c07df163 --- /dev/null +++ b/code/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NKeyMap } from '../../../../base/common/map.js'; + +export interface IDecorationStyleSet { + /** + * A 24-bit number representing `color`. + */ + color: number | undefined; + /** + * Whether the text should be rendered in bold. + */ + bold: boolean | undefined; + /** + * A number between 0 and 1 representing the opacity of the text. + */ + opacity: number | undefined; +} + +export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { + /** + * A unique identifier for this set of styles. + */ + id: number; +} + +export class DecorationStyleCache { + + private _nextId = 1; + + private readonly _cacheById = new Map(); + private readonly _cacheByStyle = new NKeyMap(); + + getOrCreateEntry( + color: number | undefined, + bold: boolean | undefined, + opacity: number | undefined + ): number { + if (color === undefined && bold === undefined && opacity === undefined) { + return 0; + } + const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + if (result) { + return result.id; + } + const id = this._nextId++; + const entry = { + id, + color, + bold, + opacity, + }; + this._cacheById.set(id, entry); + this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + return id; + } + + getStyleSet(id: number): IDecorationStyleSet | undefined { + if (id === 0) { + return undefined; + } + return this._cacheById.get(id); + } +} diff --git a/code/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css b/code/src/vs/editor/browser/gpu/css/media/decorationCssRuleExtractor.css similarity index 100% rename from code/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css rename to code/src/vs/editor/browser/gpu/css/media/decorationCssRuleExtractor.css diff --git a/code/src/vs/editor/browser/gpu/gpu.ts b/code/src/vs/editor/browser/gpu/gpu.ts index cbd292a1b3d..500b167e59a 100644 --- a/code/src/vs/editor/browser/gpu/gpu.ts +++ b/code/src/vs/editor/browser/gpu/gpu.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { IDisposable } from '../../../base/common/lifecycle.js'; import type { ViewConfigurationChangedEvent, ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewLinesInsertedEvent, ViewScrollChangedEvent, ViewTokensChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; +import type { IGlyphRasterizer } from './raster/raster.js'; export const enum BindingId { GlyphInfo, @@ -17,9 +19,11 @@ export const enum BindingId { ScrollOffset, } -export interface IGpuRenderStrategy { +export interface IGpuRenderStrategy extends IDisposable { + readonly type: string; readonly wgsl: string; readonly bindGroupEntries: GPUBindGroupEntry[]; + readonly glyphRasterizer: IGlyphRasterizer; onLinesDeleted(e: ViewLinesDeletedEvent): boolean; onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean; @@ -32,6 +36,10 @@ export interface IGpuRenderStrategy { * Resets the render strategy, clearing all data and setting up for a new frame. */ reset(): void; - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; - draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult; + draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; +} + +export interface IGpuRenderStrategyUpdateResult { + localContentWidth: number; } diff --git a/code/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/code/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 159c3ad46c9..d06871cc24d 100644 --- a/code/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/code/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -5,9 +5,11 @@ import { memoize } from '../../../../base/common/decorators.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { isMacintosh } from '../../../../base/common/platform.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; +import { ViewGpuContext } from '../viewGpuContext.js'; import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; let nextId = 0; @@ -40,7 +42,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { fontBoundingBoxAscent: 0, fontBoundingBoxDescent: 0, }; - private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; charMetadata: number } = { chars: undefined, tokenMetadata: 0, charMetadata: 0 }; + private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; decorationStyleSetId: number } = { chars: undefined, tokenMetadata: 0, decorationStyleSetId: 0 }; + + // TODO: Support workbench.fontAliasing correctly + private _antiAliasing: 'subpixel' | 'greyscale' = isMacintosh ? 'greyscale' : 'subpixel'; constructor( readonly fontSize: number, @@ -52,7 +57,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const devicePixelFontSize = Math.ceil(this.fontSize * devicePixelRatio); this._canvas = new OffscreenCanvas(devicePixelFontSize * 3, devicePixelFontSize * 3); this._ctx = ensureNonNullable(this._canvas.getContext('2d', { - willReadFrequently: true + willReadFrequently: true, + alpha: this._antiAliasing === 'greyscale', })); this._ctx.textBaseline = 'top'; this._ctx.fillStyle = '#FFFFFF'; @@ -60,7 +66,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._textMetrics = this._ctx.measureText('A'); } - // TODO: Support drawing multiple fonts and sizes /** * Rasterizes a glyph. Note that the returned object is reused across different glyphs and * therefore is only safe for synchronous access. @@ -68,7 +73,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { public rasterizeGlyph( chars: string, tokenMetadata: number, - charMetadata: number, + decorationStyleSetId: number, colorMap: string[], ): Readonly { if (chars === '') { @@ -83,19 +88,19 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. - if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.charMetadata === charMetadata) { + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.decorationStyleSetId === decorationStyleSetId) { return this._workGlyph; } this._workGlyphConfig.chars = chars; this._workGlyphConfig.tokenMetadata = tokenMetadata; - this._workGlyphConfig.charMetadata = charMetadata; - return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap); + this._workGlyphConfig.decorationStyleSetId = decorationStyleSetId; + return this._rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, colorMap); } public _rasterizeGlyph( chars: string, - metadata: number, - charMetadata: number, + tokenMetadata: number, + decorationStyleSetId: number, colorMap: string[], ): Readonly { const devicePixelFontSize = Math.ceil(this.fontSize * this.devicePixelRatio); @@ -105,15 +110,35 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._canvas.height = canvasDim; } - // TODO: Support workbench.fontAliasing - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this._ctx.save(); + + // The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this + // is used to improve the spacing between rendered characters. + const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10; + + const bgId = TokenMetadata.getBackground(tokenMetadata); + const bg = colorMap[bgId]; + + const decorationStyleSet = ViewGpuContext.decorationStyleCache.getStyleSet(decorationStyleSetId); + + // When SPAA is used, the background color must be present to get the right glyph + if (this._antiAliasing === 'subpixel') { + this._ctx.fillStyle = bg; + this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); + } else { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } const fontSb = new StringBuilder(200); - const fontStyle = TokenMetadata.getFontStyle(metadata); + const fontStyle = TokenMetadata.getFontStyle(tokenMetadata); if (fontStyle & FontStyle.Italic) { fontSb.appendString('italic '); } - if (fontStyle & FontStyle.Bold) { + if (decorationStyleSet?.bold !== undefined) { + if (decorationStyleSet.bold) { + fontSb.appendString('bold '); + } + } else if (fontStyle & FontStyle.Bold) { fontSb.appendString('bold '); } fontSb.appendString(`${devicePixelFontSize}px ${this.fontFamily}`); @@ -125,16 +150,28 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; - if (charMetadata) { - this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '0')}`; + if (decorationStyleSet?.color !== undefined) { + this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`; } else { - this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; } this._ctx.textBaseline = 'top'; - this._ctx.fillText(chars, originX, originY); + if (decorationStyleSet?.opacity !== undefined) { + this._ctx.globalAlpha = decorationStyleSet.opacity; + } + + this._ctx.fillText(chars, originX + xSubPixelXOffset, originY); + this._ctx.restore(); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + if (this._antiAliasing === 'subpixel') { + const bgR = parseInt(bg.substring(1, 3), 16); + const bgG = parseInt(bg.substring(3, 5), 16); + const bgB = parseInt(bg.substring(5, 7), 16); + this._clearColor(imageData, bgR, bgG, bgB); + this._ctx.putImageData(imageData, 0, 0); + } this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { // x: textMetrics.actualBoundingBoxLeft, @@ -190,6 +227,17 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { return this._workGlyph; } + private _clearColor(imageData: ImageData, r: number, g: number, b: number) { + for (let offset = 0; offset < imageData.data.length; offset += 4) { + // Check exact match + if (imageData.data[offset] === r && + imageData.data[offset + 1] === g && + imageData.data[offset + 2] === b) { + imageData.data[offset + 3] = 0; + } + } + } + // TODO: Does this even need to happen when measure text is used? private _findGlyphBoundingBox(imageData: ImageData, outBoundingBox: IBoundingBox) { const height = this._canvas.height; @@ -254,4 +302,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } } } + + public getTextMetrics(text: string): TextMetrics { + return this._ctx.measureText(text); + } } diff --git a/code/src/vs/editor/browser/gpu/raster/raster.ts b/code/src/vs/editor/browser/gpu/raster/raster.ts index 989a4656415..eb6c56f1ffd 100644 --- a/code/src/vs/editor/browser/gpu/raster/raster.ts +++ b/code/src/vs/editor/browser/gpu/raster/raster.ts @@ -23,15 +23,17 @@ export interface IGlyphRasterizer { * emoji, etc. * @param tokenMetadata The token metadata of the glyph to rasterize. See {@link MetadataConsts} * for how this works. - * @param charMetadata The chracter metadata of the glyph to rasterize. + * @param decorationStyleSetId The id of the decoration style sheet. Zero means no decoration. * @param colorMap A theme's color map. */ rasterizeGlyph( chars: string, tokenMetadata: number, - charMetadata: number, + decorationStyleSetId: number, colorMap: string[], ): Readonly; + + getTextMetrics(text: string): TextMetrics; } /** diff --git a/code/src/vs/editor/browser/gpu/rectangleRenderer.ts b/code/src/vs/editor/browser/gpu/rectangleRenderer.ts index d0e087dbbc8..5be1db2f162 100644 --- a/code/src/vs/editor/browser/gpu/rectangleRenderer.ts +++ b/code/src/vs/editor/browser/gpu/rectangleRenderer.ts @@ -6,6 +6,7 @@ import { getActiveWindow } from '../../../base/browser/dom.js'; import { Event } from '../../../base/common/event.js'; import { IReference, MutableDisposable } from '../../../base/common/lifecycle.js'; +import type { IObservable } from '../../../base/common/observable.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import type { ViewScrollChangedEvent } from '../../common/viewEvents.js'; @@ -56,6 +57,8 @@ export class RectangleRenderer extends ViewEventHandler { constructor( private readonly _context: ViewContext, + private readonly _contentLeft: IObservable, + private readonly _devicePixelRatio: IObservable, private readonly _canvas: HTMLCanvasElement, private readonly _ctx: GPUCanvasContext, device: Promise, @@ -281,6 +284,10 @@ export class RectangleRenderer extends ViewEventHandler { pass.setVertexBuffer(0, this._vertexBuffer); pass.setBindGroup(0, this._bindGroup); + // Only draw the content area + const contentLeft = Math.ceil(this._contentLeft.get() * this._devicePixelRatio.get()); + pass.setScissorRect(contentLeft, 0, this._canvas.width - contentLeft, this._canvas.height); + pass.draw(quadVertices.length / 2, this._shapeCollection.entryCount); pass.end(); diff --git a/code/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts b/code/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts new file mode 100644 index 00000000000..59c7a3e3a53 --- /dev/null +++ b/code/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ViewEventHandler } from '../../../common/viewEventHandler.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; +import type { IGpuRenderStrategy, IGpuRenderStrategyUpdateResult } from '../gpu.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import type { ViewGpuContext } from '../viewGpuContext.js'; + +export abstract class BaseRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy { + + get glyphRasterizer() { return this._glyphRasterizer.value; } + + abstract type: string; + abstract wgsl: string; + abstract bindGroupEntries: GPUBindGroupEntry[]; + + constructor( + protected readonly _context: ViewContext, + protected readonly _viewGpuContext: ViewGpuContext, + protected readonly _device: GPUDevice, + protected readonly _glyphRasterizer: { value: GlyphRasterizer }, + ) { + super(); + + this._context.addEventHandler(this); + } + + abstract reset(): void; + abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult; + abstract draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; +} diff --git a/code/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/code/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts similarity index 69% rename from code/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts rename to code/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index a1d5358123e..71f6cc94228 100644 --- a/code/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/code/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -3,26 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from '../../../base/browser/dom.js'; -import { BugIndicatingError } from '../../../base/common/errors.js'; -import { MandatoryMutableDisposable } from '../../../base/common/lifecycle.js'; -import { EditorOption } from '../../common/config/editorOptions.js'; -import { CursorColumns } from '../../common/core/cursorColumns.js'; -import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; -import { ViewEventHandler } from '../../common/viewEventHandler.js'; -import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; -import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; -import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js'; -import type { ViewContext } from '../../common/viewModel/viewContext.js'; -import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; -import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { Color } from '../../../../base/common/color.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { CursorColumns } from '../../../common/core/cursorColumns.js'; +import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; +import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { InlineDecoration, ViewLineRenderingData } from '../../../common/viewModel.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; +import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; +import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; -import { BindingId, type IGpuRenderStrategy } from './gpu.js'; -import { GPULifecycle } from './gpuDisposable.js'; -import { quadVertices } from './gpuUtils.js'; -import { GlyphRasterizer } from './raster/glyphRasterizer.js'; -import { ViewGpuContext } from './viewGpuContext.js'; -import { Color } from '../../../base/common/color.js'; +import { BindingId, type IGpuRenderStrategyUpdateResult } from '../gpu.js'; +import { GPULifecycle } from '../gpuDisposable.js'; +import { quadVertices } from '../gpuUtils.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import { ViewGpuContext } from '../viewGpuContext.js'; +import { BaseRenderStrategy } from './baseRenderStrategy.js'; const enum Constants { IndicesPerCell = 6, @@ -46,11 +45,25 @@ type QueuedBufferEvent = ( ViewZonesChangedEvent ); -export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy { +/** + * A render strategy that tracks a large buffer, uploading only dirty lines as they change and + * leveraging heavy caching. This is the most performant strategy but has limitations around long + * lines and too many lines. + */ +export class FullFileRenderStrategy extends BaseRenderStrategy { - readonly wgsl: string = fullFileRenderStrategyWgsl; + /** + * The hard cap for line count that can be rendered by the GPU renderer. + */ + static readonly maxSupportedLines = 3000; - private readonly _glyphRasterizer: MandatoryMutableDisposable; + /** + * The hard cap for line columns that can be rendered by the GPU renderer. + */ + static readonly maxSupportedColumns = 200; + + readonly type = 'fullfile'; + readonly wgsl: string = fullFileRenderStrategyWgsl; private _cellBindBuffer!: GPUBuffer; @@ -79,20 +92,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } constructor( - private readonly _context: ViewContext, - private readonly _viewGpuContext: ViewGpuContext, - private readonly _device: GPUDevice, + context: ViewContext, + viewGpuContext: ViewGpuContext, + device: GPUDevice, + glyphRasterizer: { value: GlyphRasterizer }, ) { - super(); - - this._context.addEventHandler(this); - - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + super(context, viewGpuContext, device, glyphRasterizer); - this._glyphRasterizer = this._register(new MandatoryMutableDisposable(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get()))); - - const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + const bufferSize = FullFileRenderStrategy.maxSupportedLines * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, @@ -125,18 +132,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean { this._invalidateAllLines(); this._queueBufferUpdate(e); - - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = this._context.configuration.options.get(EditorOption.fontSize); - const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get(); - if ( - this._glyphRasterizer.value.fontFamily !== fontFamily || - this._glyphRasterizer.value.fontSize !== fontSize || - this._glyphRasterizer.value.devicePixelRatio !== devicePixelRatio - ) { - this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio); - } - return true; } @@ -237,18 +232,20 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend this._finalRenderedLine = 0; } - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult { // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the // loop. This is done so we don't need to trust the JIT compiler to do this optimization to // avoid potential additional blocking time in garbage collector which is a common cause of // dropped frames. let chars = ''; + let segment: string | undefined; + let charWidth = 0; let y = 0; let x = 0; let absoluteOffsetX = 0; let absoluteOffsetY = 0; - let xOffset = 0; + let tabXOffset = 0; let glyph: Readonly; let cellIndex = 0; @@ -256,17 +253,19 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokenEndIndex = 0; let tokenMetadata = 0; - let charMetadata = 0; + let decorationStyleSetBold: boolean | undefined; + let decorationStyleSetColor: number | undefined; + let decorationStyleSetOpacity: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; - let content: string = ''; let fillStartIndex = 0; let fillEndIndex = 0; let tokens: IViewLineTokens; const dpr = getActiveWindow().devicePixelRatio; + let contentSegmenter: IContentSegmenter; if (!this._scrollInitialized) { this.onScrollChanged(); @@ -275,10 +274,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // Update cell data const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); - const lineIndexCount = this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; + const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; - let dirtyLineStart = Number.MAX_SAFE_INTEGER; + let dirtyLineStart = 3000; let dirtyLineEnd = 0; // Handle any queued buffer updates @@ -299,9 +298,9 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } case ViewEventType.ViewLinesDeleted: { // Shift content below deleted line up - const deletedLineContentStartIndex = (e.fromLineNumber - 1) * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; - const deletedLineContentEndIndex = (e.toLineNumber) * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; - const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; + const deletedLineContentStartIndex = (e.fromLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; + const deletedLineContentEndIndex = (e.toLineNumber) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; + const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; cellBuffer.set(cellBuffer.subarray(deletedLineContentEndIndex), deletedLineContentStartIndex); // Zero out content on lines that are no longer valid @@ -320,8 +319,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // Only attempt to render lines that the GPU renderer can handle if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) { - fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; - fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; + fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); dirtyLineStart = Math.min(dirtyLineStart, y); @@ -339,8 +338,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend dirtyLineEnd = Math.max(dirtyLineEnd, y); lineData = viewportData.getViewLineRenderingData(y); - content = lineData.content; - xOffset = 0; + tabXOffset = 0; + + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); + charWidth = viewLineOptions.spaceWidth * dpr; + absoluteOffsetX = 0; tokens = lineData.tokens; tokenStartIndex = lineData.minColumn - 1; @@ -355,12 +357,23 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend tokenMetadata = tokens.getMetadata(tokenIndex); for (x = tokenStartIndex; x < tokenEndIndex; x++) { - // TODO: This needs to move to a dynamic long line rendering strategy - if (x > this._viewGpuContext.maxGpuCols) { + // Only render lines that do not exceed maximum columns + if (x > FullFileRenderStrategy.maxSupportedColumns) { break; } - chars = content.charAt(x); - charMetadata = 0; + segment = contentSegmenter.getSegmentAtIndex(x); + if (segment === undefined) { + continue; + } + chars = segment; + + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { + charWidth = this.glyphRasterizer.getTextMetrics(chars).width; + } + + decorationStyleSetColor = undefined; + decorationStyleSetBold = undefined; + decorationStyleSetOpacity = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -386,7 +399,23 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (!parsedColor) { throw new BugIndicatingError('Invalid color format ' + value); } - charMetadata = parsedColor.toNumber24Bit(); + decorationStyleSetColor = parsedColor.toNumber32Bit(); + break; + } + case 'font-weight': { + const parsedValue = parseCssFontWeight(value); + if (parsedValue >= 400) { + decorationStyleSetBold = true; + // TODO: Set bold (https://github.com/microsoft/vscode/issues/237584) + } else { + decorationStyleSetBold = false; + // TODO: Set normal (https://github.com/microsoft/vscode/issues/237584) + } + break; + } + case 'opacity': { + const parsedValue = parseCssOpacity(value); + decorationStyleSetOpacity = parsedValue; break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); @@ -397,19 +426,25 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (chars === ' ' || chars === '\t') { // Zero out glyph to ensure it doesn't get rendered - cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; + cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry); // Adjust xOffset for tab stops if (chars === '\t') { - xOffset = CursorColumns.nextRenderTabStop(x + xOffset, lineData.tabSize) - x - 1; + // Find the pixel offset between the current position and the next tab stop + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + absoluteOffsetX += charWidth * (tabXOffset - offsetBefore); + // Convert back to offset excluding x and the current character + tabXOffset -= x + 1; + } else { + absoluteOffsetX += charWidth; } continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, charMetadata); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); - // TODO: Support non-standard character widths - absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); absoluteOffsetY = Math.round( // Top of layout box (includes line height) viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr + @@ -423,19 +458,22 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend glyph.fontBoundingBoxAscent ); - cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; - cellBuffer[cellIndex + CellBufferInfo.Offset_X] = absoluteOffsetX; + cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX); cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; + + // Adjust the x pixel offset for the next character + absoluteOffsetX += charWidth; } tokenStartIndex = tokenEndIndex; } // Clear to end of line - fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + tokenEndIndex) * Constants.IndicesPerCell; - fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; + fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); upToDateLines.add(y); @@ -444,6 +482,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; // Only write when there is changed data + dirtyLineStart = Math.min(dirtyLineStart, FullFileRenderStrategy.maxSupportedLines); + dirtyLineEnd = Math.min(dirtyLineEnd, FullFileRenderStrategy.maxSupportedLines); if (dirtyLineStart <= dirtyLineEnd) { // Write buffer and swap it out to unblock writes this._device.queue.writeBuffer( @@ -460,7 +500,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; this._visibleObjectCount = visibleObjectCount; - return visibleObjectCount; + + return { + localContentWidth: absoluteOffsetX + }; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { @@ -471,7 +514,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend quadVertices.length / 2, this._visibleObjectCount, undefined, - (viewportData.startLineNumber - 1) * this._viewGpuContext.maxGpuCols + (viewportData.startLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns ); } @@ -485,3 +528,23 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend this._queuedBufferUpdates[1].push(e); } } + +function parseCssFontWeight(value: string) { + switch (value) { + case 'lighter': + case 'normal': return 400; + case 'bolder': + case 'bold': return 700; + } + return parseInt(value); +} + +function parseCssOpacity(value: string): number { + if (value.endsWith('%')) { + return parseFloat(value.substring(0, value.length - 1)) / 100; + } + if (value.match(/^\d+(?:\.\d*)/)) { + return parseFloat(value); + } + return 1; +} diff --git a/code/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts b/code/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.wgsl.ts similarity index 94% rename from code/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts rename to code/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.wgsl.ts index 7aab399457b..0e105241f11 100644 --- a/code/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts +++ b/code/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.wgsl.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextureAtlas } from './atlas/textureAtlas.js'; -import { TextureAtlasPage } from './atlas/textureAtlasPage.js'; -import { BindingId } from './gpu.js'; +import { TextureAtlas } from '../atlas/textureAtlas.js'; +import { TextureAtlasPage } from '../atlas/textureAtlasPage.js'; +import { BindingId } from '../gpu.js'; export const fullFileRenderStrategyWgsl = /*wgsl*/ ` struct GlyphInfo { diff --git a/code/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/code/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts new file mode 100644 index 00000000000..b71aaff05bc --- /dev/null +++ b/code/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -0,0 +1,428 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { Color } from '../../../../base/common/color.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { CursorColumns } from '../../../common/core/cursorColumns.js'; +import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; +import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { InlineDecoration, ViewLineRenderingData } from '../../../common/viewModel.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; +import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; +import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; +import { BindingId, type IGpuRenderStrategyUpdateResult } from '../gpu.js'; +import { GPULifecycle } from '../gpuDisposable.js'; +import { quadVertices } from '../gpuUtils.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import { ViewGpuContext } from '../viewGpuContext.js'; +import { BaseRenderStrategy } from './baseRenderStrategy.js'; +import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; + +const enum Constants { + IndicesPerCell = 6, + CellBindBufferCapacityIncrement = 32, + CellBindBufferInitialCapacity = 63, // Will be rounded up to nearest increment +} + +const enum CellBufferInfo { + FloatsPerEntry = 6, + BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4, + Offset_X = 0, + Offset_Y = 1, + Offset_Unused1 = 2, + Offset_Unused2 = 3, + GlyphIndex = 4, + TextureIndex = 5, +} + +/** + * A render strategy that uploads the content of the entire viewport every frame. + */ +export class ViewportRenderStrategy extends BaseRenderStrategy { + /** + * The hard cap for line columns that can be rendered by the GPU renderer. + */ + static readonly maxSupportedColumns = 2000; + + readonly type = 'viewport'; + readonly wgsl: string = fullFileRenderStrategyWgsl; + + private _cellBindBufferLineCapacity = Constants.CellBindBufferInitialCapacity; + private _cellBindBuffer!: GPUBuffer; + + /** + * The cell value buffers, these hold the cells and their glyphs. It's double buffers such that + * the thread doesn't block when one is being uploaded to the GPU. + */ + private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; + private _activeDoubleBufferIndex: 0 | 1 = 0; + + private _visibleObjectCount: number = 0; + + private _scrollOffsetBindBuffer: GPUBuffer; + private _scrollOffsetValueBuffer: Float32Array; + private _scrollInitialized: boolean = false; + + get bindGroupEntries(): GPUBindGroupEntry[] { + return [ + { binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } }, + { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } + ]; + } + + private readonly _onDidChangeBindGroupEntries = this._register(new Emitter()); + readonly onDidChangeBindGroupEntries = this._onDidChangeBindGroupEntries.event; + + constructor( + context: ViewContext, + viewGpuContext: ViewGpuContext, + device: GPUDevice, + glyphRasterizer: { value: GlyphRasterizer }, + ) { + super(context, viewGpuContext, device, glyphRasterizer); + + this._rebuildCellBuffer(this._cellBindBufferLineCapacity); + + const scrollOffsetBufferSize = 2; + this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco scroll offset buffer', + size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + })).object; + this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize); + } + + private _rebuildCellBuffer(lineCount: number) { + this._cellBindBuffer?.destroy(); + + // Increase in chunks so resizing a window by hand doesn't keep allocating and throwing away + const lineCountWithIncrement = (Math.floor(lineCount / Constants.CellBindBufferCapacityIncrement) + 1) * Constants.CellBindBufferCapacityIncrement; + + const bufferSize = lineCountWithIncrement * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco full file cell buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._cellValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + this._cellBindBufferLineCapacity = lineCountWithIncrement; + + this._onDidChangeBindGroupEntries.fire(); + } + + // #region Event handlers + + // The primary job of these handlers is to: + // 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when + // it's _within the viewport_. + // 2. Pass relevant events on to the render function so it can force certain line ranges to be + // re-rendered even if they're not in the viewport. For example when a view zone is added, + // there are lines that used to be visible but are no longer, so those ranges must be + // cleared and uploaded to the GPU. + + public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean { + return true; + } + + public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { + return true; + } + + public override onTokensChanged(e: ViewTokensChangedEvent): boolean { + return true; + } + + public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean { + return true; + } + + public override onLinesInserted(e: ViewLinesInsertedEvent): boolean { + return true; + } + + public override onLinesChanged(e: ViewLinesChangedEvent): boolean { + return true; + } + + public override onScrollChanged(e?: ViewScrollChangedEvent): boolean { + const dpr = getActiveWindow().devicePixelRatio; + this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr; + this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr; + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer); + return true; + } + + public override onThemeChanged(e: ViewThemeChangedEvent): boolean { + return true; + } + + public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean { + return true; + } + + public override onZonesChanged(e: ViewZonesChangedEvent): boolean { + return true; + } + + // #endregion + + reset() { + for (const bufferIndex of [0, 1]) { + // Zero out buffer and upload to GPU to prevent stale rows from rendering + const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]); + buffer.fill(0, 0, buffer.length); + this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength); + } + } + + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult { + // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the + // loop. This is done so we don't need to trust the JIT compiler to do this optimization to + // avoid potential additional blocking time in garbage collector which is a common cause of + // dropped frames. + + let chars = ''; + let segment: string | undefined; + let charWidth = 0; + let y = 0; + let x = 0; + let absoluteOffsetX = 0; + let absoluteOffsetY = 0; + let tabXOffset = 0; + let glyph: Readonly; + let cellIndex = 0; + + let tokenStartIndex = 0; + let tokenEndIndex = 0; + let tokenMetadata = 0; + + let decorationStyleSetBold: boolean | undefined; + let decorationStyleSetColor: number | undefined; + let decorationStyleSetOpacity: number | undefined; + + let lineData: ViewLineRenderingData; + let decoration: InlineDecoration; + let fillStartIndex = 0; + let fillEndIndex = 0; + + let tokens: IViewLineTokens; + + const dpr = getActiveWindow().devicePixelRatio; + let contentSegmenter: IContentSegmenter; + + if (!this._scrollInitialized) { + this.onScrollChanged(); + this._scrollInitialized = true; + } + + // Zero out cell buffer or rebuild if needed + if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) { + this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1); + } + const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); + cellBuffer.fill(0); + + const lineIndexCount = ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; + + for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { + + // Only attempt to render lines that the GPU renderer can handle + if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) { + continue; + } + + lineData = viewportData.getViewLineRenderingData(y); + tabXOffset = 0; + + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); + charWidth = viewLineOptions.spaceWidth * dpr; + absoluteOffsetX = 0; + + tokens = lineData.tokens; + tokenStartIndex = lineData.minColumn - 1; + tokenEndIndex = 0; + for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { + tokenEndIndex = tokens.getEndOffset(tokenIndex); + if (tokenEndIndex <= tokenStartIndex) { + // The faux indent part of the line should have no token type + continue; + } + + tokenMetadata = tokens.getMetadata(tokenIndex); + + for (x = tokenStartIndex; x < tokenEndIndex; x++) { + // Only render lines that do not exceed maximum columns + if (x > ViewportRenderStrategy.maxSupportedColumns) { + break; + } + segment = contentSegmenter.getSegmentAtIndex(x); + if (segment === undefined) { + continue; + } + chars = segment; + + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { + charWidth = this.glyphRasterizer.getTextMetrics(chars).width; + } + + decorationStyleSetColor = undefined; + decorationStyleSetBold = undefined; + decorationStyleSetOpacity = undefined; + + // Apply supported inline decoration styles to the cell metadata + for (decoration of lineData.inlineDecorations) { + // This is Range.strictContainsPosition except it works at the cell level, + // it's also inlined to avoid overhead. + if ( + (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) || + (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) || + (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) + ) { + continue; + } + + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName); + for (const rule of rules) { + for (const r of rule.style) { + const value = rule.styleMap.get(r)?.toString() ?? ''; + switch (r) { + case 'color': { + // TODO: This parsing and error handling should move into canRender so fallback + // to DOM works + const parsedColor = Color.Format.CSS.parse(value); + if (!parsedColor) { + throw new BugIndicatingError('Invalid color format ' + value); + } + decorationStyleSetColor = parsedColor.toNumber32Bit(); + break; + } + case 'font-weight': { + const parsedValue = parseCssFontWeight(value); + if (parsedValue >= 400) { + decorationStyleSetBold = true; + // TODO: Set bold (https://github.com/microsoft/vscode/issues/237584) + } else { + decorationStyleSetBold = false; + // TODO: Set normal (https://github.com/microsoft/vscode/issues/237584) + } + break; + } + case 'opacity': { + const parsedValue = parseCssOpacity(value); + decorationStyleSetOpacity = parsedValue; + break; + } + default: throw new BugIndicatingError('Unexpected inline decoration style'); + } + } + } + } + + if (chars === ' ' || chars === '\t') { + // Zero out glyph to ensure it doesn't get rendered + cellIndex = ((y - 1) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; + cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry); + // Adjust xOffset for tab stops + if (chars === '\t') { + // Find the pixel offset between the current position and the next tab stop + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + absoluteOffsetX += charWidth * (tabXOffset - offsetBefore); + // Convert back to offset excluding x and the current character + tabXOffset -= x + 1; + } else { + absoluteOffsetX += charWidth; + } + continue; + } + + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); + + absoluteOffsetY = Math.round( + // Top of layout box (includes line height) + viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr + + + // Delta from top of layout box (includes line height) to top of the inline box (no line height) + Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) + + + // Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn + // with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical + // baseline. + glyph.fontBoundingBoxAscent + ); + + cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX); + cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; + cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; + cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; + + // Adjust the x pixel offset for the next character + absoluteOffsetX += charWidth; + } + + tokenStartIndex = tokenEndIndex; + } + + // Clear to end of line + fillStartIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell; + fillEndIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; + cellBuffer.fill(0, fillStartIndex, fillEndIndex); + } + + const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; + + // This render strategy always uploads the whole viewport + this._device.queue.writeBuffer( + this._cellBindBuffer, + 0, + cellBuffer.buffer, + 0, + (viewportData.endLineNumber - viewportData.startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + ); + + this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; + + this._visibleObjectCount = visibleObjectCount; + return { + localContentWidth: absoluteOffsetX + }; + } + + draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { + if (this._visibleObjectCount <= 0) { + throw new BugIndicatingError('Attempt to draw 0 objects'); + } + pass.draw(quadVertices.length / 2, this._visibleObjectCount); + } +} + +function parseCssFontWeight(value: string) { + switch (value) { + case 'lighter': + case 'normal': return 400; + case 'bolder': + case 'bold': return 700; + } + return parseInt(value); +} + +function parseCssOpacity(value: string): number { + if (value.endsWith('%')) { + return parseFloat(value.substring(0, value.length - 1)) / 100; + } + if (value.match(/^\d+(?:\.\d*)/)) { + return parseFloat(value); + } + return 1; +} diff --git a/code/src/vs/editor/browser/gpu/taskQueue.ts b/code/src/vs/editor/browser/gpu/taskQueue.ts index 27d64d01fe2..a6cf28c8c86 100644 --- a/code/src/vs/editor/browser/gpu/taskQueue.ts +++ b/code/src/vs/editor/browser/gpu/taskQueue.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from '../../../base/browser/dom.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; /** * Copyright (c) 2022 The xterm.js authors. All rights reserved. * @license MIT */ -interface ITaskQueue { +export interface ITaskQueue extends IDisposable { /** * Adds a task to the queue which will run in a future idle callback. * To avoid perceivable stalls on the mainthread, tasks with heavy workload @@ -134,13 +134,7 @@ export class PriorityTaskQueue extends TaskQueue { } } -/** - * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's - * deadline given by the environment. The tasks will run in the order they are enqueued, but they - * will run some time later, and care should be taken to ensure they're non-urgent and will not - * introduce race conditions. - */ -export class IdleTaskQueue extends TaskQueue { +class IdleTaskQueueInternal extends TaskQueue { protected _requestCallback(callback: IdleRequestCallback): number { return getActiveWindow().requestIdleCallback(callback); } @@ -150,6 +144,16 @@ export class IdleTaskQueue extends TaskQueue { } } +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + * + * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks. + */ +export const IdleTaskQueue = ('requestIdleCallback' in getActiveWindow()) ? IdleTaskQueueInternal : PriorityTaskQueue; + /** * An object that tracks a single debounced task that will run on the next idle frame. When called * multiple times, only the last set task will run. diff --git a/code/src/vs/editor/browser/gpu/viewGpuContext.ts b/code/src/vs/editor/browser/gpu/viewGpuContext.ts index 54e4a4892bd..3cf8238c6c6 100644 --- a/code/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/code/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -19,33 +19,25 @@ import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; -import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; +import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js'; import { Event } from '../../../base/common/event.js'; -import type { IEditorOptions } from '../../common/config/editorOptions.js'; +import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js'; import { InlineDecorationType } from '../../common/viewModel.js'; - -const enum GpuRenderLimits { - maxGpuLines = 3000, - maxGpuCols = 200, -} +import { DecorationStyleCache } from './css/decorationStyleCache.js'; +import { ViewportRenderStrategy } from './renderStrategy/viewportRenderStrategy.js'; export class ViewGpuContext extends Disposable { /** - * The temporary hard cap for lines rendered by the GPU renderer. This can be removed once more - * dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227091 - */ - readonly maxGpuLines = GpuRenderLimits.maxGpuLines; - - /** - * The temporary hard cap for line columns rendered by the GPU renderer. This can be removed - * once more dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227108 + * The hard cap for line columns rendered by the GPU renderer. */ - readonly maxGpuCols = GpuRenderLimits.maxGpuCols; + readonly maxGpuCols = ViewportRenderStrategy.maxSupportedColumns; readonly canvas: FastDomNode; + readonly scrollWidthElement: FastDomNode; readonly ctx: GPUCanvasContext; - readonly device: Promise; + static device: Promise; + static deviceSync: GPUDevice | undefined; readonly rectangleRenderer: RectangleRenderer; @@ -54,6 +46,11 @@ export class ViewGpuContext extends Disposable { return ViewGpuContext._decorationCssRuleExtractor; } + private static readonly _decorationStyleCache = new DecorationStyleCache(); + static get decorationStyleCache(): DecorationStyleCache { + return ViewGpuContext._decorationStyleCache; + } + private static _atlas: TextureAtlas | undefined; /** @@ -79,6 +76,7 @@ export class ViewGpuContext extends Disposable { readonly canvasDevicePixelDimensions: IObservable<{ width: number; height: number }>; readonly devicePixelRatio: IObservable; + readonly contentLeft: IObservable; constructor( context: ViewContext, @@ -90,6 +88,7 @@ export class ViewGpuContext extends Disposable { this.canvas = createFastDomNode(document.createElement('canvas')); this.canvas.setClassName('editorCanvas'); + this.scrollWidthElement = createFastDomNode(document.createElement('div')); // Adjust the canvas size to avoid drawing under the scroll bar this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => { @@ -102,20 +101,23 @@ export class ViewGpuContext extends Disposable { this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu')); - this.device = GPULifecycle.requestDevice((message) => { - const choices: IPromptChoice[] = [{ - label: nls.localize('editor.dom.render', "Use DOM-based rendering"), - run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'), - }]; - this._notificationService.prompt(Severity.Warning, message, choices); - }).then(ref => this._register(ref).object); - this.device.then(device => { - if (!ViewGpuContext._atlas) { - ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, device.limits.maxTextureDimension2D, undefined); - } - }); - - this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.canvas.domNode, this.ctx, this.device); + // Request the GPU device, we only want to do this a single time per window as it's async + // and can delay the initial render. + if (!ViewGpuContext.device) { + ViewGpuContext.device = GPULifecycle.requestDevice((message) => { + const choices: IPromptChoice[] = [{ + label: nls.localize('editor.dom.render', "Use DOM-based rendering"), + run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'), + }]; + this._notificationService.prompt(Severity.Warning, message, choices); + }).then(ref => { + ViewGpuContext.deviceSync = ref.object; + if (!ViewGpuContext._atlas) { + ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined); + } + return ref.object; + }); + } const dprObs = observableValue(this, getActiveWindow().devicePixelRatio); this._register(addDisposableListener(getActiveWindow(), 'resize', () => { @@ -135,6 +137,14 @@ export class ViewGpuContext extends Disposable { } )); this.canvasDevicePixelDimensions = canvasDevicePixelDimensions; + + const contentLeft = observableValue(this, 0); + this._register(this.configurationService.onDidChangeConfiguration(e => { + contentLeft.set(context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined); + })); + this.contentLeft = contentLeft; + + this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device); } /** @@ -148,9 +158,7 @@ export class ViewGpuContext extends Disposable { // Check if the line has simple attributes that aren't supported if ( data.containsRTL || - data.maxColumn > GpuRenderLimits.maxGpuCols || - data.continuesWithWrappedLine || - lineNumber >= GpuRenderLimits.maxGpuLines + data.maxColumn > this.maxGpuCols ) { return false; } @@ -170,7 +178,7 @@ export class ViewGpuContext extends Disposable { return false; } for (const r of rule.style) { - if (!gpuSupportedDecorationCssRules.includes(r)) { + if (!supportsCssRule(r, rule.style)) { return false; } } @@ -195,12 +203,9 @@ export class ViewGpuContext extends Disposable { if (data.containsRTL) { reasons.push('containsRTL'); } - if (data.maxColumn > GpuRenderLimits.maxGpuCols) { + if (data.maxColumn > this.maxGpuCols) { reasons.push('maxColumn > maxGpuCols'); } - if (data.continuesWithWrappedLine) { - reasons.push('continuesWithWrappedLine'); - } if (data.inlineDecorations.length > 0) { let supported = true; const problemTypes: InlineDecorationType[] = []; @@ -220,8 +225,8 @@ export class ViewGpuContext extends Disposable { return false; } for (const r of rule.style) { - if (!gpuSupportedDecorationCssRules.includes(r)) { - problemRules.push(r); + if (!supportsCssRule(r, rule.style)) { + problemRules.push(`${r}: ${rule.style[r as any]}`); return false; } } @@ -241,16 +246,25 @@ export class ViewGpuContext extends Disposable { reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`); } } - if (lineNumber >= GpuRenderLimits.maxGpuLines) { - reasons.push('lineNumber >= maxGpuLines'); - } return reasons; } } /** - * A list of fully supported decoration CSS rules that can be used in the GPU renderer. + * A list of supported decoration CSS rules that can be used in the GPU renderer. */ const gpuSupportedDecorationCssRules = [ 'color', + 'font-weight', + 'opacity', ]; + +function supportsCssRule(rule: string, style: CSSStyleDeclaration) { + if (!gpuSupportedDecorationCssRules.includes(rule)) { + return false; + } + // Check for values that aren't supported + switch (rule) { + default: return true; + } +} diff --git a/code/src/vs/editor/browser/observableCodeEditor.ts b/code/src/vs/editor/browser/observableCodeEditor.ts index ac1ab182fa4..e81a961a7f2 100644 --- a/code/src/vs/editor/browser/observableCodeEditor.ts +++ b/code/src/vs/editor/browser/observableCodeEditor.ts @@ -231,6 +231,7 @@ export class ObservableCodeEditor extends Disposable { public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); public readonly layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); + public readonly layoutInfoWidth = this.layoutInfo.map(l => l.width); public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); @@ -286,7 +287,14 @@ export class ObservableCodeEditor extends Disposable { start.read(reader); end.read(reader); const range = lineRange.read(reader); - const s = this.editor.getTopForLineNumber(range.startLineNumber) - this.scrollTop.read(reader); + const lineCount = this.model.read(reader)?.getLineCount(); + const s = ( + (typeof lineCount !== 'undefined' && range.startLineNumber > lineCount + ? this.editor.getBottomForLineNumber(lineCount) + : this.editor.getTopForLineNumber(range.startLineNumber) + ) + - this.scrollTop.read(reader) + ); const e = range.isEmpty ? s : (this.editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this.scrollTop.read(reader)); return new OffsetRange(s, e); }); @@ -304,8 +312,14 @@ export class ObservableCodeEditor extends Disposable { }, getId: () => contentWidgetId, allowEditorOverflow: false, - afterRender(position, coordinate) { - result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined); + afterRender: (position, coordinate) => { + const model = this._model.get(); + if (model && pos && pos.lineNumber > model.getLineCount()) { + // the position is after the last line + result.set(new Point(0, this.editor.getBottomForLineNumber(model.getLineCount()) - this.scrollTop.get()), undefined); + } else { + result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined); + } }, }; this.editor.addContentWidget(w); diff --git a/code/src/vs/editor/browser/rect.ts b/code/src/vs/editor/browser/rect.ts index 6de464d201a..09a93216f26 100644 --- a/code/src/vs/editor/browser/rect.ts +++ b/code/src/vs/editor/browser/rect.ts @@ -25,10 +25,10 @@ export class Rect { } public static hull(rects: Rect[]): Rect { - let left = Number.MAX_VALUE; - let top = Number.MAX_VALUE; - let right = Number.MIN_VALUE; - let bottom = Number.MIN_VALUE; + let left = Number.MAX_SAFE_INTEGER; + let top = Number.MAX_SAFE_INTEGER; + let right = Number.MIN_SAFE_INTEGER; + let bottom = Number.MIN_SAFE_INTEGER; for (const rect of rects) { left = Math.min(left, rect.left); @@ -133,4 +133,12 @@ export class Rect { withTop(top: number): Rect { return new Rect(this.left, top, this.right, this.bottom); } + + moveLeft(delta: number): Rect { + return new Rect(this.left - delta, this.top, this.right - delta, this.bottom); + } + + moveUp(delta: number): Rect { + return new Rect(this.left, this.top - delta, this.right, this.bottom - delta); + } } diff --git a/code/src/vs/editor/browser/view.ts b/code/src/vs/editor/browser/view.ts index 10ecd41bb16..e383a7c9b2c 100644 --- a/code/src/vs/editor/browser/view.ts +++ b/code/src/vs/editor/browser/view.ts @@ -63,6 +63,7 @@ import { NativeEditContext } from './controller/editContext/native/nativeEditCon import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js'; import { EditContext } from './controller/editContext/native/editContextFactory.js'; import { GpuMarkOverlay } from './viewParts/gpuMark/gpuMark.js'; +import { AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js'; export interface IContentWidgetData { @@ -101,6 +102,7 @@ export class View extends ViewEventHandler { private readonly _viewController: ViewController; private _experimentalEditContextEnabled: boolean; + private _accessibilitySupport: AccessibilitySupport; private _editContext: AbstractEditContext; private readonly _pointerHandler: PointerHandler; @@ -145,7 +147,8 @@ export class View extends ViewEventHandler { // Keyboard handler this._experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); - this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled); + this._accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); + this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled, this._accessibilitySupport); this._viewParts.push(this._editContext); @@ -253,6 +256,7 @@ export class View extends ViewEventHandler { this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); if (this._viewGpuContext) { this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); + this._linesContent.appendChild(this._viewGpuContext.scrollWidthElement); } this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()); @@ -274,10 +278,10 @@ export class View extends ViewEventHandler { this._pointerHandler = this._register(new PointerHandler(this._context, this._viewController, this._createPointerHandlerHelper())); } - private _instantiateEditContext(experimentalEditContextEnabled: boolean): AbstractEditContext { + private _instantiateEditContext(experimentalEditContextEnabled: boolean, accessibilitySupport: AccessibilitySupport): AbstractEditContext { const domNode = dom.getWindow(this._overflowGuardContainer.domNode); const isEditContextSupported = EditContext.supported(domNode); - if (experimentalEditContextEnabled && isEditContextSupported) { + if (experimentalEditContextEnabled && isEditContextSupported && accessibilitySupport !== AccessibilitySupport.Enabled) { return this._instantiationService.createInstance(NativeEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } else { return this._instantiationService.createInstance(TextAreaEditContext, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); @@ -286,14 +290,16 @@ export class View extends ViewEventHandler { private _updateEditContext(): void { const experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); - if (this._experimentalEditContextEnabled === experimentalEditContextEnabled) { + const accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); + if (this._experimentalEditContextEnabled === experimentalEditContextEnabled && this._accessibilitySupport === accessibilitySupport) { return; } this._experimentalEditContextEnabled = experimentalEditContextEnabled; + this._accessibilitySupport = accessibilitySupport; const isEditContextFocused = this._editContext.isFocused(); const indexOfEditContext = this._viewParts.indexOf(this._editContext); this._editContext.dispose(); - this._editContext = this._instantiateEditContext(experimentalEditContextEnabled); + this._editContext = this._instantiateEditContext(experimentalEditContextEnabled, accessibilitySupport); if (isEditContextFocused) { this._editContext.focus(); } diff --git a/code/src/vs/editor/browser/view/viewLayer.ts b/code/src/vs/editor/browser/view/viewLayer.ts index 7162a1bb939..ec5d8bce6f3 100644 --- a/code/src/vs/editor/browser/view/viewLayer.ts +++ b/code/src/vs/editor/browser/view/viewLayer.ts @@ -277,9 +277,20 @@ export class VisibleLinesCollection { return false; } - public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + public onFlushed(e: viewEvents.ViewFlushedEvent, flushDom?: boolean): boolean { + // No need to clear the dom node because a full .innerHTML will occur in + // ViewLayerRenderer._render, however the the fallbakc mechanism in the + // GPU renderer may cause this to be necessary as the .innerHTML call + // may not happen depending on the new state, leaving stale DOM nodes + // around. + if (flushDom) { + const start = this._linesCollection.getStartLineNumber(); + const end = this._linesCollection.getEndLineNumber(); + for (let i = start; i <= end; i++) { + this._linesCollection.getLine(i).getDomNode()?.remove(); + } + } this._linesCollection.flush(); - // No need to clear the dom node because a full .innerHTML will occur in ViewLayerRenderer._render return true; } diff --git a/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index dcce94f4963..6ae696f90dd 100644 --- a/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -5,7 +5,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js'; -import { ContentWidgetPositionPreference, IContentWidget } from '../../editorBrowser.js'; +import { ContentWidgetPositionPreference, IContentWidget, IContentWidgetRenderedCoordinate } from '../../editorBrowser.js'; import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js'; import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; @@ -600,7 +600,7 @@ class PositionPair { ) { } } -class Coordinate { +class Coordinate implements IContentWidgetRenderedCoordinate { _coordinateBrand: void = undefined; constructor( diff --git a/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index 6de14aedb6a..78324773a68 100644 --- a/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -254,7 +254,7 @@ export class ViewLines extends ViewPart implements IViewLines { return true; } public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { - const shouldRender = this._visibleLines.onFlushed(e); + const shouldRender = this._visibleLines.onFlushed(e, this._viewLineOptions.useGpu); this._maxLineWidth = 0; return shouldRender; } diff --git a/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 678dec6ab99..f7485a2f2b7 100644 --- a/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { autorun, observableValue, runOnChange } from '../../../../base/common/observable.js'; +import { autorun, runOnChange } from '../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -14,7 +14,6 @@ import { Range } from '../../../common/core/range.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js'; -import { FullFileRenderStrategy } from '../../gpu/fullFileRenderStrategy.js'; import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js'; import { GPULifecycle } from '../../gpu/gpuDisposable.js'; import { quadVertices } from '../../gpu/gpuUtils.js'; @@ -25,6 +24,12 @@ import { ViewLineOptions } from '../viewLines/viewLineOptions.js'; import type * as viewEvents from '../../../common/viewEvents.js'; import { CursorColumns } from '../../../common/core/cursorColumns.js'; import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js'; +import { createContentSegmenter, type IContentSegmenter } from '../../gpu/contentSegmenter.js'; +import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderStrategy.js'; +import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; +import { MutableDisposable } from '../../../../base/common/lifecycle.js'; +import type { ViewLineRenderingData } from '../../../common/viewModel.js'; +import { GlyphRasterizer } from '../../gpu/raster/glyphRasterizer.js'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -44,6 +49,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _initViewportData?: ViewportData[]; private _lastViewportData?: ViewportData; private _lastViewLineOptions?: ViewLineOptions; + private _maxLocalContentWidthSoFar = 0; private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; @@ -59,9 +65,9 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _initialized = false; - private _renderStrategy!: IGpuRenderStrategy; - - private _contentLeftObs = observableValue('contentLeft', 0); + private readonly _glyphRasterizer: MutableDisposable = this._register(new MutableDisposable()); + private readonly _renderStrategy: MutableDisposable = this._register(new MutableDisposable()); + private _rebuildBindGroup?: () => void; constructor( context: ViewContext, @@ -93,7 +99,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { async initWebgpu() { // #region General - this._device = await this._viewGpuContext.device; + this._device = ViewGpuContext.deviceSync || await ViewGpuContext.device; if (this._store.isDisposed) { return; @@ -106,7 +112,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { this._atlasGpuTextureVersions.length = 0; this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; - this._renderStrategy.reset(); + this._renderStrategy.value!.reset(); })); const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); @@ -160,7 +166,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { this._register(runOnChange(this._viewGpuContext.canvasDevicePixelDimensions, ({ width, height }) => { this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(width, height)); })); - this._register(runOnChange(this._contentLeftObs, () => { + this._register(runOnChange(this._viewGpuContext.contentLeft, () => { this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues()); })); } @@ -189,7 +195,16 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Storage buffers - this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device)); + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + this._glyphRasterizer.value = this._register(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get())); + this._register(runOnChange(this._viewGpuContext.devicePixelRatio, () => { + this._refreshGlyphRasterizer(); + })); + + + this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer }); + // this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device); this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', @@ -226,7 +241,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const module = this._device.createShaderModule({ label: 'Monaco shader module', - code: this._renderStrategy.wgsl, + code: this._renderStrategy.value!.wgsl, }); // #endregion Shader module @@ -271,25 +286,28 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Bind group - this._bindGroup = this._device.createBindGroup({ - label: 'Monaco bind group', - layout: this._pipeline.getBindGroupLayout(0), - entries: [ - // TODO: Pass in generically as array? - { binding: BindingId.GlyphInfo, resource: { buffer: this._glyphStorageBuffer } }, - { - binding: BindingId.TextureSampler, resource: this._device.createSampler({ - label: 'Monaco atlas sampler', - magFilter: 'nearest', - minFilter: 'nearest', - }) - }, - { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, - { binding: BindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } }, - { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, - ...this._renderStrategy.bindGroupEntries - ], - }); + this._rebuildBindGroup = () => { + this._bindGroup = this._device.createBindGroup({ + label: 'Monaco bind group', + layout: this._pipeline.getBindGroupLayout(0), + entries: [ + // TODO: Pass in generically as array? + { binding: BindingId.GlyphInfo, resource: { buffer: this._glyphStorageBuffer } }, + { + binding: BindingId.TextureSampler, resource: this._device.createSampler({ + label: 'Monaco atlas sampler', + magFilter: 'nearest', + minFilter: 'nearest', + }) + }, + { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, + { binding: BindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } }, + { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, + ...this._renderStrategy.value!.bindGroupEntries + ], + }); + }; + this._rebuildBindGroup(); // endregion Bind group @@ -306,6 +324,30 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { } } + private _refreshRenderStrategy(viewportData: ViewportData) { + if (this._renderStrategy.value?.type === 'viewport') { + return; + } + if (viewportData.endLineNumber < FullFileRenderStrategy.maxSupportedLines && this._viewportMaxColumn(viewportData) < FullFileRenderStrategy.maxSupportedColumns) { + return; + } + this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines or ${FullFileRenderStrategy.maxSupportedColumns} columns, switching to viewport render strategy`); + const viewportRenderStrategy = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer }); + this._renderStrategy.value = viewportRenderStrategy; + this._register(viewportRenderStrategy.onDidChangeBindGroupEntries(() => this._rebuildBindGroup?.())); + this._rebuildBindGroup?.(); + } + + private _viewportMaxColumn(viewportData: ViewportData): number { + let maxColumn = 0; + let lineData: ViewLineRenderingData; + for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) { + lineData = viewportData.getViewLineRenderingData(i); + maxColumn = Math.max(maxColumn, lineData.maxColumn); + } + return maxColumn; + } + private _updateAtlasStorageBufferAndTexture() { for (const [layerIndex, page] of ViewGpuContext.atlas.pages.entries()) { if (layerIndex >= TextureAtlas.maximumPageCount) { @@ -381,10 +423,13 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // from that side. Luckily rendering is cheap, it's only when uploaded data changes does it // start to cost. + override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this._refreshGlyphRasterizer(); + return true; + } override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } - override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; } override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { return true; } override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; } @@ -394,15 +439,28 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { return true; } override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; } - override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - this._contentLeftObs.set(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined); - return true; - } - // #endregion + private _refreshGlyphRasterizer() { + const glyphRasterizer = this._glyphRasterizer.value; + if (!glyphRasterizer) { + return; + } + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get(); + if ( + glyphRasterizer.fontFamily !== fontFamily || + glyphRasterizer.fontSize !== fontSize || + glyphRasterizer.devicePixelRatio !== devicePixelRatio + ) { + this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio); + } + } + public renderText(viewportData: ViewportData): void { if (this._initialized) { + this._refreshRenderStrategy(viewportData); return this._renderText(viewportData); } else { this._initViewportData = this._initViewportData ?? []; @@ -415,7 +473,14 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); - const visibleObjectCount = this._renderStrategy.update(viewportData, options); + const { localContentWidth } = this._renderStrategy.value!.update(viewportData, options); + + // Track the largest local content width so far in this session and use it as the scroll + // width. This is how the DOM renderer works as well, so you may not be able to scroll to + // the right in a file with long lines until you scroll down. + this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth / this._viewGpuContext.devicePixelRatio.get()); + this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLocalContentWidthSoFar); + this._viewGpuContext.scrollWidthElement.setWidth(this._context.viewLayout.getScrollWidth()); this._updateAtlasStorageBufferAndTexture(); @@ -427,16 +492,12 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { pass.setVertexBuffer(0, this._vertexBuffer); // Only draw the content area - const contentLeft = Math.ceil(this._contentLeftObs.get() * this._viewGpuContext.devicePixelRatio.get()); + const contentLeft = Math.ceil(this._viewGpuContext.contentLeft.get() * this._viewGpuContext.devicePixelRatio.get()); pass.setScissorRect(contentLeft, 0, this.canvas.width - contentLeft, this.canvas.height); pass.setBindGroup(0, this._bindGroup); - if (this._renderStrategy?.draw) { - this._renderStrategy.draw(pass, viewportData); - } else { - pass.draw(quadVertices.length / 2, visibleObjectCount); - } + this._renderStrategy.value!.draw(pass, viewportData); pass.end(); @@ -526,17 +587,45 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // Resolve tab widths for this line const lineData = viewportData.getViewLineRenderingData(lineNumber); const content = lineData.content; + + let contentSegmenter: IContentSegmenter | undefined; + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); + } + + let chars: string | undefined = ''; + let resolvedStartColumn = 0; + let resolvedStartCssPixelOffset = 0; for (let x = 0; x < startColumn - 1; x++) { - if (content[x] === '\t') { + if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) { + chars = content.charAt(x); + } else { + chars = contentSegmenter!.getSegmentAtIndex(x); + if (chars === undefined) { + continue; + } + resolvedStartCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; + } + if (chars === '\t') { resolvedStartColumn = CursorColumns.nextRenderTabStop(resolvedStartColumn, lineData.tabSize); } else { resolvedStartColumn++; } } let resolvedEndColumn = resolvedStartColumn; + let resolvedEndCssPixelOffset = 0; for (let x = startColumn - 1; x < endColumn - 1; x++) { - if (content[x] === '\t') { + if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) { + chars = content.charAt(x); + } else { + chars = contentSegmenter!.getSegmentAtIndex(x); + if (chars === undefined) { + continue; + } + resolvedEndCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; + } + if (chars === '\t') { resolvedEndColumn = CursorColumns.nextRenderTabStop(resolvedEndColumn, lineData.tabSize); } else { resolvedEndColumn++; @@ -545,8 +634,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // Visible horizontal range in _scaled_ pixels const result = new VisibleRanges(false, [new FloatHorizontalRange( - resolvedStartColumn * viewLineOptions.spaceWidth, - (resolvedEndColumn - resolvedStartColumn) * viewLineOptions.spaceWidth) + resolvedStartColumn * viewLineOptions.spaceWidth + resolvedStartCssPixelOffset, + (resolvedEndColumn - resolvedStartColumn) * viewLineOptions.spaceWidth + resolvedEndCssPixelOffset) ]); return result; @@ -587,24 +676,46 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); const content = lineData.content; - let visualColumnTarget = Math.round(mouseContentHorizontalOffset / this._lastViewLineOptions.spaceWidth); - let contentColumn = 0; - let contentColumnWithTabStops = 0; - while (visualColumnTarget > 0) { - let columnWithTabStopsSize = 0; - if (content[contentColumn] === '\t') { - const tabStop = CursorColumns.nextRenderTabStop(contentColumnWithTabStops, lineData.tabSize); - columnWithTabStopsSize = tabStop - contentColumnWithTabStops; + const dpr = getActiveWindow().devicePixelRatio; + const mouseContentHorizontalOffsetDevicePixels = mouseContentHorizontalOffset * dpr; + const spaceWidthDevicePixels = this._lastViewLineOptions.spaceWidth * dpr; + const contentSegmenter = createContentSegmenter(lineData, this._lastViewLineOptions); + + let widthSoFar = 0; + let charWidth = 0; + let tabXOffset = 0; + let column = 0; + for (let x = 0; x < content.length; x++) { + const chars = contentSegmenter.getSegmentAtIndex(x); + + // Part of an earlier segment + if (chars === undefined) { + column++; + continue; + } + + // Get the width of the character + if (chars === '\t') { + // Find the pixel offset between the current position and the next tab stop + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + charWidth = spaceWidthDevicePixels * (tabXOffset - offsetBefore); + // Convert back to offset excluding x and the current character + tabXOffset -= x + 1; + } else if (lineData.isBasicASCII && this._lastViewLineOptions.useMonospaceOptimizations) { + charWidth = spaceWidthDevicePixels; } else { - columnWithTabStopsSize = 1; + charWidth = this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width; } - if (visualColumnTarget - columnWithTabStopsSize / 2 < 0) { + + if (mouseContentHorizontalOffsetDevicePixels < widthSoFar + charWidth / 2) { break; } - visualColumnTarget -= columnWithTabStopsSize; - contentColumn++; - contentColumnWithTabStops += columnWithTabStopsSize; + + widthSoFar += charWidth; + column++; } - return new Position(lineNumber, Math.floor(contentColumn) + 1); + + return new Position(lineNumber, column + 1); } } diff --git a/code/src/vs/editor/browser/widget/diffEditor/commands.ts b/code/src/vs/editor/browser/widget/diffEditor/commands.ts index 9e04b2aad05..0d0676fea62 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/commands.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/commands.ts @@ -20,6 +20,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import './registrations.contribution.js'; import { DiffEditorSelectionHunkToolbarContext } from './features/gutterFeature.js'; import { URI } from '../../../../base/common/uri.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; export class ToggleCollapseUnchangedRegions extends Action2 { constructor() { @@ -255,7 +256,7 @@ export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | if (activeElement) { for (const d of diffEditors) { const container = d.getContainerDomNode(); - if (isElementOrParentOf(container, activeElement)) { + if (container.contains(activeElement)) { return d; } } @@ -264,13 +265,24 @@ export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | return null; } -function isElementOrParentOf(elementOrParent: Element, element: Element): boolean { - let e: Element | null = element; - while (e) { - if (e === elementOrParent) { - return true; + +/** + * If `editor` is the original or modified editor of a diff editor, it returns it. + * It returns null otherwise. + */ +export function findDiffEditorContainingCodeEditor(accessor: ServicesAccessor, editor: ICodeEditor): IDiffEditor | null { + if (!editor.getOption(EditorOption.inDiffEditor)) { + return null; + } + + const codeEditorService = accessor.get(ICodeEditorService); + + for (const diffEditor of codeEditorService.listDiffEditors()) { + const originalEditor = diffEditor.getOriginalEditor(); + const modifiedEditor = diffEditor.getModifiedEditor(); + if (originalEditor === editor || modifiedEditor === editor) { + return diffEditor; } - e = e.parentElement; } - return false; + return null; } diff --git a/code/src/vs/editor/common/codecs/baseToken.ts b/code/src/vs/editor/common/codecs/baseToken.ts index 9ebe3ad8abc..6430ffb61a5 100644 --- a/code/src/vs/editor/common/codecs/baseToken.ts +++ b/code/src/vs/editor/common/codecs/baseToken.ts @@ -18,6 +18,11 @@ export abstract class BaseToken { return this._range; } + /** + * Return text representation of the token. + */ + public abstract get text(): string; + /** * Check if this token has the same range as another one. */ diff --git a/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts b/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts index 5120f4ac322..a509940bc4e 100644 --- a/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts +++ b/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts @@ -31,6 +31,13 @@ export class CarriageReturn extends BaseToken { return CarriageReturn.byte; } + /** + * Return text representation of the token. + */ + public get text(): string { + return CarriageReturn.symbol; + } + /** * Create new `CarriageReturn` token with range inside * the given `Line` at the given `column number`. diff --git a/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts b/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts index 19b80dd88a3..fb826b759ca 100644 --- a/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts +++ b/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts @@ -24,6 +24,13 @@ export class NewLine extends BaseToken { */ public static readonly byte = VSBuffer.fromString(NewLine.symbol); + /** + * Return text representation of the token. + */ + public get text(): string { + return NewLine.symbol; + } + /** * The byte representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts b/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts new file mode 100644 index 00000000000..3703fa1df29 --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownLink } from './tokens/markdownLink.js'; +import { NewLine } from '../linesCodec/tokens/newLine.js'; +import { assert } from '../../../../base/common/assert.js'; +import { FormFeed } from '../simpleCodec/tokens/formFeed.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { VerticalTab } from '../simpleCodec/tokens/verticalTab.js'; +import { ReadableStream } from '../../../../base/common/stream.js'; +import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; +import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { LeftBracket, RightBracket } from '../simpleCodec/tokens/brackets.js'; +import { SimpleDecoder, TSimpleToken } from '../simpleCodec/simpleDecoder.js'; +import { ParserBase, TAcceptTokenResult } from '../simpleCodec/parserBase.js'; +import { LeftParenthesis, RightParenthesis } from '../simpleCodec/tokens/parentheses.js'; + +/** + * Tokens handled by this decoder. + */ +export type TMarkdownToken = MarkdownLink | TSimpleToken; + +/** + * List of characters that stop a markdown link sequence. + */ +const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLine, VerticalTab, FormFeed] + .map((token) => { return token.symbol; }); + +/** + * The parser responsible for parsing a `markdown link caption` part of a markdown + * link (e.g., the `[caption text]` part of the `[caption text](./some/path)` link). + * + * The parsing process starts with single `[` token and collects all tokens until + * the first `]` token is encountered. In this successful case, the parser transitions + * into the {@linkcode MarkdownLinkCaption} parser type which continues the general + * parsing process of the markdown link. + * + * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * is encountered before the `]` token, the parsing process is aborted which is communicated to + * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible + * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no + * longer represent a coherent token entity of a larger size. + */ +class PartialMarkdownLinkCaption extends ParserBase { + constructor(token: LeftBracket) { + super([token]); + } + + public accept(token: TSimpleToken): TAcceptTokenResult { + // any of stop characters is are breaking a markdown link caption sequence + if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // the `]` character ends the caption of a markdown link + if (token instanceof RightBracket) { + return { + result: 'success', + nextParser: new MarkdownLinkCaption([...this.tokens, token]), + wasTokenConsumed: true, + }; + } + + // otherwise, include the token in the sequence + // and keep the current parser object instance + this.currentTokens.push(token); + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } +} + +/** + * The parser responsible for transitioning from a {@linkcode PartialMarkdownLinkCaption} + * parser to the {@link PartialMarkdownLink} one, therefore serves a parser glue between + * the `[caption]` and the `(./some/path)` parts of the `[caption](./some/path)` link. + * + * The only successful case of this parser is the `(` token that initiated the process + * of parsing the `reference` part of a markdown link and in this case the parser + * transitions into the `PartialMarkdownLink` parser type. + * + * Any other character is considered a failure result. In this case, the caller is assumed + * to be responsible for re-emitting the {@link tokens} accumulated so far as standalone + * entities since they are no longer represent a coherent token entity of a larger size. + */ +class MarkdownLinkCaption extends ParserBase { + public accept(token: TSimpleToken): TAcceptTokenResult { + // the `(` character starts the link part of a markdown link + // that is the only character that can follow the caption + if (token instanceof LeftParenthesis) { + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new PartialMarkdownLink([...this.tokens], token), + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * The parser responsible for parsing a `link reference` part of a markdown link + * (e.g., the `(./some/path)` part of the `[caption text](./some/path)` link). + * + * The parsing process starts with tokens that represent the `[caption]` part of a markdown + * link, followed by the `(` token. The parser collects all subsequent tokens until final closing + * parenthesis (`)`) is encountered (*\*see [1] below*). In this successful case, the parser object + * transitions into the {@linkcode MarkdownLink} token type which signifies the end of the entire + * parsing process of the link text. + * + * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * is encountered before the final `)` token, the parsing process is aborted which is communicated to + * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible + * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no + * longer represent a coherent token entity of a larger size. + * + * `[1]` The `reference` part of the markdown link can contain any number of nested parenthesis, e.g., + * `[caption](/some/p(th/file.md)` is a valid markdown link and a valid folder name, hence number + * of open parenthesis must match the number of closing ones and the path sequence is considered + * to be complete as soon as this requirement is met. Therefore the `final` word is used in + * the description comments above to highlight this important detail. + */ +class PartialMarkdownLink extends ParserBase { + /** + * Number of open parenthesis in the sequence. + * See comment in the {@linkcode accept} method for more details. + */ + private openParensCount: number = 1; + + constructor( + protected readonly captionTokens: TSimpleToken[], + token: LeftParenthesis, + ) { + super([token]); + } + + public override get tokens(): readonly TSimpleToken[] { + return [...this.captionTokens, ...this.currentTokens]; + } + + public accept(token: TSimpleToken): TAcceptTokenResult { + // markdown links allow for nested parenthesis inside the link reference part, but + // the number of open parenthesis must match the number of closing parenthesis, e.g.: + // - `[caption](/some/p()th/file.md)` is a valid markdown link + // - `[caption](/some/p(th/file.md)` is an invalid markdown link + // hence we use the `openParensCount` variable to keep track of the number of open + // parenthesis encountered so far; then upon encountering a closing parenthesis we + // decrement the `openParensCount` and if it reaches 0 - we consider the link reference + // to be complete + + if (token instanceof LeftParenthesis) { + this.openParensCount += 1; + } + + if (token instanceof RightParenthesis) { + this.openParensCount -= 1; + + // sanity check! this must alway hold true because we return a complete markdown + // link as soon as we encounter matching number of closing parenthesis, hence + // we must never have `openParensCount` that is less than 0 + assert( + this.openParensCount >= 0, + `Unexpected right parenthesis token encountered: '${token}'.`, + ); + + // the markdown link is complete as soon as we get the same number of closing parenthesis + if (this.openParensCount === 0) { + const { startLineNumber, startColumn } = this.captionTokens[0].range; + + // create link caption string + const caption = this.captionTokens + .map((token) => { return token.text; }) + .join(''); + + // create link reference string + this.currentTokens.push(token); + const reference = this.currentTokens + .map((token) => { return token.text; }).join(''); + + // return complete markdown link object + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new MarkdownLink( + startLineNumber, + startColumn, + caption, + reference, + ), + }; + } + } + + // any of stop characters is are breaking a markdown link reference sequence + if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // the rest of the tokens can be included in the sequence + this.currentTokens.push(token); + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } +} + +/** + * Decoder capable of parsing markdown entities (e.g., links) from a sequence of simplier tokens. + */ +export class MarkdownDecoder extends BaseDecoder { + /** + * Current parser object that is responsible for parsing a sequence of tokens + * into some markdown entity. + */ + private current?: PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink; + + constructor( + stream: ReadableStream, + ) { + super(new SimpleDecoder(stream)); + } + + protected override onStreamData(token: TSimpleToken): void { + // markdown links start with `[` character, so here we can + // initiate the process of parsing a markdown link + if (token instanceof LeftBracket && !this.current) { + this.current = new PartialMarkdownLinkCaption(token); + + return; + } + + // if current parser was not initiated before, - we are not inside a + // sequence of tokens we care about, therefore re-emit the token + // immediately and continue to the next one + if (!this.current) { + this._onData.fire(token); + return; + } + + // if there is a current parser object, submit the token to it + // so it can progress with parsing the tokens sequence + const parseResult = this.current.accept(token); + if (parseResult.result === 'success') { + // if got a parsed out `MarkdownLink` back, emit it + // then reset the current parser object + if (parseResult.nextParser instanceof MarkdownLink) { + this._onData.fire(parseResult.nextParser); + delete this.current; + } else { + // otherwise, update the current parser object + this.current = parseResult.nextParser; + } + } else { + // if failed to parse a sequence of a tokens as a single markdown + // entity (e.g., a link), re-emit the tokens accumulated so far + // then reset the current parser object + for (const token of this.current.tokens) { + this._onData.fire(token); + delete this.current; + } + } + + // if token was not consumed by the parser, call `onStreamData` again + // so the token is properly handled by the decoder in the case when a + // new sequence starts with this token + if (!parseResult.wasTokenConsumed) { + this.onStreamData(token); + } + } + + protected override onStreamEnd(): void { + // if the stream has ended and there is a current incomplete parser + // object present, then re-emit its tokens as standalone entities + if (this.current) { + const { tokens } = this.current; + delete this.current; + + for (const token of [...tokens]) { + this._onData.fire(token); + } + } + + super.onStreamEnd(); + } +} diff --git a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts new file mode 100644 index 00000000000..b4c8947a213 --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { MarkdownToken } from './markdownToken.js'; +import { IRange, Range } from '../../../core/range.js'; +import { assert } from '../../../../../base/common/assert.js'; + +/** + * A token that represent a `markdown link` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class MarkdownLink extends MarkdownToken { + /** + * Check if this `markdown link` points to a valid URL address. + */ + public readonly isURL: boolean; + + constructor( + /** + * The starting line number of the link (1-based indexing). + */ + lineNumber: number, + /** + * The starting column number of the link (1-based indexing). + */ + columnNumber: number, + /** + * The caption of the link, including the square brackets. + */ + private readonly caption: string, + /** + * The reference of the link, including the parentheses. + */ + private readonly reference: string, + ) { + assert( + !isNaN(lineNumber), + `The line number must not be a NaN.`, + ); + + assert( + lineNumber > 0, + `The line number must be >= 1, got "${lineNumber}".`, + ); + + assert( + columnNumber > 0, + `The column number must be >= 1, got "${columnNumber}".`, + ); + + assert( + caption[0] === '[' && caption[caption.length - 1] === ']', + `The caption must be enclosed in square brackets, got "${caption}".`, + ); + + assert( + reference[0] === '(' && reference[reference.length - 1] === ')', + `The reference must be enclosed in parentheses, got "${reference}".`, + ); + + super( + new Range( + lineNumber, + columnNumber, + lineNumber, + columnNumber + caption.length + reference.length, + ), + ); + + // set up the `isURL` flag based on the current + try { + new URL(this.path); + this.isURL = true; + } catch { + this.isURL = false; + } + } + + public override get text(): string { + return `${this.caption}${this.reference}`; + } + + /** + * Returns the `reference` part of the link without enclosing parentheses. + */ + public get path(): string { + return this.reference.slice(1, this.reference.length - 1); + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if (!(other instanceof MarkdownLink)) { + return false; + } + + return this.text === other.text; + } + + /** + * Get the range of the `link part` of the token. + */ + public get linkRange(): IRange | undefined { + if (this.path.length === 0) { + return undefined; + } + + const { range } = this; + + // note! '+1' for openning `(` of the link + const startColumn = range.startColumn + this.caption.length + 1; + const endColumn = startColumn + this.path.length; + + return new Range( + range.startLineNumber, + startColumn, + range.endLineNumber, + endColumn, + ); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `md-link("${this.text}")${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts new file mode 100644 index 00000000000..fc1935d081b --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; + +/** + * Common base token that all `markdown` tokens should + * inherit from. + */ +export abstract class MarkdownToken extends BaseToken { } diff --git a/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts b/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts new file mode 100644 index 00000000000..9e864177f9f --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../baseToken.js'; + +/** + * Common interface for a result of accepting a next token + * in a sequence. + */ +export interface IAcceptTokenResult { + /** + * The result type of accepting a next token in a sequence. + */ + result: 'success' | 'failure'; + + /** + * Whether the token to accept was consumed by the parser + * during the accept operation. + */ + wasTokenConsumed: boolean; +} + +/** + * Successful result of accepting a next token in a sequence. + */ +export interface IAcceptTokenSuccess extends IAcceptTokenResult { + result: 'success'; + nextParser: T; +} + +/** + * Failure result of accepting a next token in a sequence. + */ +export interface IAcceptTokenFailure extends IAcceptTokenResult { + result: 'failure'; +} + +/** + * The result of operation of accepting a next token in a sequence. + */ +export type TAcceptTokenResult = IAcceptTokenSuccess | IAcceptTokenFailure; + +/** + * An abstract parser class that is able to parse a sequence of + * tokens into a new single entity. + */ +export abstract class ParserBase { + constructor( + /** + * Set of tokens that were accumulated so far. + */ + protected readonly currentTokens: TToken[] = [], + ) { } + + /** + * Get the tokens that were accumulated so far. + */ + public get tokens(): readonly TToken[] { + return this.currentTokens; + } + + /** + * Accept a new token returning parsing result: + * - successful result must include the next parser object or a fully parsed out token + * - failure result must indicate that the token was not consumed + * + * @param token The token to accept. + * @returns The parsing result. + */ + public abstract accept(token: TToken): TAcceptTokenResult; +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts b/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts index 64173eceabd..88ad1298501 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Hash } from './tokens/hash.js'; +import { Colon } from './tokens/colon.js'; import { FormFeed } from './tokens/formFeed.js'; import { Tab } from '../simpleCodec/tokens/tab.js'; import { Word } from '../simpleCodec/tokens/word.js'; @@ -10,22 +12,39 @@ import { VerticalTab } from './tokens/verticalTab.js'; import { Space } from '../simpleCodec/tokens/space.js'; import { NewLine } from '../linesCodec/tokens/newLine.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; +import { LeftBracket, RightBracket } from './tokens/brackets.js'; import { ReadableStream } from '../../../../base/common/stream.js'; import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; import { LinesDecoder, TLineToken } from '../linesCodec/linesDecoder.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { LeftParenthesis, RightParenthesis } from './tokens/parentheses.js'; /** * A token type that this decoder can handle. */ -export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed | CarriageReturn; +export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed | CarriageReturn | LeftBracket + | RightBracket | LeftParenthesis | RightParenthesis | Colon | Hash; + +/** + * List of well-known distinct tokens that this decoder emits (excluding + * the word stop characters defined below). Everything else is considered + * an arbitrary "text" sequence and is emitted as a single `Word` token. + */ +const WELL_KNOWN_TOKENS = [ + Space, Tab, VerticalTab, FormFeed, LeftBracket, RightBracket, + LeftParenthesis, RightParenthesis, Colon, Hash, +]; /** * Characters that stop a "word" sequence. * Note! the `\r` and `\n` are excluded from the list because this decoder based on `LinesDecoder` which * already handles the `carriagereturn`/`newline` cases and emits lines that don't contain them. */ -const STOP_CHARACTERS = [Space.symbol, Tab.symbol, VerticalTab.symbol, FormFeed.symbol]; +const WORD_STOP_CHARACTERS = [ + Space.symbol, Tab.symbol, VerticalTab.symbol, FormFeed.symbol, + LeftBracket.symbol, RightBracket.symbol, LeftParenthesis.symbol, + RightParenthesis.symbol, Colon.symbol, Hash.symbol, +]; /** * A decoder that can decode a stream of `Line`s into a stream @@ -39,7 +58,7 @@ export class SimpleDecoder extends BaseDecoder { } protected override onStreamData(token: TLineToken): void { - // re-emit new line tokens + // re-emit new line tokens immediately if (token instanceof CarriageReturn || token instanceof NewLine) { this._onData.fire(token); @@ -52,46 +71,30 @@ export class SimpleDecoder extends BaseDecoder { // index is 0-based, but column numbers are 1-based const columnNumber = i + 1; - // if a space character, emit a `Space` token and continue - if (token.text[i] === Space.symbol) { - this._onData.fire(Space.newOnLine(token, columnNumber)); - - i++; - continue; - } - - // if a tab character, emit a `Tab` token and continue - if (token.text[i] === Tab.symbol) { - this._onData.fire(Tab.newOnLine(token, columnNumber)); - - i++; - continue; - } - - // if a vertical tab character, emit a `VerticalTab` token and continue - if (token.text[i] === VerticalTab.symbol) { - this._onData.fire(VerticalTab.newOnLine(token, columnNumber)); - - i++; - continue; - } + // check if the current character is a well-known token + const tokenConstructor = WELL_KNOWN_TOKENS + .find((wellKnownToken) => { + return wellKnownToken.symbol === token.text[i]; + }); - // if a form feed character, emit a `FormFeed` token and continue - if (token.text[i] === FormFeed.symbol) { - this._onData.fire(FormFeed.newOnLine(token, columnNumber)); + // if it is a well-known token, emit it and continue to the next one + if (tokenConstructor) { + this._onData.fire(tokenConstructor.newOnLine(token, columnNumber)); i++; continue; } - // if a non-space character, parse out the whole word and - // emit it, then continue from the last word character position + // otherwise, it is an arbitrary "text" sequence of characters, + // that needs to be collected into a single `Word` token, hence + // read all the characters until a stop character is encountered let word = ''; - while (i < token.text.length && !(STOP_CHARACTERS.includes(token.text[i]))) { + while (i < token.text.length && !(WORD_STOP_CHARACTERS.includes(token.text[i]))) { word += token.text[i]; i++; } + // emit a "text" sequence of characters as a single `Word` token this._onData.fire( Word.newOnLine(word, token, columnNumber), ); diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts new file mode 100644 index 00000000000..5c6c1e46a5d --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `[` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class LeftBracket extends BaseToken { + /** + * The underlying symbol of the `LeftBracket` token. + */ + public static readonly symbol: string = '['; + + /** + * Return text representation of the token. + */ + public get text(): string { + return LeftBracket.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): LeftBracket { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new LeftBracket(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `left-bracket${this.range}`; + } +} + +/** + * A token that represent a `]` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class RightBracket extends BaseToken { + /** + * The underlying symbol of the `RightBracket` token. + */ + public static readonly symbol: string = ']'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return RightBracket.symbol; + } + + /** + * Create new `RightBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): RightBracket { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new RightBracket(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `right-bracket${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts new file mode 100644 index 00000000000..2c4b89d9ce5 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `:` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Colon extends BaseToken { + /** + * The underlying symbol of the `LeftBracket` token. + */ + public static readonly symbol: string = ':'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return Colon.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): Colon { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new Colon(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `colon${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts index ab40192f459..35f55dd8a2a 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts @@ -18,6 +18,13 @@ export class FormFeed extends BaseToken { */ public static readonly symbol: string = '\f'; + /** + * Return text representation of the token. + */ + public get text(): string { + return FormFeed.symbol; + } + /** * Create new `FormFeed` token with range inside * the given `Line` at the given `column number`. diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts new file mode 100644 index 00000000000..372e0b2ee3d --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `#` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Hash extends BaseToken { + /** + * The underlying symbol of the `LeftBracket` token. + */ + public static readonly symbol: string = '#'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return Hash.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): Hash { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new Hash(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `hash${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts new file mode 100644 index 00000000000..b67f4e10f5c --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `(` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class LeftParenthesis extends BaseToken { + /** + * The underlying symbol of the `LeftParenthesis` token. + */ + public static readonly symbol: string = '('; + + /** + * Return text representation of the token. + */ + public get text(): string { + return LeftParenthesis.symbol; + } + + /** + * Create new `LeftParenthesis` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): LeftParenthesis { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new LeftParenthesis(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `left-parenthesis${this.range}`; + } +} + +/** + * A token that represent a `)` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class RightParenthesis extends BaseToken { + /** + * The underlying symbol of the `RightParenthesis` token. + */ + public static readonly symbol: string = ')'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return RightParenthesis.symbol; + } + + /** + * Create new `RightParenthesis` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): RightParenthesis { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new RightParenthesis(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `right-parenthesis${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts index 9961c38ece9..18a5dff4a0a 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts @@ -18,6 +18,13 @@ export class Space extends BaseToken { */ public static readonly symbol: string = ' '; + /** + * Return text representation of the token. + */ + public get text(): string { + return Space.symbol; + } + /** * Create new `Space` token with range inside * the given `Line` at the given `column number`. diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts index aab11327bc1..7f511c2626b 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts @@ -18,6 +18,13 @@ export class Tab extends BaseToken { */ public static readonly symbol: string = '\t'; + /** + * Return text representation of the token. + */ + public get text(): string { + return Tab.symbol; + } + /** * Create new `Tab` token with range inside * the given `Line` at the given `column number`. diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts index 11e5ca6efab..c6b87db0e37 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts @@ -18,6 +18,13 @@ export class VerticalTab extends BaseToken { */ public static readonly symbol: string = '\v'; + /** + * Return text representation of the token. + */ + public get text(): string { + return VerticalTab.symbol; + } + /** * Create new `VerticalTab` token with range inside * the given `Line` at the given `column number`. diff --git a/code/src/vs/editor/common/config/editorOptions.ts b/code/src/vs/editor/common/config/editorOptions.ts index 908dcac26bd..89d37bb27ee 100644 --- a/code/src/vs/editor/common/config/editorOptions.ts +++ b/code/src/vs/editor/common/config/editorOptions.ts @@ -16,6 +16,7 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; +import product from '../../../platform/product/common/product.js'; //#region typed options @@ -1670,6 +1671,11 @@ export interface IEditorFindOptions { * Controls how the find widget search history should be stored */ history?: 'never' | 'workspace'; + /** + * @internal + * Controls how the replace widget search history should be stored + */ + replaceHistory?: 'never' | 'workspace'; } /** @@ -1688,6 +1694,7 @@ class EditorFind extends BaseEditorOption(input.history, this.defaultValue.history, ['never', 'workspace']), + replaceHistory: stringSet<'never' | 'workspace'>(input.replaceHistory, this.defaultValue.replaceHistory, ['never', 'workspace']), }; } } @@ -4193,15 +4211,28 @@ export interface IInlineSuggestOptions { fontFamily?: string | 'default'; edits?: { - experimental?: { - enabled?: boolean; - useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; - useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; - useWordInsertionView?: 'never' | 'whenPossible'; - useWordReplacementView?: 'never' | 'whenPossible'; - - useGutterIndicator?: boolean; - }; + codeShifting?: boolean; + + /** + * @internal + */ + enabled?: boolean; + /** + * @internal + */ + useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; + /** + * @internal + */ + useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + /** + * @internal + */ + useMultiLineGhostText?: boolean; + /** + * @internal + */ + useGutterIndicator?: boolean; }; } @@ -4228,14 +4259,12 @@ class InlineEditorSuggest extends BaseEditorOption lineCount) { + const lineLength = this.getLineLength(lineCount); + return new Position(lineCount, lineLength + 1); + } + if (position.column < 1) { + return new Position(position.lineNumber, 1); + } + const lineLength = this.getLineLength(position.lineNumber); + if (position.column - 1 > lineLength) { + return new Position(position.lineNumber, lineLength + 1); + } + return position; } getOffsetRange(range: Range): OffsetRange { diff --git a/code/src/vs/editor/common/core/textLength.ts b/code/src/vs/editor/common/core/textLength.ts index 76f3ee378bd..a1f2d8e35c8 100644 --- a/code/src/vs/editor/common/core/textLength.ts +++ b/code/src/vs/editor/common/core/textLength.ts @@ -115,7 +115,7 @@ export class TextLength { } public toLineRange(): LineRange { - return LineRange.ofLength(1, this.lineCount); + return LineRange.ofLength(1, this.lineCount + 1); } public addToPosition(position: Position): Position { diff --git a/code/src/vs/editor/common/languages.ts b/code/src/vs/editor/common/languages.ts index 0a0545a891a..95aa2bfee68 100644 --- a/code/src/vs/editor/common/languages.ts +++ b/code/src/vs/editor/common/languages.ts @@ -28,6 +28,8 @@ import { ExtensionIdentifier } from '../../platform/extensions/common/extensions import { IMarkerData } from '../../platform/markers/common/markers.js'; import { IModelTokensChangedEvent } from './textModelEvents.js'; import type { Parser } from '@vscode/tree-sitter-wasm'; +import { ITextModel } from './model.js'; +import { TokenUpdate } from './model/tokenStore.js'; /** * @internal @@ -84,14 +86,29 @@ export class EncodedTokenizationResult { } } +export interface SyntaxNode { + startIndex: number; + endIndex: number; +} + +export interface QueryCapture { + name: string; + text?: string; + node: SyntaxNode; +} + /** * An intermediate interface for scaffolding the new tree sitter tokenization support. Not final. * @internal */ export interface ITreeSitterTokenizationSupport { + /** + * exposed for testing + */ + getTokensInRange(textModel: ITextModel, range: Range, rangeStartOffset: number, rangeEndOffset: number): TokenUpdate[] | undefined; tokenizeEncoded(lineNumber: number, textModel: model.ITextModel): Uint32Array | undefined; - captureAtPosition(lineNumber: number, column: number, textModel: model.ITextModel): Parser.QueryCapture[]; - captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): Parser.QueryCapture[]; + captureAtPosition(lineNumber: number, column: number, textModel: model.ITextModel): QueryCapture[]; + captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): QueryCapture[]; onDidChangeTokens: Event<{ textModel: model.ITextModel; changes: IModelTokensChangedEvent }>; tokenizeEncodedInstrumented(lineNumber: number, textModel: model.ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined; } @@ -834,6 +851,8 @@ export interface InlineCompletionsProvider; } -export interface DocumentContextItem { - readonly uri: URI; - readonly version: number; - readonly ranges: IRange[]; -} - -export interface MappedEditsContext { - /** The outer array is sorted by priority - from highest to lowest. The inner arrays contain elements of the same priority. */ - readonly documents: DocumentContextItem[][]; - /** - * @internal - */ - readonly conversation?: (ConversationRequest | ConversationResponse)[]; -} - -/** - * @internal - */ -export interface ConversationRequest { - readonly type: 'request'; - readonly message: string; -} - -/** - * @internal - */ -export interface ConversationResponse { - readonly type: 'response'; - readonly message: string; - readonly references?: DocumentContextItem[]; -} - -export interface MappedEditsProvider { - /** - * @internal - */ - readonly displayName: string; // internal - - /** - * Provider maps code blocks from the chat into a workspace edit. - * - * @param document The document to provide mapped edits for. - * @param codeBlocks Code blocks that come from an LLM's reply. - * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. - * @param context The context for providing mapped edits. - * @param token A cancellation token. - * @returns A provider result of text edits. - */ - provideMappedEdits( - document: model.ITextModel, - codeBlocks: string[], - context: MappedEditsContext, - token: CancellationToken - ): Promise; -} - export interface IInlineEdit { text: string; range: IRange; @@ -2421,6 +2384,7 @@ export enum InlineEditTriggerKind { } export interface InlineEditProvider { + displayName?: string; provideInlineEdit(model: model.ITextModel, context: IInlineEditContext, token: CancellationToken): ProviderResult; freeInlineEdit(edit: T): void; } diff --git a/code/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/code/src/vs/editor/common/languages/languageConfigurationRegistry.ts index bf0516d531b..f42b06d0b74 100644 --- a/code/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/code/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js'; import * as strings from '../../../base/common/strings.js'; import { ITextModel } from '../model.js'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from '../core/wordHelper.js'; @@ -202,7 +202,7 @@ class ComposedLanguageConfiguration { ); this._entries.push(entry); this._resolved = null; - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { for (let i = 0; i < this._entries.length; i++) { if (this._entries[i] === entry) { this._entries.splice(i, 1); @@ -210,7 +210,7 @@ class ComposedLanguageConfiguration { break; } } - }); + })); } public getResolvedConfiguration(): ResolvedLanguageConfiguration | null { @@ -332,10 +332,10 @@ export class LanguageConfigurationRegistry extends Disposable { const disposable = entries.register(configuration, priority); this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { disposable.dispose(); this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); - }); + })); } public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null { diff --git a/code/src/vs/editor/common/model.ts b/code/src/vs/editor/common/model.ts index 6700e53da2c..146c9830e04 100644 --- a/code/src/vs/editor/common/model.ts +++ b/code/src/vs/editor/common/model.ts @@ -865,6 +865,11 @@ export interface ITextModel { */ validateRange(range: IRange): Range; + /** + * Verifies the range is valid. + */ + isValidRange(range: IRange): boolean; + /** * Converts the position to a zero-based offset. * diff --git a/code/src/vs/editor/common/model/textModel.ts b/code/src/vs/editor/common/model/textModel.ts index 0313e5c208a..e54c9ebd69d 100644 --- a/code/src/vs/editor/common/model/textModel.ts +++ b/code/src/vs/editor/common/model/textModel.ts @@ -454,7 +454,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._setValueFromTextBuffer(textBuffer, disposable); } - private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean, isEolChange: boolean): IModelContentChangedEvent { + private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, rangeEndPosition: Position, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean, isEolChange: boolean): IModelContentChangedEvent { return { changes: [{ range: range, @@ -500,7 +500,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati false, false ), - this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, this.getValue(), false, false, true, false) + this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, true, false) ); } @@ -531,7 +531,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati false, false ), - this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, this.getValue(), false, false, false, true) + this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, false, true) ); } @@ -1034,6 +1034,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._validatePosition(position.lineNumber, position.column, validationType); } + public isValidRange(range: Range): boolean { + return this._isValidRange(range, StringOffsetValidationType.SurrogatePairs); + } + private _isValidRange(range: Range, validationType: StringOffsetValidationType): boolean { const startLineNumber = range.startLineNumber; const startColumn = range.startColumn; diff --git a/code/src/vs/editor/common/model/tokenStore.ts b/code/src/vs/editor/common/model/tokenStore.ts new file mode 100644 index 00000000000..a37c355ffc7 --- /dev/null +++ b/code/src/vs/editor/common/model/tokenStore.ts @@ -0,0 +1,486 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../base/common/lifecycle.js'; +import { ITextModel } from '../model.js'; + +class ListNode implements IDisposable { + parent?: ListNode; + private readonly _children: Node[] = []; + get children(): ReadonlyArray { return this._children; } + + private _length: number = 0; + get length(): number { return this._length; } + + constructor(public readonly height: number) { } + + static create(node1: Node, node2: Node) { + const list = new ListNode(node1.height + 1); + list.appendChild(node1); + list.appendChild(node2); + return list; + } + + canAppendChild(): boolean { + return this._children.length < 3; + } + + appendChild(node: Node) { + if (!this.canAppendChild()) { + throw new Error('Cannot insert more than 3 children in a ListNode'); + } + this._children.push(node); + + this._length += node.length; + this._updateParentLength(node.length); + node.parent = this; + } + + private _updateParentLength(delta: number) { + let updateParent = this.parent; + while (updateParent) { + updateParent._length += delta; + updateParent = updateParent.parent; + } + } + + unappendChild(): Node { + const child = this._children.pop()!; + this._length -= child.length; + this._updateParentLength(-child.length); + return child; + } + + prependChild(node: Node) { + if (this._children.length >= 3) { + throw new Error('Cannot prepend more than 3 children in a ListNode'); + } + this._children.unshift(node); + + this._length += node.length; + this._updateParentLength(node.length); + node.parent = this; + } + + unprependChild(): Node { + const child = this._children.shift()!; + this._length -= child.length; + this._updateParentLength(-child.length); + return child; + } + + lastChild(): Node { + return this._children[this._children.length - 1]; + } + + dispose() { + this._children.splice(0, this._children.length); + } +} + +type Node = ListNode | LeafNode; + +interface LeafNode { + readonly length: number; + parent?: ListNode; + token: number; + needsRefresh?: boolean; + height: 0; +} + +export interface TokenUpdate { + readonly startOffsetInclusive: number; + readonly length: number; + readonly token: number; +} + +function isLeaf(node: Node): node is LeafNode { + return (node as LeafNode).token !== undefined; +} + +// Heavily inspired by https://github.com/microsoft/vscode/blob/4eb2658d592cb6114a7a393655574176cc790c5b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts#L108-L109 +function append(node: Node, nodeToAppend: Node): Node { + let curNode = node; + const parents: ListNode[] = []; + let nodeToAppendOfCorrectHeight: Node | undefined; + while (true) { + if (nodeToAppend.height === curNode.height) { + nodeToAppendOfCorrectHeight = nodeToAppend; + break; + } + + if (isLeaf(curNode)) { + throw new Error('unexpected'); + } + parents.push(curNode); + curNode = curNode.lastChild(); + } + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]; + if (nodeToAppendOfCorrectHeight) { + // Can we take the element? + if (parent.children.length >= 3) { + // we need to split to maintain (2,3)-tree property. + // Send the third element + the new element to the parent. + const newList = ListNode.create(parent.unappendChild()!, nodeToAppendOfCorrectHeight); + nodeToAppendOfCorrectHeight = newList; + } else { + parent.appendChild(nodeToAppendOfCorrectHeight); + nodeToAppendOfCorrectHeight = undefined; + } + } + } + if (nodeToAppendOfCorrectHeight) { + const newList = new ListNode(nodeToAppendOfCorrectHeight.height + 1); + newList.appendChild(node); + newList.appendChild(nodeToAppendOfCorrectHeight); + return newList; + } else { + return node; + } +} + +function prepend(list: Node, nodeToAppend: Node): Node { + let curNode = list; + const parents: ListNode[] = []; + while (nodeToAppend.height !== curNode.height) { + if (isLeaf(curNode)) { + throw new Error('unexpected'); + } + parents.push(curNode); + // assert 2 <= curNode.childrenFast.length <= 3 + curNode = curNode.children[0] as ListNode; + } + let nodeToPrependOfCorrectHeight: Node | undefined = nodeToAppend; + // assert nodeToAppendOfCorrectHeight!.listHeight === curNode.listHeight + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]; + if (nodeToPrependOfCorrectHeight) { + // Can we take the element? + if (parent.children.length >= 3) { + // we need to split to maintain (2,3)-tree property. + // Send the third element + the new element to the parent. + nodeToPrependOfCorrectHeight = ListNode.create(nodeToPrependOfCorrectHeight, parent.unprependChild()); + } else { + parent.prependChild(nodeToPrependOfCorrectHeight); + nodeToPrependOfCorrectHeight = undefined; + } + } + } + if (nodeToPrependOfCorrectHeight) { + return ListNode.create(nodeToPrependOfCorrectHeight, list); + } else { + return list; + } +} + +function concat(node1: Node, node2: Node): Node { + if (node1.height === node2.height) { + return ListNode.create(node1, node2); + } + else if (node1.height > node2.height) { + // node1 is the tree we want to insert into + return append(node1, node2); + } else { + return prepend(node2, node1); + } +} + +export class TokenStore implements IDisposable { + private _root: Node; + get root(): Node { + return this._root; + } + + constructor(private readonly _textModel: ITextModel) { + this._root = this.createEmptyRoot(); + } + + private createEmptyRoot(): Node { + return { + length: this._textModel.getValueLength(), + token: 0, + height: 0 + }; + } + + /** + * + * @param update all the tokens for the document in sequence + */ + buildStore(tokens: TokenUpdate[]) { + this._root = this.createFromUpdates(tokens, true); + } + + private createFromUpdates(tokens: TokenUpdate[], needsRefresh?: boolean): Node { + if (tokens.length === 0) { + return this.createEmptyRoot(); + } + let newRoot: Node = { + length: tokens[0].length, + token: tokens[0].token, + height: 0, + needsRefresh + }; + for (let j = 1; j < tokens.length; j++) { + newRoot = append(newRoot, { length: tokens[j].length, token: tokens[j].token, height: 0, needsRefresh }); + } + return newRoot; + } + + /** + * + * @param tokens tokens are in sequence in the document. + */ + update(length: number, tokens: TokenUpdate[], needsRefresh?: boolean) { + if (tokens.length === 0) { + return; + } + this.replace(length, tokens[0].startOffsetInclusive, tokens, needsRefresh); + } + + delete(length: number, startOffset: number) { + this.replace(length, startOffset, [], true); + } + + /** + * + * @param tokens tokens are in sequence in the document. + */ + private replace(length: number, updateOffsetStart: number, tokens: TokenUpdate[], needsRefresh?: boolean) { + const firstUnchangedOffsetAfterUpdate = updateOffsetStart + length; + // Find the last unchanged node preceding the update + const precedingNodes: Node[] = []; + // Find the first unchanged node after the update + const postcedingNodes: Node[] = []; + const stack: { node: Node; offset: number }[] = [{ node: this._root, offset: 0 }]; + + while (stack.length > 0) { + const node = stack.pop()!; + const currentOffset = node.offset; + + if (currentOffset < updateOffsetStart && currentOffset + node.node.length <= updateOffsetStart) { + node.node.parent = undefined; + precedingNodes.push(node.node); + continue; + } else if (isLeaf(node.node) && (currentOffset < updateOffsetStart)) { + // We have a partial preceding node + precedingNodes.push({ length: updateOffsetStart - currentOffset, token: node.node.token, height: 0, needsRefresh: needsRefresh || node.node.needsRefresh }); + // Node could also be postceeding, so don't continue + } + + if ((updateOffsetStart <= currentOffset) && (currentOffset + node.node.length <= firstUnchangedOffsetAfterUpdate)) { + continue; + } + + if (currentOffset >= firstUnchangedOffsetAfterUpdate) { + node.node.parent = undefined; + postcedingNodes.push(node.node); + continue; + } else if (isLeaf(node.node) && (currentOffset + node.node.length >= firstUnchangedOffsetAfterUpdate)) { + // we have a partial postceeding node + postcedingNodes.push({ length: currentOffset + node.node.length - firstUnchangedOffsetAfterUpdate, token: node.node.token, height: 0, needsRefresh: needsRefresh || node.node.needsRefresh }); + continue; + } + + if (!isLeaf(node.node)) { + // Push children in reverse order to process them left-to-right when popping + let childOffset = currentOffset + node.node.length; + for (let i = node.node.children.length - 1; i >= 0; i--) { + childOffset -= node.node.children[i].length; + stack.push({ node: node.node.children[i], offset: childOffset }); + } + } + } + + let allNodes: Node[]; + if (tokens.length > 0) { + allNodes = precedingNodes.concat(this.createFromUpdates(tokens, needsRefresh), postcedingNodes); + } else { + allNodes = precedingNodes.concat(postcedingNodes); + } + let newRoot: Node = allNodes[0]; + for (let i = 1; i < allNodes.length; i++) { + newRoot = concat(newRoot, allNodes[i]); + } + + this._root = newRoot ?? this.createEmptyRoot(); + } + + /** + * + * @param startOffsetInclusive + * @param endOffsetExclusive + * @param visitor Return true from visitor to exit early + * @returns + */ + private traverseInOrderInRange(startOffsetInclusive: number, endOffsetExclusive: number, visitor: (node: Node, offset: number) => boolean): void { + const stack: { node: Node; offset: number }[] = [{ node: this._root, offset: 0 }]; + + while (stack.length > 0) { + const { node, offset } = stack.pop()!; + const nodeEnd = offset + node.length; + + // Skip nodes that are completely before or after the range + if (nodeEnd <= startOffsetInclusive || offset >= endOffsetExclusive) { + continue; + } + + if (visitor(node, offset)) { + return; + } + + if (!isLeaf(node)) { + // Push children in reverse order to process them left-to-right when popping + let childOffset = offset + node.length; + for (let i = node.children.length - 1; i >= 0; i--) { + childOffset -= node.children[i].length; + stack.push({ node: node.children[i], offset: childOffset }); + } + } + } + } + + getTokenAt(offset: number): TokenUpdate | undefined { + let result: TokenUpdate | undefined; + this.traverseInOrderInRange(offset, this._root.length, (node, offset) => { + if (isLeaf(node)) { + result = { token: node.token, startOffsetInclusive: offset, length: node.length }; + return true; + } + return false; + }); + return result; + } + + getTokensInRange(startOffsetInclusive: number, endOffsetExclusive: number): TokenUpdate[] { + const result: { token: number; startOffsetInclusive: number; length: number }[] = []; + this.traverseInOrderInRange(startOffsetInclusive, endOffsetExclusive, (node, offset) => { + if (isLeaf(node)) { + let clippedLength = node.length; + let clippedOffset = offset; + if (offset < startOffsetInclusive) { + clippedLength -= (startOffsetInclusive - offset); + clippedOffset = startOffsetInclusive; + } else if (offset + node.length > endOffsetExclusive) { + clippedLength -= (offset + node.length - endOffsetExclusive); + } + result.push({ token: node.token, startOffsetInclusive: clippedOffset, length: clippedLength }); + } + return false; + }); + return result; + } + + markForRefresh(startOffsetInclusive: number, endOffsetExclusive: number): void { + this.traverseInOrderInRange(startOffsetInclusive, endOffsetExclusive, (node) => { + if (isLeaf(node)) { + node.needsRefresh = true; + } + return false; + }); + } + + rangeNeedsRefresh(startOffsetInclusive: number, endOffsetExclusive: number): boolean { + let needsRefresh = false; + this.traverseInOrderInRange(startOffsetInclusive, endOffsetExclusive, (node) => { + if (isLeaf(node) && node.needsRefresh) { + needsRefresh = true; + } + return false; + }); + return needsRefresh; + } + + getNeedsRefresh(): { startOffset: number; endOffset: number }[] { + const result: { startOffset: number; endOffset: number }[] = []; + + this.traverseInOrderInRange(0, this._textModel.getValueLength(), (node, offset) => { + if (isLeaf(node) && node.needsRefresh) { + if ((result.length > 0) && (result[result.length - 1].endOffset === offset)) { + result[result.length - 1].endOffset += node.length; + } else { + result.push({ startOffset: offset, endOffset: offset + node.length }); + } + } + return false; + }); + return result; + } + + public deepCopy(): TokenStore { + const newStore = new TokenStore(this._textModel); + newStore._root = this._copyNodeIterative(this._root); + return newStore; + } + + private _copyNodeIterative(root: Node): Node { + const newRoot = isLeaf(root) + ? { length: root.length, token: root.token, needsRefresh: root.needsRefresh, height: root.height } + : new ListNode(root.height); + + const stack: Array<[Node, Node]> = [[root, newRoot]]; + + while (stack.length > 0) { + const [oldNode, clonedNode] = stack.pop()!; + if (!isLeaf(oldNode)) { + for (const child of oldNode.children) { + const childCopy = isLeaf(child) + ? { length: child.length, token: child.token, needsRefresh: child.needsRefresh, height: child.height } + : new ListNode(child.height); + + (clonedNode as ListNode).appendChild(childCopy); + stack.push([child, childCopy]); + } + } + } + + return newRoot; + } + + /** + * Returns a string representation of the token tree using an iterative approach + */ + printTree(root: Node = this._root): string { + const result: string[] = []; + const stack: Array<[Node, number]> = [[root, 0]]; + + while (stack.length > 0) { + const [node, depth] = stack.pop()!; + const indent = ' '.repeat(depth); + + if (isLeaf(node)) { + result.push(`${indent}Leaf(length: ${node.length}, token: ${node.token}, refresh: ${node.needsRefresh})\n`); + } else { + result.push(`${indent}List(length: ${node.length})\n`); + // Push children in reverse order so they get processed left-to-right + for (let i = node.children.length - 1; i >= 0; i--) { + stack.push([node.children[i], depth + 1]); + } + } + } + + return result.join(''); + } + + dispose(): void { + const stack: Array<[Node, boolean]> = [[this._root, false]]; + while (stack.length > 0) { + const [node, visited] = stack.pop()!; + if (isLeaf(node)) { + node.parent = undefined; + } else if (!visited) { + stack.push([node, true]); + for (let i = node.children.length - 1; i >= 0; i--) { + stack.push([node.children[i], false]); + } + } else { + node.dispose(); + node.parent = undefined; + } + } + this._root = undefined!; + } +} diff --git a/code/src/vs/editor/common/model/treeSitterTokenStoreService.ts b/code/src/vs/editor/common/model/treeSitterTokenStoreService.ts new file mode 100644 index 00000000000..9a2de36707e --- /dev/null +++ b/code/src/vs/editor/common/model/treeSitterTokenStoreService.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../core/range.js'; +import { ITextModel } from '../model.js'; +import { TokenStore, TokenUpdate } from './tokenStore.js'; +import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; +import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; + +export interface ITreeSitterTokenizationStoreService { + readonly _serviceBrand: undefined; + setTokens(model: ITextModel, tokens: TokenUpdate[]): void; + getTokens(model: ITextModel, line: number): Uint32Array | undefined; + updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void; + markForRefresh(model: ITextModel, range: Range): void; + getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[]; + hasTokens(model: ITextModel, accurateForRange?: Range): boolean; +} + +export const ITreeSitterTokenizationStoreService = createDecorator('treeSitterTokenizationStoreService'); + +export interface TokenInformation { + tokens: Uint32Array; + needsRefresh?: boolean; +} + +class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStoreService, IDisposable { + readonly _serviceBrand: undefined; + + private readonly tokens = new Map(); + + constructor() { } + + setTokens(model: ITextModel, tokens: TokenUpdate[]): void { + const disposables = new DisposableStore(); + const store = disposables.add(new TokenStore(model)); + this.tokens.set(model, { store: store, accurateVersion: model.getVersionId(), disposables, guessVersion: model.getVersionId() }); + + store.buildStore(tokens); + disposables.add(model.onDidChangeContent(e => { + const storeInfo = this.tokens.get(model); + if (!storeInfo) { + return; + } + + storeInfo.guessVersion = e.versionId; + for (const change of e.changes) { + if (change.text.length > change.rangeLength) { + const oldToken = storeInfo.store.getTokenAt(change.rangeOffset); + let newToken: TokenUpdate; + if (oldToken) { + // Insert. Just grow the token at this position to include the insert. + newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; + } else { + // The document got larger and the change is at the end of the document. + newToken = { startOffsetInclusive: change.rangeOffset, length: change.text.length, token: 0 }; + } + storeInfo.store.update(oldToken?.length ?? 0, [newToken], true); + } else if (change.text.length < change.rangeLength) { + // Delete. Delete the tokens at the corresponding range. + const deletedCharCount = change.rangeLength - change.text.length; + storeInfo.store.delete(deletedCharCount, change.rangeOffset); + } + const refreshLength = change.rangeLength > change.text.length ? change.rangeLength : change.text.length; + storeInfo.store.markForRefresh(change.rangeOffset, change.rangeOffset + refreshLength); + } + })); + disposables.add(model.onWillDispose(() => { + const storeInfo = this.tokens.get(model); + if (storeInfo) { + storeInfo.disposables.dispose(); + this.tokens.delete(model); + } + })); + } + + hasTokens(model: ITextModel, accurateForRange?: Range): boolean { + const tokens = this.tokens.get(model); + if (!tokens) { + return false; + } + if (!accurateForRange || (tokens.guessVersion === tokens.accurateVersion)) { + return true; + } + + return !tokens.store.rangeNeedsRefresh(model.getOffsetAt(accurateForRange.getStartPosition()), model.getOffsetAt(accurateForRange.getEndPosition())); + } + + getTokens(model: ITextModel, line: number): Uint32Array | undefined { + const tokens = this.tokens.get(model)?.store; + if (!tokens) { + return undefined; + } + const lineStartOffset = model.getOffsetAt({ lineNumber: line, column: 1 }); + const lineTokens = tokens.getTokensInRange(lineStartOffset, model.getOffsetAt({ lineNumber: line, column: model.getLineMaxColumn(line) }) + 1); + const result = new Uint32Array(lineTokens.length * 2); + for (let i = 0; i < lineTokens.length; i++) { + result[i * 2] = lineTokens[i].startOffsetInclusive - lineStartOffset + lineTokens[i].length; + result[i * 2 + 1] = lineTokens[i].token; + } + return result; + } + + updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void { + const existingTokens = this.tokens.get(model); + if (!existingTokens) { + return; + } + + existingTokens.accurateVersion = version; + for (const update of updates) { + const lastToken = update.newTokens.length > 0 ? update.newTokens[update.newTokens.length - 1] : undefined; + const oldRangeLength = ((existingTokens.guessVersion >= version) && lastToken) ? (lastToken.startOffsetInclusive + lastToken.length - update.newTokens[0].startOffsetInclusive) : update.oldRangeLength; + existingTokens.store.update(oldRangeLength, update.newTokens); + } + } + + markForRefresh(model: ITextModel, range: Range): void { + const tree = this.tokens.get(model)?.store; + if (!tree) { + return; + } + + tree.markForRefresh(model.getOffsetAt(range.getStartPosition()), model.getOffsetAt(range.getEndPosition())); + } + + getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[] { + const needsRefreshOffsetRanges = this.tokens.get(model)?.store.getNeedsRefresh(); + if (!needsRefreshOffsetRanges) { + return []; + } + return needsRefreshOffsetRanges.map(range => ({ + range: Range.fromPositions(model.getPositionAt(range.startOffset), model.getPositionAt(range.endOffset)), + startOffset: range.startOffset, + endOffset: range.endOffset + })); + } + + dispose(): void { + for (const [, value] of this.tokens) { + value.disposables.dispose(); + } + } +} + +registerSingleton(ITreeSitterTokenizationStoreService, TreeSitterTokenizationStoreService, InstantiationType.Delayed); diff --git a/code/src/vs/editor/common/model/treeSitterTokens.ts b/code/src/vs/editor/common/model/treeSitterTokens.ts index 7f8f91bb276..eee41ee17df 100644 --- a/code/src/vs/editor/common/model/treeSitterTokens.ts +++ b/code/src/vs/editor/common/model/treeSitterTokens.ts @@ -7,10 +7,11 @@ import { ILanguageIdCodec, ITreeSitterTokenizationSupport, TreeSitterTokenizatio import { LineTokens } from '../tokens/lineTokens.js'; import { StandardTokenType } from '../encodedTokenAttributes.js'; import { TextModel } from './textModel.js'; -import { ITreeSitterParserService } from '../services/treeSitterParserService.js'; import { IModelContentChangedEvent } from '../textModelEvents.js'; import { AbstractTokens } from './tokens.js'; import { IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; +import { ITreeSitterTokenizationStoreService } from './treeSitterTokenStoreService.js'; +import { Range } from '../core/range.js'; export class TreeSitterTokens extends AbstractTokens { private _tokenizationSupport: ITreeSitterTokenizationSupport | null = null; @@ -20,7 +21,7 @@ export class TreeSitterTokens extends AbstractTokens { constructor(languageIdCodec: ILanguageIdCodec, textModel: TextModel, languageId: () => string, - @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService) { + @ITreeSitterTokenizationStoreService private readonly _tokenStore: ITreeSitterTokenizationStoreService) { super(languageIdCodec, textModel, languageId); this._initialize(); @@ -42,7 +43,7 @@ export class TreeSitterTokens extends AbstractTokens { public getLineTokens(lineNumber: number): LineTokens { const content = this._textModel.getLineContent(lineNumber); if (this._tokenizationSupport) { - const rawTokens = this._tokenizationSupport.tokenizeEncoded(lineNumber, this._textModel); + const rawTokens = this._tokenStore.getTokens(this._textModel, lineNumber); if (rawTokens) { return new LineTokens(rawTokens, content, this._languageIdCodec); } @@ -77,16 +78,17 @@ export class TreeSitterTokens extends AbstractTokens { } public override forceTokenization(lineNumber: number): void { - // TODO @alexr00 implement + if (this._tokenizationSupport) { + this._tokenizationSupport.tokenizeEncoded(lineNumber, this._textModel); + } } public override hasAccurateTokensForLine(lineNumber: number): boolean { - // TODO @alexr00 update for background tokenization - return true; + return this._tokenStore.hasTokens(this._textModel, new Range(lineNumber, 1, lineNumber, this._textModel.getLineMaxColumn(lineNumber))); } public override isCheapToTokenize(lineNumber: number): boolean { - // TODO @alexr00 update for background tokenization + // TODO @alexr00 determine what makes it cheap to tokenize? return true; } @@ -99,8 +101,6 @@ export class TreeSitterTokens extends AbstractTokens { return null; } public override get hasTokens(): boolean { - // TODO @alexr00 once we have a token store, implement properly - const hasTree = this._treeSitterService.getParseResult(this._textModel) !== undefined; - return hasTree; + return this._tokenStore.hasTokens(this._textModel); } } diff --git a/code/src/vs/editor/common/services/languageFeatures.ts b/code/src/vs/editor/common/services/languageFeatures.ts index 13977f1c1f6..0379781a0de 100644 --- a/code/src/vs/editor/common/services/languageFeatures.ts +++ b/code/src/vs/editor/common/services/languageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageFeatureRegistry, NotebookInfoResolver } from '../languageFeatureRegistry.js'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MappedEditsProvider, MultiDocumentHighlightProvider, NewSymbolNamesProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, InlineEditProvider } from '../languages.js'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MultiDocumentHighlightProvider, NewSymbolNamesProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, InlineEditProvider } from '../languages.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; export const ILanguageFeaturesService = createDecorator('ILanguageFeaturesService'); @@ -77,8 +77,6 @@ export interface ILanguageFeaturesService { readonly documentDropEditProvider: LanguageFeatureRegistry; - readonly mappedEditsProvider: LanguageFeatureRegistry; - // -- setNotebookTypeResolver(resolver: NotebookInfoResolver | undefined): void; diff --git a/code/src/vs/editor/common/services/languageFeaturesService.ts b/code/src/vs/editor/common/services/languageFeaturesService.ts index 5c8d6de6919..02ac4418841 100644 --- a/code/src/vs/editor/common/services/languageFeaturesService.ts +++ b/code/src/vs/editor/common/services/languageFeaturesService.ts @@ -5,7 +5,7 @@ import { URI } from '../../../base/common/uri.js'; import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from '../languageFeatureRegistry.js'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, MultiDocumentHighlightProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, MappedEditsProvider, NewSymbolNamesProvider, InlineEditProvider } from '../languages.js'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, MultiDocumentHighlightProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, NewSymbolNamesProvider, InlineEditProvider } from '../languages.js'; import { ILanguageFeaturesService } from './languageFeatures.js'; import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; @@ -45,7 +45,6 @@ export class LanguageFeaturesService implements ILanguageFeaturesService { readonly documentSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentDropEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentPasteEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); - readonly mappedEditsProvider: LanguageFeatureRegistry = new LanguageFeatureRegistry(this._score.bind(this)); private _notebookTypeResolver?: NotebookInfoResolver; diff --git a/code/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts similarity index 56% rename from code/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts rename to code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 81d1736d775..4ca0e3396de 100644 --- a/code/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts +++ b/code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -5,12 +5,12 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { AppResourcePath, FileAccess, nodeModulesAsarUnpackedPath, nodeModulesPath } from '../../../../base/common/network.js'; -import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter } from '../../../common/services/treeSitterParserService.js'; -import { IModelService } from '../../../common/services/model.js'; +import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, RangeChange, TreeUpdateEvent, TreeParseUpdateEvent } from '../treeSitterParserService.js'; +import { IModelService } from '../model.js'; import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ITextModel } from '../../../common/model.js'; +import { ITextModel } from '../../model.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { IModelContentChange } from '../../../common/textModelEvents.js'; +import { IModelContentChange, IModelContentChangedEvent } from '../../textModelEvents.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -21,7 +21,10 @@ import { CancellationToken, cancelOnDispose } from '../../../../base/common/canc import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { PromiseResult } from '../../../../base/common/observable.js'; -import { Range } from '../../../common/core/range.js'; +import { Range } from '../../core/range.js'; +import { Position } from '../../core/position.js'; +import { LimitedQueue } from '../../../../base/common/async.js'; +import { TextLength } from '../../core/textLength.js'; const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; @@ -32,9 +35,10 @@ function getModuleLocation(environmentService: IEnvironmentService): AppResource } export class TextModelTreeSitter extends Disposable implements ITextModelTreeSitter { - private _onDidChangeParseResult: Emitter = this._register(new Emitter()); - public readonly onDidChangeParseResult: Event = this._onDidChangeParseResult.event; + private _onDidChangeParseResult: Emitter = this._register(new Emitter()); + public readonly onDidChangeParseResult: Event = this._onDidChangeParseResult.event; private _parseResult: TreeSitterParseResult | undefined; + private _versionId: number = 0; get parseResult(): ITreeSitterParseResult | undefined { return this._parseResult; } @@ -53,7 +57,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit } } - private readonly _languageSessionDisposables = this._register(new DisposableStore()); + private readonly _parseSessionDisposables = this._register(new DisposableStore()); /** * Be very careful when making changes to this method as it is easy to introduce race conditions. */ @@ -62,10 +66,10 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit } public async parse(languageId: string = this.model.getLanguageId()): Promise { - this._languageSessionDisposables.clear(); + this._parseSessionDisposables.clear(); this._parseResult = undefined; - const token = cancelOnDispose(this._languageSessionDisposables); + const token = cancelOnDispose(this._parseSessionDisposables); let language: Parser.Language | undefined; try { language = await this._getLanguage(languageId, token); @@ -81,14 +85,20 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit return; } - const treeSitterTree = this._languageSessionDisposables.add(new TreeSitterParseResult(new Parser(), language, this._logService, this._telemetryService)); - this._languageSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e.changes))); - await this._onDidChangeContent(treeSitterTree, []); + const treeSitterTree = this._parseSessionDisposables.add(new TreeSitterParseResult(new Parser(), language, this._logService, this._telemetryService)); + this._parseResult = treeSitterTree; + this._parseSessionDisposables.add(treeSitterTree.onDidUpdate(e => { + if (e.ranges && (e.versionId > this._versionId)) { + this._versionId = e.versionId; + this._onDidChangeParseResult.fire({ ranges: e.ranges, versionId: e.versionId }); + } + })); + this._parseSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e))); + this._onDidChangeContent(treeSitterTree, undefined); if (token.isCancellationRequested) { return; } - this._parseResult = treeSitterTree; return this._parseResult; } @@ -113,13 +123,8 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit }); } - private async _onDidChangeContent(treeSitterTree: TreeSitterParseResult, changes: IModelContentChange[]) { - const diff = await treeSitterTree.onDidChangeContent(this.model, changes); - if (!diff || diff.length > 0) { - // Tree sitter is 0 based, text model is 1 based - const ranges = diff ? diff.map(r => new Range(r.startPosition.row + 1, r.startPosition.column + 1, r.endPosition.row + 1, r.endPosition.column + 1)) : [this.model.getFullModelRange()]; - this._onDidChangeParseResult.fire(ranges); - } + private _onDidChangeContent(treeSitterTree: TreeSitterParseResult, change: IModelContentChangedEvent | undefined) { + return treeSitterTree.onDidChangeContent(this.model, change); } } @@ -128,8 +133,27 @@ const enum TelemetryParseType { Incremental = 'incrementalParse' } +interface ChangedRange { + newNodeId: number; + newStartPosition: Position; + newEndPosition: Position; + newStartIndex: number; + newEndIndex: number; + oldStartIndex: number; + oldEndIndex: number; +} + export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResult { private _tree: Parser.Tree | undefined; + private _lastFullyParsed: Parser.Tree | undefined; + private _lastFullyParsedWithEdits: Parser.Tree | undefined; + private readonly _onDidUpdate: Emitter = new Emitter(); + public readonly onDidUpdate: Event = this._onDidUpdate.event; + private _versionId: number = 0; + private _editVersion: number = 0; + get versionId() { + return this._versionId; + } private _isDisposed: boolean = false; constructor(public readonly parser: Parser, public /** exposed for tests **/ readonly language: Parser.Language, @@ -140,58 +164,239 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } dispose(): void { this._isDisposed = true; + this._onDidUpdate.dispose(); this._tree?.delete(); + this._lastFullyParsed?.delete(); + this._lastFullyParsedWithEdits?.delete(); this.parser?.delete(); } - get tree() { return this._tree; } - private set tree(newTree: Parser.Tree | undefined) { - this._tree?.delete(); - this._tree = newTree; - } + get tree() { return this._lastFullyParsed; } get isDisposed() { return this._isDisposed; } - private _onDidChangeContentQueue: Promise = Promise.resolve(); - public async onDidChangeContent(model: ITextModel, changes: IModelContentChange[]): Promise { - const oldTree = this.tree?.copy(); - this._applyEdits(model, changes); - return new Promise(resolve => { - this._onDidChangeContentQueue = this._onDidChangeContentQueue.then(async () => { - if (this.isDisposed) { - // No need to continue the queue if we are disposed - return; + private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree): ChangedRange[] { + const newCursor = newTree.walk(); + const oldCursor = oldTree.walk(); + const gotoNextSibling = () => { + const n = newCursor.gotoNextSibling(); + const o = oldCursor.gotoNextSibling(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + return n && o; + }; + const gotoParent = () => { + const n = newCursor.gotoParent(); + const o = oldCursor.gotoParent(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + return n && o; + }; + const gotoNthChild = (index: number) => { + const n = newCursor.gotoFirstChild(); + const o = oldCursor.gotoFirstChild(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + if (index === 0) { + return n && o; + } + for (let i = 1; i <= index; i++) { + const nn = newCursor.gotoNextSibling(); + const oo = oldCursor.gotoNextSibling(); + if (nn !== oo) { + throw new Error('Trees are out of sync'); + } + if (!nn || !oo) { + return false; + } + } + return n && o; + }; + + const changedRanges: ChangedRange[] = []; + let next = true; + const nextSiblingOrParentSibling = () => { + do { + if (newCursor.currentNode.nextSibling) { + return gotoNextSibling(); + } + if (newCursor.currentNode.parent) { + gotoParent(); + } + } while (newCursor.currentNode.nextSibling || newCursor.currentNode.parent); + return false; + }; + + const getClosestPreviousNodes = (): { old: Parser.SyntaxNode; new: Parser.SyntaxNode } | undefined => { + // Go up parents until the end of the parent is before the start of the current. + const newFindPrev = newTree.walk(); + newFindPrev.resetTo(newCursor); + const oldFindPrev = oldTree.walk(); + oldFindPrev.resetTo(oldCursor); + const startingNode = newCursor.currentNode; + do { + if (newFindPrev.currentNode.previousSibling && ((newFindPrev.currentNode.endIndex - newFindPrev.currentNode.startIndex) !== 0)) { + newFindPrev.gotoPreviousSibling(); + oldFindPrev.gotoPreviousSibling(); + } else { + while (!newFindPrev.currentNode.previousSibling && newFindPrev.currentNode.parent) { + newFindPrev.gotoParent(); + oldFindPrev.gotoParent(); + } + newFindPrev.gotoPreviousSibling(); + oldFindPrev.gotoPreviousSibling(); + } + } while ((newFindPrev.currentNode.endIndex > startingNode.startIndex) + && (newFindPrev.currentNode.parent || newFindPrev.currentNode.previousSibling) + + && (newFindPrev.currentNode.id !== startingNode.id)); + + if ((newFindPrev.currentNode.id !== startingNode.id) && newFindPrev.currentNode.endIndex <= startingNode.startIndex) { + return { old: oldFindPrev.currentNode, new: newFindPrev.currentNode }; + } else { + return undefined; + } + }; + do { + if (newCursor.currentNode.hasChanges) { + // Check if only one of the children has changes. + // If it's only one, then we go to that child. + // If it's more then, we need to go to each child + // If it's none, then we've found one of our ranges + const newChildren = newCursor.currentNode.children; + const indexChangedChildren: number[] = []; + const changedChildren = newChildren.filter((c, index) => { + if (c.hasChanges) { + indexChangedChildren.push(index); + } + return c.hasChanges; + }); + if (changedChildren.length >= 1) { + next = gotoNthChild(indexChangedChildren[0]); + } else if (changedChildren.length === 0) { + // walk up again until we get to the first one that's named as unnamed nodes can be too granular + while (newCursor.currentNode.parent && !newCursor.currentNode.isNamed && next) { + next = gotoParent(); + } + + const newNode = newCursor.currentNode; + const oldNode = oldCursor.currentNode; + + const newEndPosition = new Position(newNode.endPosition.row + 1, newNode.endPosition.column + 1); + const oldEndIndex = oldNode.endIndex; + + // Fill holes between nodes. + const closestPrev = getClosestPreviousNodes(); + const newStartPosition = new Position(closestPrev ? closestPrev.new.endPosition.row + 1 : newNode.startPosition.row + 1, closestPrev ? closestPrev.new.endPosition.column + 1 : newNode.startPosition.column + 1); + const newStartIndex = closestPrev ? closestPrev.new.endIndex : newNode.startIndex; + const oldStartIndex = closestPrev ? closestPrev.old.endIndex : oldNode.startIndex; + + changedRanges.push({ newStartPosition, newEndPosition, oldStartIndex, oldEndIndex, newNodeId: newNode.id, newStartIndex, newEndIndex: newNode.endIndex }); + next = nextSiblingOrParentSibling(); } - await this._parseAndUpdateTree(model); - resolve((this.tree && oldTree) ? oldTree.getChangedRanges(this.tree) : undefined); + } else { + next = nextSiblingOrParentSibling(); + } + } while (next); - }).catch((e) => { - this._logService.error('Error parsing tree-sitter tree', e); - }); + if (changedRanges.length === 0 && newTree.rootNode.hasChanges) { + return [{ newStartPosition: new Position(newTree.rootNode.startPosition.row + 1, newTree.rootNode.startPosition.column + 1), newEndPosition: new Position(newTree.rootNode.endPosition.row + 1, newTree.rootNode.endPosition.column + 1), oldStartIndex: oldTree.rootNode.startIndex, oldEndIndex: oldTree.rootNode.endIndex, newStartIndex: newTree.rootNode.startIndex, newEndIndex: newTree.rootNode.endIndex, newNodeId: newTree.rootNode.id }]; + } else { + return changedRanges; + } + } + + private calculateRangeChange(changedNodes: ChangedRange[] | undefined): RangeChange[] | undefined { + if (!changedNodes) { + return undefined; + } + + // Collapse conginguous ranges + const ranges: RangeChange[] = []; + for (let i = 0; i < changedNodes.length; i++) { + const node = changedNodes[i]; + + // Check if contiguous with previous + const prevNode = changedNodes[i - 1]; + if ((i > 0) && prevNode.newEndPosition.equals(node.newStartPosition)) { + const prevRangeChange = ranges[ranges.length - 1]; + prevRangeChange.newRange = new Range(prevRangeChange.newRange.startLineNumber, prevRangeChange.newRange.startColumn, node.newEndPosition.lineNumber, node.newEndPosition.column); + prevRangeChange.oldRangeLength = node.oldEndIndex - prevNode.oldStartIndex; + prevRangeChange.newRangeEndOffset = node.newEndIndex; + } else { + ranges.push({ newRange: Range.fromPositions(node.newStartPosition, node.newEndPosition), oldRangeLength: node.oldEndIndex - node.oldStartIndex, newRangeStartOffset: node.newStartIndex, newRangeEndOffset: node.newEndIndex }); + } + } + return ranges; + } + + private _onDidChangeContentQueue: LimitedQueue = new LimitedQueue(); + public onDidChangeContent(model: ITextModel, changes: IModelContentChangedEvent | undefined): void { + const version = model.getVersionId(); + if (version === this._editVersion) { + return; + } + + this._applyEdits(changes?.changes ?? [], version); + + this._onDidChangeContentQueue.queue(async () => { + if (this.isDisposed) { + // No need to continue the queue if we are disposed + return; + } + + let ranges: RangeChange[] | undefined; + if (this._lastFullyParsedWithEdits && this._lastFullyParsed) { + ranges = this.calculateRangeChange(this.findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed)); + } + + const completed = await this._parseAndUpdateTree(model, version); + if (completed) { + if (!ranges) { + ranges = [{ newRange: model.getFullModelRange(), oldRangeLength: model.getValueLength(), newRangeStartOffset: 0, newRangeEndOffset: model.getValueLength() }]; + } + this._onDidUpdate.fire({ ranges, versionId: version }); + } }); } - private _newEdits = true; - private _applyEdits(model: ITextModel, changes: IModelContentChange[]) { + private _applyEdits(changes: IModelContentChange[], version: number) { for (const change of changes) { - const newEndOffset = change.rangeOffset + change.text.length; - const newEndPosition = model.getPositionAt(newEndOffset); - - this.tree?.edit({ + const originalTextLength = TextLength.ofRange(Range.lift(change.range)); + const newTextLength = TextLength.ofText(change.text); + const summedTextLengths = change.text.length === 0 ? newTextLength : originalTextLength.add(newTextLength); + const edit = { startIndex: change.rangeOffset, oldEndIndex: change.rangeOffset + change.rangeLength, newEndIndex: change.rangeOffset + change.text.length, startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, - newEndPosition: { row: newEndPosition.lineNumber - 1, column: newEndPosition.column - 1 } - }); - this._newEdits = true; + newEndPosition: { row: change.range.startLineNumber + summedTextLengths.lineCount - 1, column: summedTextLengths.lineCount ? summedTextLengths.columnCount : (change.range.endColumn + summedTextLengths.columnCount) } + }; + this._tree?.edit(edit); + this._lastFullyParsedWithEdits?.edit(edit); } + this._editVersion = version; } - private async _parseAndUpdateTree(model: ITextModel) { + private async _parseAndUpdateTree(model: ITextModel, version: number): Promise { const tree = await this._parse(model); - if (!this._newEdits) { - this.tree = tree; + if (tree) { + this._tree?.delete(); + this._tree = tree; + this._lastFullyParsed?.delete(); + this._lastFullyParsed = tree.copy(); + this._lastFullyParsedWithEdits?.delete(); + this._lastFullyParsedWithEdits = tree.copy(); + this._versionId = version; + return tree; + } else if (!this._tree) { + // No tree means this is the inial parse and there were edits + // parse function doesn't handle this well and we can end up with an incorrect tree, so we reset + this.parser.reset(); } + return undefined; } private _parse(model: ITextModel): Promise { @@ -204,14 +409,15 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul private async _parseAndYield(model: ITextModel, parseType: TelemetryParseType): Promise { const language = model.getLanguageId(); - let tree: Parser.Tree | undefined; let time: number = 0; let passes: number = 0; - this._newEdits = false; + const inProgressVersion = this._editVersion; + let newTree: Parser.Tree | undefined; + do { const timer = performance.now(); try { - tree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); + newTree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this._tree); } catch (e) { // parsing can fail when the timeout is reached, will resume upon next loop } finally { @@ -219,15 +425,12 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul passes++; } - // Even if the model changes and edits are applied, the tree parsing will continue correctly after the await. + // So long as this isn't the initial parse, even if the model changes and edits are applied, the tree parsing will continue correctly after the await. await new Promise(resolve => setTimeout0(resolve)); - if (model.isDisposed() || this.isDisposed) { - return; - } - } while (!tree && !this._newEdits); // exit if there a new edits, as anhy parsing done while there are new edits is throw away work + } while (!model.isDisposed() && !this.isDisposed && !newTree && inProgressVersion === model.getVersionId()); this.sendParseTimeTelemetry(parseType, language, time, passes); - return tree; + return (newTree && (inProgressVersion === model.getVersionId())) ? newTree : undefined; } private _parseCallback(textModel: ITextModel, index: number): string | null { @@ -357,8 +560,10 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte private readonly _treeSitterLanguages: TreeSitterLanguages; public readonly onDidAddLanguage: Event<{ id: string; language: Parser.Language }>; - private _onDidUpdateTree: Emitter<{ textModel: ITextModel; ranges: Range[] }> = this._register(new Emitter()); - public readonly onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }> = this._onDidUpdateTree.event; + private _onDidUpdateTree: Emitter = this._register(new Emitter()); + public readonly onDidUpdateTree: Event = this._onDidUpdateTree.event; + + public isTest: boolean = false; constructor(@IModelService private readonly _modelService: IModelService, @IFileService fileService: IFileService, @@ -387,10 +592,11 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte return textModelTreeSitter?.textModelTreeSitter.parseResult; } + /** + * For testing + */ async getTree(content: string, languageId: string): Promise { - await this._init; - - const language = await this._treeSitterLanguages.getLanguage(languageId); + const language = await this.getLanguage(languageId); const Parser = await this._treeSitterImporter.getParserClass(); if (language) { const parser = new Parser(); @@ -400,12 +606,26 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte return undefined; } + /** + * For testing + */ + async getLanguage(languageId: string): Promise { + await this._init; + return this._treeSitterLanguages.getLanguage(languageId); + } + private async _doInitParser() { const Parser = await this._treeSitterImporter.getParserClass(); const environmentService = this._environmentService; + const isTest = this.isTest; await Parser.init({ locateFile(_file: string, _folder: string) { - return FileAccess.asBrowserUri(`${getModuleLocation(environmentService)}/${FILENAME_TREESITTER_WASM}`).toString(true); + const location: AppResourcePath = `${getModuleLocation(environmentService)}/${FILENAME_TREESITTER_WASM}`; + if (isTest) { + return FileAccess.asFileUri(location).toString(true); + } else { + return FileAccess.asBrowserUri(location).toString(true); + } } }); return true; @@ -469,20 +689,22 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte this._modelService.getModels().forEach(model => this._createTextModelTreeSitter(model)); } - public getTextModelTreeSitter(model: ITextModel): ITextModelTreeSitter { - return new TextModelTreeSitter(model, this._treeSitterLanguages, this._treeSitterImporter, this._logService, this._telemetryService, false); + public async getTextModelTreeSitter(model: ITextModel, parseImmediately: boolean = false): Promise { + await this.getLanguage(model.getLanguageId()); + return this._createTextModelTreeSitter(model, parseImmediately); } - private _createTextModelTreeSitter(model: ITextModel) { - const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterLanguages, this._treeSitterImporter, this._logService, this._telemetryService); + private _createTextModelTreeSitter(model: ITextModel, parseImmediately: boolean = true): ITextModelTreeSitter { + const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterLanguages, this._treeSitterImporter, this._logService, this._telemetryService, parseImmediately); const disposables = new DisposableStore(); disposables.add(textModelTreeSitter); - disposables.add(textModelTreeSitter.onDidChangeParseResult((ranges) => this._onDidUpdateTree.fire({ textModel: model, ranges }))); + disposables.add(textModelTreeSitter.onDidChangeParseResult(change => this._onDidUpdateTree.fire({ textModel: model, ranges: change.ranges ?? [], versionId: change.versionId }))); this._textModelTreeSitters.set(model, { textModelTreeSitter, disposables, dispose: disposables.dispose.bind(disposables) }); + return textModelTreeSitter; } private _addGrammar(languageId: string, grammarName: string) { diff --git a/code/src/vs/editor/common/services/treeSitterParserService.ts b/code/src/vs/editor/common/services/treeSitterParserService.ts index a19f3093be0..4d0ddb3f971 100644 --- a/code/src/vs/editor/common/services/treeSitterParserService.ts +++ b/code/src/vs/editor/common/services/treeSitterParserService.ts @@ -13,22 +13,41 @@ export const EDITOR_EXPERIMENTAL_PREFER_TREESITTER = 'editor.experimental.prefer export const ITreeSitterParserService = createDecorator('treeSitterParserService'); +export interface RangeChange { + newRange: Range; + oldRangeLength: number; + newRangeStartOffset: number; + newRangeEndOffset: number; +} + +export interface TreeParseUpdateEvent { + ranges: RangeChange[] | undefined; + versionId: number; +} + +export interface TreeUpdateEvent { + textModel: ITextModel; + ranges: RangeChange[]; + versionId: number; +} + export interface ITreeSitterParserService { readonly _serviceBrand: undefined; onDidAddLanguage: Event<{ id: string; language: Parser.Language }>; getOrInitLanguage(languageId: string): Parser.Language | undefined; getParseResult(textModel: ITextModel): ITreeSitterParseResult | undefined; getTree(content: string, languageId: string): Promise; - onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }>; + onDidUpdateTree: Event; /** * For testing purposes so that the time to parse can be measured. */ - getTextModelTreeSitter(textModel: ITextModel): ITextModelTreeSitter | undefined; + getTextModelTreeSitter(model: ITextModel, parseImmediately?: boolean): Promise; } export interface ITreeSitterParseResult { readonly tree: Parser.Tree | undefined; readonly language: Parser.Language; + versionId: number; } export interface ITextModelTreeSitter { diff --git a/code/src/vs/editor/common/textModelEvents.ts b/code/src/vs/editor/common/textModelEvents.ts index 768563c4be6..c35d0472106 100644 --- a/code/src/vs/editor/common/textModelEvents.ts +++ b/code/src/vs/editor/common/textModelEvents.ts @@ -34,7 +34,7 @@ export interface IModelLanguageConfigurationChangedEvent { export interface IModelContentChange { /** - * The range that got replaced. + * The old range that got replaced. */ readonly range: IRange; /** diff --git a/code/src/vs/editor/contrib/codelens/browser/codeLensCache.ts b/code/src/vs/editor/contrib/codelens/browser/codeLensCache.ts index 5f479695091..6dcde99c369 100644 --- a/code/src/vs/editor/contrib/codelens/browser/codeLensCache.ts +++ b/code/src/vs/editor/contrib/codelens/browser/codeLensCache.ts @@ -77,7 +77,7 @@ export class CodeLensCache implements ICodeLensCache { }; }); const copyModel = new CodeLensModel(); - copyModel.add({ lenses: copyItems, dispose: () => { } }, this._fakeProvider); + copyModel.add({ lenses: copyItems }, this._fakeProvider); const item = new CacheItem(model.getLineCount(), copyModel); this._cache.set(model.uri.toString(), item); diff --git a/code/src/vs/editor/contrib/codelens/browser/codelens.ts b/code/src/vs/editor/contrib/codelens/browser/codelens.ts index 0a777339b04..be24cae08b0 100644 --- a/code/src/vs/editor/contrib/codelens/browser/codelens.ts +++ b/code/src/vs/editor/contrib/codelens/browser/codelens.ts @@ -5,7 +5,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, isDisposable } from '../../../../base/common/lifecycle.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ITextModel } from '../../../common/model.js'; @@ -24,18 +24,21 @@ export class CodeLensModel { lenses: CodeLensItem[] = []; - private readonly _disposables = new DisposableStore(); + private _store: DisposableStore | undefined; dispose(): void { - this._disposables.dispose(); + this._store?.dispose(); } get isDisposed(): boolean { - return this._disposables.isDisposed; + return this._store?.isDisposed ?? false; } add(list: CodeLensList, provider: CodeLensProvider): void { - this._disposables.add(list); + if (isDisposable(list)) { + this._store ??= new DisposableStore(); + this._store.add(list); + } for (const symbol of list.lenses) { this.lenses.push({ symbol, provider }); } diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index ed17348099a..6efeeb360f8 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -8,7 +8,7 @@ import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { createStringDataTransferItem, matchesMimeType, UriList, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { createStringDataTransferItem, IReadonlyVSDataTransfer, matchesMimeType, UriList, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -74,6 +74,11 @@ export type PastePreference = | { readonly providerId: string } // Only used internally ; +interface CopyOperation { + readonly providerMimeTypes: readonly string[]; + readonly operation: CancelablePromise; +} + export class CopyPasteController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.copyPasteActionController'; @@ -97,7 +102,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi */ private static _currentCopyOperation?: { readonly handle: string; - readonly dataTransferPromise: CancelablePromise; + readonly operations: ReadonlyArray; }; private readonly _editor: ICodeEditor; @@ -222,31 +227,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi defaultPastePayload }); - const promise = createCancelablePromise(async token => { - const results = coalesce(await Promise.all(providers.map(async provider => { - try { - return await provider.prepareDocumentPaste!(model, ranges, dataTransfer, token); - } catch (err) { - console.error(err); - return undefined; - } - }))); - - // Values from higher priority providers should overwrite values from lower priority ones. - // Reverse the array to so that the calls to `replace` below will do this - results.reverse(); - - for (const result of results) { - for (const [mime, value] of result) { - dataTransfer.replace(mime, value); - } - } - - return dataTransfer; + const operations = providers.map((provider): CopyOperation => { + return { + providerMimeTypes: provider.copyMimeTypes, + operation: createCancelablePromise(token => + provider.prepareDocumentPaste!(model, ranges, dataTransfer, token) + .catch(err => { + console.error(err); + return undefined; + })) + }; }); - CopyPasteController._currentCopyOperation?.dataTransferPromise.cancel(); - CopyPasteController._currentCopyOperation = { handle: handle, dataTransferPromise: promise }; + CopyPasteController._currentCopyOperation?.operations.forEach(entry => entry.operation.cancel()); + CopyPasteController._currentCopyOperation = { handle, operations }; } private async handlePaste(e: ClipboardEvent) { @@ -356,7 +350,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const token = cts.token; try { - await this.mergeInDataFromCopy(dataTransfer, metadata, token); + await this.mergeInDataFromCopy(allProviders, dataTransfer, metadata, token); if (token.isCancellationRequested) { return; } @@ -371,6 +365,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const context: DocumentPasteContext = { triggerKind: DocumentPasteTriggerKind.Automatic, }; + const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token); disposables.add(editSession); if (token.isCancellationRequested) { @@ -448,7 +443,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const disposables = new DisposableStore(); const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token)); try { - await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); + await this.mergeInDataFromCopy(allProviders, dataTransfer, metadata, tokenSource.token); if (tokenSource.token.isCancellationRequested) { return; } @@ -583,15 +578,26 @@ export class CopyPasteController extends Disposable implements IEditorContributi return undefined; } - private async mergeInDataFromCopy(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken): Promise { + private async mergeInDataFromCopy(allProviders: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken): Promise { if (metadata?.id && CopyPasteController._currentCopyOperation?.handle === metadata.id) { - const toMergeDataTransfer = await CopyPasteController._currentCopyOperation.dataTransferPromise; + // Only resolve providers that have data we may care about + const toResolve = CopyPasteController._currentCopyOperation.operations + .filter(op => allProviders.some(provider => provider.pasteMimeTypes.some(type => matchesMimeType(type, op.providerMimeTypes)))) + .map(op => op.operation); + + const toMergeResults = await Promise.all(toResolve); if (token.isCancellationRequested) { return; } - for (const [key, value] of toMergeDataTransfer) { - dataTransfer.replace(key, value); + // Values from higher priority providers should overwrite values from lower priority ones. + // Reverse the array to so that the calls to `DataTransfer.replace` later will do this + for (const toMergeData of toMergeResults.reverse()) { + if (toMergeData) { + for (const [key, value] of toMergeData) { + dataTransfer.replace(key, value); + } + } } } diff --git a/code/src/vs/editor/contrib/find/browser/findController.ts b/code/src/vs/editor/contrib/find/browser/findController.ts index 128d89dc571..6b6102ea5ac 100644 --- a/code/src/vs/editor/contrib/find/browser/findController.ts +++ b/code/src/vs/editor/contrib/find/browser/findController.ts @@ -33,6 +33,7 @@ import { IThemeService, themeColorFromId } from '../../../../platform/theme/comm import { Selection } from '../../../common/core/selection.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { FindWidgetSearchHistory } from './findWidgetSearchHistory.js'; +import { ReplaceWidgetHistory } from './replaceWidgetHistory.js'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -444,6 +445,7 @@ export class FindController extends CommonFindController implements IFindControl private _widget: FindWidget | null; private _findOptionsWidget: FindOptionsWidget | null; private _findWidgetSearchHistory: FindWidgetSearchHistory; + private _replaceWidgetHistory: ReplaceWidgetHistory; constructor( editor: ICodeEditor, @@ -460,6 +462,7 @@ export class FindController extends CommonFindController implements IFindControl this._widget = null; this._findOptionsWidget = null; this._findWidgetSearchHistory = FindWidgetSearchHistory.getOrCreate(_storageService); + this._replaceWidgetHistory = ReplaceWidgetHistory.getOrCreate(_storageService); } protected override async _start(opts: IFindStartOptions, newState?: INewFindReplaceState): Promise { @@ -511,7 +514,7 @@ export class FindController extends CommonFindController implements IFindControl } private _createFindWidget() { - this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._hoverService, this._findWidgetSearchHistory)); + this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._hoverService, this._findWidgetSearchHistory, this._replaceWidgetHistory)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService)); } diff --git a/code/src/vs/editor/contrib/find/browser/findWidget.ts b/code/src/vs/editor/contrib/find/browser/findWidget.ts index 96786676a39..e60addcd084 100644 --- a/code/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/code/src/vs/editor/contrib/find/browser/findWidget.ts @@ -175,6 +175,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL notificationService: INotificationService, private readonly _hoverService: IHoverService, private readonly _findWidgetSearchHistory: IHistory | undefined, + private readonly _replaceWidgetHistory: IHistory | undefined, ) { super(); this._codeEditor = codeEditor; @@ -942,6 +943,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL const flexibleWidth = true; // Find input const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).history; + const replaceHistoryConfig = this._codeEditor.getOption(EditorOption.find).replaceHistory; this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewProvider, { width: FIND_INPUT_AREA_WIDTH, label: NLS_FIND_INPUT_LABEL, @@ -1112,13 +1114,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, appendPreserveCaseLabel: this._keybindingLabelFor(FIND_IDS.TogglePreserveCaseCommand), - history: [], + history: replaceHistoryConfig === 'workspace' ? this._replaceWidgetHistory : new Set([]), flexibleHeight, flexibleWidth, flexibleMaxHeight: 118, showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService), inputBoxStyles: defaultInputBoxStyles, - toggleStyles: defaultToggleStyles + toggleStyles: defaultToggleStyles, }, this._contextKeyService, true)); this._replaceInput.setPreserveCase(!!this._state.preserveCase); this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e))); diff --git a/code/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts b/code/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts new file mode 100644 index 00000000000..a570cc7b9e2 --- /dev/null +++ b/code/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../base/common/event.js'; +import { IHistory } from '../../../../base/common/history.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +export class ReplaceWidgetHistory implements IHistory { + public static readonly FIND_HISTORY_KEY = 'workbench.replace.history'; + private inMemoryValues: Set = new Set(); + public onDidChange?: Event; + private _onDidChangeEmitter: Emitter; + + private static _instance: ReplaceWidgetHistory | null = null; + + static getOrCreate( + storageService: IStorageService, + ): ReplaceWidgetHistory { + if (!ReplaceWidgetHistory._instance) { + ReplaceWidgetHistory._instance = new ReplaceWidgetHistory(storageService); + } + return ReplaceWidgetHistory._instance; + } + + constructor( + @IStorageService private readonly storageService: IStorageService, + ) { + this._onDidChangeEmitter = new Emitter(); + this.onDidChange = this._onDidChangeEmitter.event; + this.load(); + } + + delete(t: string): boolean { + const result = this.inMemoryValues.delete(t); + this.save(); + return result; + } + + add(t: string): this { + this.inMemoryValues.add(t); + this.save(); + return this; + } + + has(t: string): boolean { + return this.inMemoryValues.has(t); + } + + clear(): void { + this.inMemoryValues.clear(); + this.save(); + } + + forEach(callbackfn: (value: string, value2: string, set: Set) => void, thisArg?: any): void { + // fetch latest from storage + this.load(); + return this.inMemoryValues.forEach(callbackfn); + } + replace?(t: string[]): void { + this.inMemoryValues = new Set(t); + this.save(); + } + + load() { + let result: [] | undefined; + const raw = this.storageService.get( + ReplaceWidgetHistory.FIND_HISTORY_KEY, + StorageScope.WORKSPACE + ); + + if (raw) { + try { + result = JSON.parse(raw); + } catch (e) { + // Invalid data + } + } + + this.inMemoryValues = new Set(result || []); + } + + // Run saves async + save(): Promise { + const elements: string[] = []; + this.inMemoryValues.forEach(e => elements.push(e)); + return new Promise(resolve => { + this.storageService.store( + ReplaceWidgetHistory.FIND_HISTORY_KEY, + JSON.stringify(elements), + StorageScope.WORKSPACE, + StorageTarget.USER, + ); + this._onDidChangeEmitter.fire(elements); + resolve(); + }); + } +} diff --git a/code/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/code/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 58adf582bdd..a1a3afe9a5b 100644 --- a/code/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/code/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -118,7 +118,7 @@ class DebugEditorGpuRendererAction extends EditorAction { } const tokenMetadata = 0; const charMetadata = 0; - const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata); + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata, 0); if (!rasterizedGlyph) { return; } diff --git a/code/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/code/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts index 81ccfcbc60b..a80bdb7966f 100644 --- a/code/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts +++ b/code/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -273,6 +273,7 @@ class RenderedContentHoverParts extends Disposable { ...hoverContext }; const disposables = new DisposableStore(); + disposables.add(statusBar); for (const participant of participants) { const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext); disposables.add(renderedHoverParts); @@ -294,7 +295,7 @@ class RenderedContentHoverParts extends Disposable { actions: renderedStatusBar.actions, }); } - return toDisposable(() => { disposables.dispose(); }); + return disposables; } private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts { diff --git a/code/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/code/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index fdfc464bc04..1b30ad9357a 100644 --- a/code/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/code/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -17,6 +17,7 @@ import { getHoverAccessibleViewHint, HoverWidget } from '../../../../base/browse import { PositionAffinity } from '../../../common/model.js'; import { Emitter } from '../../../../base/common/event.js'; import { RenderedContentHover } from './contentHoverRendered.js'; +import { ScrollEvent } from '../../../../base/common/scrollable.js'; const HORIZONTAL_SCROLLING_BY = 30; @@ -37,6 +38,9 @@ export class ContentHoverWidget extends ResizableContentWidget { private readonly _onDidResize = this._register(new Emitter()); public readonly onDidResize = this._onDidResize.event; + private readonly _onDidScroll = this._register(new Emitter()); + public readonly onDidScroll = this._onDidScroll.event; + public get isVisibleFromKeyboard(): boolean { return (this._renderedHover?.source === HoverStartSource.Keyboard); } @@ -86,6 +90,9 @@ export class ContentHoverWidget extends ResizableContentWidget { this._register(focusTracker.onDidBlur(() => { this._hoverFocusedKey.set(false); })); + this._register(this._hover.scrollbar.onScroll((e) => { + this._onDidScroll.fire(e); + })); this._setRenderedHover(undefined); this._editor.addContentWidget(this); } diff --git a/code/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts b/code/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts index ad9a9920967..1fa7e2dc9e4 100644 --- a/code/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts +++ b/code/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts @@ -58,6 +58,9 @@ export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidge this._register(this._contentHoverWidget.onDidResize(() => { this._participants.forEach(participant => participant.handleResize?.()); })); + this._register(this._contentHoverWidget.onDidScroll((e) => { + this._participants.forEach(participant => participant.handleScroll?.(e)); + })); return participants; } diff --git a/code/src/vs/editor/contrib/hover/browser/hover.css b/code/src/vs/editor/contrib/hover/browser/hover.css index 1314484424b..2f5c2352713 100644 --- a/code/src/vs/editor/contrib/hover/browser/hover.css +++ b/code/src/vs/editor/contrib/hover/browser/hover.css @@ -44,24 +44,30 @@ } .monaco-editor .monaco-hover .hover-row .verbosity-actions { + border-right: 1px solid var(--vscode-editorHoverWidget-border); + width: 22px; + overflow: hidden; +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner { display: flex; flex-direction: column; padding-left: 5px; padding-right: 5px; justify-content: flex-end; - border-right: 1px solid var(--vscode-editorHoverWidget-border); + position: relative; } -.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon { +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner .codicon { cursor: pointer; font-size: 11px; } -.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.enabled { +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner .codicon.enabled { color: var(--vscode-textLink-foreground); } -.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.disabled { +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner .codicon.disabled { opacity: 0.6; } diff --git a/code/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/code/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index ffce8901c85..0ffff8a229b 100644 --- a/code/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/code/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -6,7 +6,7 @@ import { localize } from '../../../../nls.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { ContentHoverController } from './contentHoverController.js'; import { AccessibleViewType, AccessibleViewProviderId, AccessibleContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -27,7 +27,7 @@ namespace HoverAccessibilityHelpNLS { export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', ``); } -export class HoverAccessibleView implements IAccessibleViewImplentation { +export class HoverAccessibleView implements IAccessibleViewImplementation { public readonly type = AccessibleViewType.View; public readonly priority = 95; @@ -49,7 +49,7 @@ export class HoverAccessibleView implements IAccessibleViewImplentation { } } -export class HoverAccessibilityHelp implements IAccessibleViewImplentation { +export class HoverAccessibilityHelp implements IAccessibleViewImplementation { public readonly priority = 100; public readonly name = 'hover'; @@ -228,7 +228,7 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider } } -export class ExtHoverAccessibleView implements IAccessibleViewImplentation { +export class ExtHoverAccessibleView implements IAccessibleViewImplementation { public readonly type = AccessibleViewType.View; public readonly priority = 90; public readonly name = 'extension-hover'; diff --git a/code/src/vs/editor/contrib/hover/browser/hoverOperation.ts b/code/src/vs/editor/contrib/hover/browser/hoverOperation.ts index daa3e06cc3b..acfc9361a02 100644 --- a/code/src/vs/editor/contrib/hover/browser/hoverOperation.ts +++ b/code/src/vs/editor/contrib/hover/browser/hoverOperation.ts @@ -108,6 +108,7 @@ export class HoverOperation extends Disposable { } private _setState(state: HoverOperationState, options: TArgs): void { + this._options = options; this._state = state; this._fireResult(options); } diff --git a/code/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/code/src/vs/editor/contrib/hover/browser/hoverTypes.ts index ad5442f1fa2..9e85daf3eb6 100644 --- a/code/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/code/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -13,6 +13,7 @@ import { Range } from '../../../common/core/range.js'; import { IModelDecoration } from '../../../common/model.js'; import { BrandedService, IConstructorSignature } from '../../../../platform/instantiation/common/instantiation.js'; import { HoverStartSource } from './hoverOperation.js'; +import { ScrollEvent } from '../../../../base/common/scrollable.js'; export interface IHoverPart { /** @@ -167,6 +168,7 @@ export interface IEditorHoverParticipant { getAccessibleContent(hoverPart: T): string; handleResize?(): void; handleHide?(): void; + handleScroll?(e: ScrollEvent): void; } export type IEditorHoverParticipantCtor = IConstructorSignature; diff --git a/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 3fd5876cfe4..bc663ef46bb 100644 --- a/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -35,6 +35,7 @@ import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry import { getHoverProviderResultsAsAsyncIterable } from './getHover.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { HoverStartSource } from './hoverOperation.js'; +import { ScrollEvent } from '../../../../base/common/scrollable.js'; const $ = dom.$; const increaseHoverVerbosityIcon = registerIcon('hover-increase-verbosity', Codicon.add, nls.localize('increaseHoverVerbosity', 'Icon for increaseing hover verbosity.')); @@ -196,6 +197,10 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { public readonly hoverPart: MarkdownHover, public readonly hoverElement: HTMLElement, public readonly disposables: DisposableStore, + public readonly actionsContainer?: HTMLElement ) { } get hoverAccessibleContent(): string { @@ -295,10 +301,11 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { const actionsContainer = $('div.verbosity-actions'); renderedMarkdownElement.prepend(actionsContainer); - - disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Increase, canIncreaseVerbosity)); - disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); - return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); + const actionsContainerInner = $('div.verbosity-actions-inner'); + actionsContainer.append(actionsContainerInner); + disposables.add(this._renderHoverExpansionAction(actionsContainerInner, HoverVerbosityAction.Increase, canIncreaseVerbosity)); + disposables.add(this._renderHoverExpansionAction(actionsContainerInner, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables, actionsContainerInner); } private _renderMarkdownHover( @@ -333,6 +340,29 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { return store; } + public handleScroll(e: ScrollEvent): void { + this.renderedHoverParts.forEach(renderedHoverPart => { + const actionsContainerInner = renderedHoverPart.actionsContainer; + if (!actionsContainerInner) { + return; + } + const hoverElement = renderedHoverPart.hoverElement; + const topOfHoverScrollPosition = e.scrollTop; + const bottomOfHoverScrollPosition = topOfHoverScrollPosition + e.height; + const topOfRenderedPart = hoverElement.offsetTop; + const hoverElementHeight = hoverElement.clientHeight; + const bottomOfRenderedPart = topOfRenderedPart + hoverElementHeight; + const iconsHeight = 22; + let top: number; + if (bottomOfRenderedPart <= bottomOfHoverScrollPosition || topOfRenderedPart >= bottomOfHoverScrollPosition) { + top = hoverElementHeight - iconsHeight; + } else { + top = bottomOfHoverScrollPosition - topOfRenderedPart - iconsHeight; + } + actionsContainerInner.style.top = `${top}px`; + }); + } + public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number): Promise<{ hoverPart: MarkdownHover; hoverElement: HTMLElement } | undefined> { const model = this._editor.getModel(); if (!model) { @@ -425,7 +455,8 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { const newRenderedHoverPart = new RenderedMarkdownHoverPart( hoverPart, currentRenderedMarkdown, - renderedHoverPart.disposables + renderedHoverPart.disposables, + renderedHoverPart.actionsContainer ); currentRenderedHoverPart.dispose(); this.renderedHoverParts[index] = newRenderedHoverPart; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 2fe48fc4eb9..a95f0b594be 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -84,7 +84,7 @@ export class ExplicitTriggerInlineEditAction extends EditorAction { constructor() { super({ id: 'editor.action.inlineSuggest.triggerInlineEditExplicit', - label: nls.localize2('action.inlineSuggest.trigger.explicitInlineEdit', "Trigger Inline Edit"), + label: nls.localize2('action.inlineSuggest.trigger.explicitInlineEdit', "Trigger Next Edit Suggestion"), precondition: EditorContextKeys.writable, }); } @@ -271,7 +271,7 @@ export class HideInlineCompletion extends EditorAction { label: nls.localize2('action.inlineSuggest.hide', "Hide Inline Suggestion"), precondition: ContextKeyExpr.or(InlineCompletionContextKeys.inlineSuggestionVisible, InlineCompletionContextKeys.inlineEditVisible), kbOpts: { - weight: 100, + weight: KeybindingWeight.EditorContrib + 90, // same as hiding the suggest widget primary: KeyCode.Escape, }, menuOpts: [{ diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index b2b4b8ca6c6..5b9811a2f03 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -8,7 +8,7 @@ import { timeout } from '../../../../../base/common/async.js'; import { cancelOnDispose } from '../../../../../base/common/cancellation.js'; import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { ITransaction, autorun, derived, derivedDisposable, derivedObservableWithCache, observableFromEvent, observableSignal, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; +import { ITransaction, autorun, derived, derivedDisposable, derivedObservableWithCache, observableFromEvent, observableSignal, observableValue, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isUndefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -82,6 +82,13 @@ export class InlineCompletionsController extends Disposable { { min: 50, max: 50 } ); + private readonly _focusIsInMenu = observableValue(this, false); + private readonly _focusIsInEditorOrMenu = derived(this, reader => { + const editorHasFocus = this._editorObs.isFocused.read(reader); + const menuHasFocus = this._focusIsInMenu.read(reader); + return editorHasFocus || menuHasFocus; + }); + private readonly _cursorIsInIndentation = derived(this, reader => { const cursorPos = this._editorObs.cursorPosition.read(reader); if (cursorPos === null) { return false; } @@ -114,7 +121,7 @@ export class InlineCompletionsController extends Disposable { private readonly _hideInlineEditOnSelectionChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => true); - protected readonly _view = this._register(new InlineCompletionsView(this.editor, this.model, this._instantiationService)); + protected readonly _view = this._register(new InlineCompletionsView(this.editor, this.model, this._focusIsInMenu, this._instantiationService)); constructor( public readonly editor: ICodeEditor, @@ -190,7 +197,12 @@ export class InlineCompletionsController extends Disposable { } })); - this._register(this.editor.onDidBlurEditorWidget(() => { + this._register(autorun(reader => { + const isFocused = this._focusIsInEditorOrMenu.read(reader); + if (isFocused) { + return; + } + // This is a hidden setting very useful for debugging if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') @@ -280,7 +292,7 @@ export class InlineCompletionsController extends Disposable { this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.suppressSuggestions, reader => { const model = this.model.read(reader); const state = model?.inlineCompletionState.read(reader); - return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.inlineCompletion.source.inlineCompletions.suppressSuggestions : undefined; + return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.source.inlineCompletions.suppressSuggestions : undefined; })); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.inlineSuggestionVisible, reader => { const model = this.model.read(reader); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 20b6cc6abae..0a4e864d2c6 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -9,13 +9,13 @@ import { ICodeEditorService } from '../../../browser/services/codeEditorService. import { InlineCompletionContextKeys } from './controller/inlineCompletionContextKeys.js'; import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; import { AccessibleViewType, AccessibleViewProviderId, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { InlineCompletionsModel } from './model/inlineCompletionsModel.js'; -export class InlineCompletionsAccessibleView implements IAccessibleViewImplentation { +export class InlineCompletionsAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; readonly priority = 95; readonly name = 'inline-completions'; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts index f5bf0689028..2e9b53e49a1 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts @@ -159,7 +159,7 @@ function deletedCharacters(changes: readonly IDiffChange[]): number { * * The parenthesis are preprocessed to ensure that they match correctly. */ -function smartDiff(originalValue: string, newValue: string, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined { +export function smartDiff(originalValue: string, newValue: string, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined { if (originalValue.length > 5000 || newValue.length > 5000) { // We don't want to work on strings that are too big return undefined; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index efb2b8c4bc0..f824698d7b0 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -57,7 +57,7 @@ export class InlineCompletionsModel extends Disposable { private readonly _suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); private readonly _suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); private readonly _inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); - private readonly _inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.experimental?.enabled); + private readonly _inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); constructor( public readonly textModel: ITextModel, @@ -89,6 +89,19 @@ export class InlineCompletionsModel extends Disposable { } } })); + this._register(autorun(reader => { + /** @description handle text edits collapsing */ + const inlineCompletions = this._source.inlineCompletions.read(reader); + if (!inlineCompletions) { + return; + } + for (const inlineCompletion of inlineCompletions.inlineCompletions) { + const singleEdit = inlineCompletion.toSingleTextEdit(reader); + if (singleEdit.isEmpty) { + this.stop(); + } + } + })); this._register(autorun(reader => { this._editorObs.versionId.read(reader); @@ -204,7 +217,7 @@ export class InlineCompletionsModel extends Disposable { includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, includeInlineEdits: this._inlineEditsEnabled.read(reader), }; - const itemToPreserveCandidate = this.selectedInlineCompletion.get(); + const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable ? itemToPreserveCandidate : undefined; return this._source.fetch(cursorPosition, context, itemToPreserve); @@ -235,9 +248,11 @@ export class InlineCompletionsModel extends Disposable { public stop(stopReason: 'explicitCancel' | 'automatic' = 'automatic', tx?: ITransaction): void { subtransaction(tx, tx => { if (stopReason === 'explicitCancel') { - const completion = this.state.get()?.inlineCompletion?.inlineCompletion; - if (completion && completion.source.provider.handleRejection) { - completion.source.provider.handleRejection(completion.source.inlineCompletions, completion.sourceInlineCompletion); + const inlineCompletion = this.state.get()?.inlineCompletion; + const source = inlineCompletion?.source; + const sourceInlineCompletion = inlineCompletion?.sourceInlineCompletion; + if (sourceInlineCompletion && source?.provider.handleRejection) { + source.provider.handleRejection(source.inlineCompletions, sourceInlineCompletion); } } @@ -261,7 +276,7 @@ export class InlineCompletionsModel extends Disposable { let inlineEdit: InlineCompletionWithUpdatedRange | undefined = undefined; const visibleCompletions: InlineCompletionWithUpdatedRange[] = []; for (const completion of c.inlineCompletions) { - if (!completion.inlineCompletion.sourceInlineCompletion.isInlineEdit) { + if (!completion.sourceInlineCompletion.isInlineEdit) { if (completion.isVisible(this.textModel, cursorPosition, reader)) { visibleCompletions.push(completion); } @@ -306,7 +321,7 @@ export class InlineCompletionsModel extends Disposable { }); public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, - r => this.selectedInlineCompletion.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? [] + r => this.selectedInlineCompletion.read(r)?.source.inlineCompletions.commands ?? [] ); public readonly lastTriggerKind: IObservable @@ -351,13 +366,18 @@ export class InlineCompletionsModel extends Disposable { const model = this.textModel; const item = this._inlineCompletionItems.read(reader); - if (item?.inlineEdit) { - let edit = item.inlineEdit.toSingleTextEdit(reader); + const inlineEditResult = item?.inlineEdit; + if (inlineEditResult) { + if (inlineEditResult.inlineEdit.read(reader) === null) { + return undefined; + } + + let edit = inlineEditResult.toSingleTextEdit(reader); edit = singleTextRemoveCommonPrefix(edit, model); const cursorPos = this.primaryPosition.read(reader); const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber); - const cursorInsideShowRange = cursorAtInlineEdit || (item.inlineEdit.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true); + const cursorInsideShowRange = cursorAtInlineEdit || (inlineEditResult.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true); if (!cursorInsideShowRange && !this._inAcceptFlow.read(reader)) { return undefined; @@ -365,13 +385,13 @@ export class InlineCompletionsModel extends Disposable { const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber); const disableCollapsing = true; - const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEdit.semanticId); + const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === inlineEditResult.semanticId); - const commands = item.inlineEdit.inlineCompletion.source.inlineCompletions.commands; + const commands = inlineEditResult.inlineCompletion.source.inlineCompletions.commands; const renderExplicitly = this._jumpedTo.read(reader); - const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], item.inlineEdit.inlineCompletion); + const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], inlineEditResult.inlineCompletion); - return { kind: 'inlineEdit', inlineEdit, inlineCompletion: item.inlineEdit, edits: [edit], cursorAtInlineEdit }; + return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: [edit], cursorAtInlineEdit }; } this._jumpedTo.set(false, undefined); @@ -575,7 +595,6 @@ export class InlineCompletionsModel extends Disposable { editor.pushUndoStop(); if (completion.snippetInfo) { - // ... editor.executeEdits( 'inlineSuggestion.accept', [ @@ -717,10 +736,11 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = this._computeAugmentation(itemEdit, undefined); if (!augmentedCompletion) { return; } - const inlineCompletion = augmentedCompletion.completion.inlineCompletion; - inlineCompletion.source.provider.handlePartialAccept?.( - inlineCompletion.source.inlineCompletions, - inlineCompletion.sourceInlineCompletion, + const source = augmentedCompletion.completion.source; + const sourceInlineCompletion = augmentedCompletion.completion.sourceInlineCompletion; + source.provider.handlePartialAccept?.( + source.inlineCompletions, + sourceInlineCompletion, itemEdit.text.length, { kind: PartialAcceptTriggerKind.Suggest, diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 79b3b8faacb..d1d476f5e7d 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -7,13 +7,15 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.js'; import { matchesSubString } from '../../../../../base/common/filters.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ITransaction, derivedOpts, disposableObservableValue, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, IReader, ISettableObservable, ITransaction, derivedOpts, disposableObservableValue, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; +import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit } from '../../../../common/core/textEdit.js'; @@ -23,6 +25,8 @@ import { ILanguageConfigurationService } from '../../../../common/languages/lang import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; +import { IModelContentChange, IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; +import { smartDiff } from './computeGhostText.js'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; @@ -34,6 +38,8 @@ export class InlineCompletionsSource extends Disposable { public readonly suggestWidgetInlineCompletions = disposableObservableValue('suggestWidgetInlineCompletions', undefined); private readonly _loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); + private readonly _invalidationDelay = observableConfigValue('editor.inlineSuggest.edits.experimental.invalidationDelay', 4000, this._configurationService).recomputeInitiallyAndOnChange(this._store); + private readonly _structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast< { kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry | { kind: 'end'; error: any; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry @@ -53,8 +59,15 @@ export class InlineCompletionsSource extends Disposable { ) { super(); - this._register(this._textModel.onDidChangeContent(() => { + this._register(this._textModel.onDidChangeContent((e) => { this._updateOperation.clear(); + + const inlineCompletions = this.inlineCompletions.get(); + if (inlineCompletions) { + transaction(tx => { + inlineCompletions.acceptTextModelChangeEvent(e, tx); + }); + } })); } @@ -139,13 +152,20 @@ export class InlineCompletionsSource extends Disposable { return false; } + // Reuse Inline Edit if possible + if (activeInlineCompletion && activeInlineCompletion.isInlineEdit && (activeInlineCompletion.canBeReused(this._textModel, position) || updatedCompletions.has(activeInlineCompletion.inlineCompletion) /* Inline Edit wins over completions if it's already been shown*/)) { + updatedCompletions.dispose(); + return false; + } + const endTime = new Date(); this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime()); - const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId); - if (activeInlineCompletion) { + // Reuse Inline Completion if possible + const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId, this._invalidationDelay); + if (activeInlineCompletion && !activeInlineCompletion.isInlineEdit && activeInlineCompletion.canBeReused(this._textModel, position)) { const asInlineCompletion = activeInlineCompletion.toInlineCompletion(undefined); - if (activeInlineCompletion.canBeReused(this._textModel, position) && !updatedCompletions.has(asInlineCompletion)) { + if (!updatedCompletions.has(asInlineCompletion)) { completions.prepend(activeInlineCompletion.inlineCompletion, asInlineCompletion.range, true); } } @@ -247,6 +267,7 @@ export class UpToDateInlineCompletions implements IDisposable { public readonly request: UpdateRequest, private readonly _textModel: ITextModel, private readonly _versionId: IObservable, + private readonly _invalidationDelay: IObservable, ) { const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({ range: i.range, @@ -256,10 +277,16 @@ export class UpToDateInlineCompletions implements IDisposable { }))); this._inlineCompletions = inlineCompletionProviderResult.completions.map( - (i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId, this.request) + (i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId, this._invalidationDelay, this.request) ); } + public acceptTextModelChangeEvent(e: IModelContentChangedEvent, tx: ITransaction) { + for (const inlineCompletion of this._inlineCompletions) { + inlineCompletion.acceptTextModelChangeEvent(e, tx); + } + } + public clone(): this { this._refCount++; return this; @@ -293,7 +320,7 @@ export class UpToDateInlineCompletions implements IDisposable { description: 'inline-completion-tracking-range' }, }])[0]; - this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId, this.request)); + this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId, this._invalidationDelay, this.request)); this._prependedInlineCompletionItems.push(inlineCompletion); } } @@ -306,29 +333,173 @@ export class InlineCompletionWithUpdatedRange { ]); public get forwardStable() { - return this.inlineCompletion.source.inlineCompletions.enableForwardStability ?? false; + return this.source.inlineCompletions.enableForwardStability ?? false; } private readonly _updatedRange = derivedOpts({ owner: this, equalsFn: Range.equalsRange }, reader => { - this._modelVersion.read(reader); - return this._textModel.getDecorationRange(this.decorationId); + if (this._inlineEdit.read(reader)) { + const edit = this.toSingleTextEdit(reader); + return (edit.isEmpty ? null : edit.range); + } else { + this._modelVersion.read(reader); + return this._textModel.getDecorationRange(this.decorationId); + } }); + /** + * This will be null for ghost text completions + */ + private _inlineEdit: ISettableObservable; + public get inlineEdit(): IObservable { return this._inlineEdit; } + + public get source() { return this.inlineCompletion.source; } + public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; } + public get isInlineEdit() { return this.inlineCompletion.sourceInlineCompletion.isInlineEdit; } + + private _invalidationTime: number | undefined = Date.now() + this._invalidationDelay.get(); + + private _lastChangePartOfInlineEdit = false; + constructor( public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, private readonly _textModel: ITextModel, private readonly _modelVersion: IObservable, + private readonly _invalidationDelay: IObservable, public readonly request: UpdateRequest, ) { + const inlineCompletions = this.inlineCompletion.source.inlineCompletions.items; + if (inlineCompletions.length > 0 && inlineCompletions[inlineCompletions.length - 1].isInlineEdit) { + this._inlineEdit = observableValue(this, this._toIndividualEdits(this.inlineCompletion.range, this.inlineCompletion.insertText)); + } else { + this._inlineEdit = observableValue(this, null); + } + } + + private _toIndividualEdits(range: Range, _replaceText: string): OffsetEdit { + const originalText = this._textModel.getValueInRange(range); + const replaceText = _replaceText.replace(/\r\n|\r|\n/g, this._textModel.getEOL()); + const diffs = smartDiff(originalText, replaceText, false); + const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); + if (!diffs || diffs.length === 0) { + return new OffsetEdit( + [new SingleOffsetEdit(OffsetRange.ofStartAndLength(startOffset, originalText.length), replaceText)] + ); + } + return new OffsetEdit( + diffs.map(diff => { + const originalRange = OffsetRange.ofStartAndLength(startOffset + diff.originalStart, diff.originalLength); + const modifiedText = replaceText.substring(diff.modifiedStart, diff.modifiedStart + diff.modifiedLength); + return new SingleOffsetEdit(originalRange, modifiedText); + }) + ); + } + + public acceptTextModelChangeEvent(e: IModelContentChangedEvent, tx: ITransaction): void { + this._lastChangePartOfInlineEdit = false; + + const offsetEdit = this._inlineEdit.get(); + if (!offsetEdit) { + return; + } + + const editUpdates = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes)); + const newEdits = editUpdates.filter(({ changeType }) => changeType !== 'fullyAccepted').map(({ edit }) => edit); + + const emptyEdit = newEdits.find(edit => edit.isEmpty); + if (emptyEdit || newEdits.length === 0) { + // Either a change collided with one of our edits, so we will have to drop the completion + // Or the completion has been typed by the user + this._inlineEdit.set(new OffsetEdit([emptyEdit ?? new SingleOffsetEdit(new OffsetRange(0, 0), '')]), tx); + return; + } + + const changePartiallyAcceptsEdit = editUpdates.some(({ changeType }) => changeType === 'partiallyAccepted' || changeType === 'fullyAccepted'); + + if (changePartiallyAcceptsEdit) { + this._invalidationTime = undefined; + } + if (this._invalidationTime && this._invalidationTime < Date.now()) { + // The completion has been shown for a while and the user + // has been working on a different part of the document, so invalidate it + this._inlineEdit.set(new OffsetEdit([new SingleOffsetEdit(new OffsetRange(0, 0), '')]), tx); + return; + } + + this._lastChangePartOfInlineEdit = changePartiallyAcceptsEdit; + this._inlineEdit.set(new OffsetEdit(newEdits), tx); + + function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): { edit: SingleOffsetEdit; changeType: 'move' | 'partiallyAccepted' | 'fullyAccepted' } { + let start = edit.replaceRange.start; + let end = edit.replaceRange.endExclusive; + let newText = edit.newText; + let changeType: 'move' | 'partiallyAccepted' | 'fullyAccepted' = 'move'; + for (let i = changes.length - 1; i >= 0; i--) { + const change = changes[i]; + + // Edit is an insertion: user inserted text at the start of the completion + if (edit.replaceRange.isEmpty && change.rangeLength === 0 && change.rangeOffset === start && newText.startsWith(change.text)) { + start += change.text.length; + end = Math.max(start, end); + newText = newText.substring(change.text.length); + changeType = newText.length === 0 ? 'fullyAccepted' : 'partiallyAccepted'; + continue; + } + + // Edit is a deletion: user deleted text inside the deletion range + if (!edit.replaceRange.isEmpty && change.text.length === 0 && change.rangeOffset >= start && change.rangeOffset + change.rangeLength <= end) { + end -= change.rangeLength; + changeType = start === end ? 'fullyAccepted' : 'partiallyAccepted'; + continue; + } + + if (change.rangeOffset > end) { + // the change happens after the completion range + continue; + } + if (change.rangeOffset + change.rangeLength < start) { + // the change happens before the completion range + start += change.text.length - change.rangeLength; + end += change.text.length - change.rangeLength; + continue; + } + + // The change intersects the completion, so we will have to drop the completion + start = change.rangeOffset; + end = change.rangeOffset; + newText = ''; + } + return { edit: new SingleOffsetEdit(new OffsetRange(start, end), newText), changeType }; + } } public toInlineCompletion(reader: IReader | undefined): InlineCompletionItem { - return this.inlineCompletion.withRange(this._updatedRange.read(reader) ?? emptyRange); + const singleTextEdit = this.toSingleTextEdit(reader); + return this.inlineCompletion.withRangeInsertTextAndFilterText(singleTextEdit.range, singleTextEdit.text, singleTextEdit.text); } public toSingleTextEdit(reader: IReader | undefined): SingleTextEdit { - return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.insertText); + this._modelVersion.read(reader); + const offsetEdit = this._inlineEdit.read(reader); + if (!offsetEdit) { + return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.insertText); + } + + const startOffset = offsetEdit.edits[0].replaceRange.start; + const endOffset = offsetEdit.edits[offsetEdit.edits.length - 1].replaceRange.endExclusive; + const overallOffsetRange = new OffsetRange(startOffset, endOffset); + const overallLnColRange = Range.fromPositions( + this._textModel.getPositionAt(overallOffsetRange.start), + this._textModel.getPositionAt(overallOffsetRange.endExclusive) + ); + let text = this._textModel.getValueInRange(overallLnColRange); + for (let i = offsetEdit.edits.length - 1; i >= 0; i--) { + const edit = offsetEdit.edits[i]; + const relativeStartOffset = edit.replaceRange.start - startOffset; + const relativeEndOffset = edit.replaceRange.endExclusive - startOffset; + text = text.substring(0, relativeStartOffset) + edit.newText + text.substring(relativeEndOffset); + } + return new SingleTextEdit(overallLnColRange, text); } public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { @@ -373,6 +544,13 @@ export class InlineCompletionWithUpdatedRange { } public canBeReused(model: ITextModel, position: Position): boolean { + const inlineEdit = this._inlineEdit.get(); + if (inlineEdit !== null) { + return model === this._textModel + && !inlineEdit.isEmpty + && this._lastChangePartOfInlineEdit; + } + const updatedRange = this._updatedRange.read(undefined); const result = !!updatedRange && updatedRange.containsPosition(position) @@ -382,7 +560,8 @@ export class InlineCompletionWithUpdatedRange { } private _toFilterTextReplacement(reader: IReader | undefined): SingleTextEdit { - return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.filterText); + const inlineCompletion = this.toInlineCompletion(reader); + return new SingleTextEdit(inlineCompletion.range, inlineCompletion.filterText); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts index cf034658647..2511825052e 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts @@ -79,6 +79,7 @@ export class InlineEditsAdapter extends Disposable { }; }), commands: definedEdits.flatMap(e => e.result.commands ?? []), + enableForwardStability: true, }; }, handleRejection: (completions: InlineCompletions, item: InlineCompletionsAndEdits['items'][number]): void => { diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index d6b357f0b14..d964f81ac36 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -337,6 +337,8 @@ export class InlineCompletionItem { ); } + static ID = 1; + private _didCallShow = false; constructor( @@ -362,7 +364,10 @@ export class InlineCompletionItem { * Used for event data to ensure referential equality. */ readonly source: InlineCompletionList, + + readonly id = `InlineCompletion:${InlineCompletionItem.ID++}`, ) { + // TODO: these statements are no-ops filterText = filterText.replace(/\r\n|\r/g, '\n'); insertText = filterText.replace(/\r\n|\r/g, '\n'); } @@ -386,6 +391,23 @@ export class InlineCompletionItem { this.additionalTextEdits, this.sourceInlineCompletion, this.source, + this.id, + ); + } + + public withRangeInsertTextAndFilterText(updatedRange: Range, updatedInsertText: string, updatedFilterText: string): InlineCompletionItem { + return new InlineCompletionItem( + updatedFilterText, + this.command, + this.shownCommand, + updatedRange, + updatedInsertText, + this.snippetInfo, + this.cursorShowRange, + this.additionalTextEdits, + this.sourceInlineCompletion, + this.source, + this.id, ); } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 0f37508a9a2..4541e90d9fa 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -53,11 +53,15 @@ export function substringPos(text: string, pos: Position): string { return text.substring(offset); } -export function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { +export function getModifiedRangesAfterApplying(edits: readonly SingleTextEdit[]): Range[] { const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); const edit = new TextEdit(sortPerm.apply(edits)); const sortedNewRanges = edit.getNewRanges(); - const newRanges = sortPerm.inverse().apply(sortedNewRanges); + return sortPerm.inverse().apply(sortedNewRanges); +} + +export function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { + const newRanges = getModifiedRangesAfterApplying(edits); return newRanges.map(range => range.getEndPosition()); } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 01df0592939..47644496fea 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -9,7 +9,7 @@ import { Disposable, toDisposable } from '../../../../../../base/common/lifecycl import { IObservable, autorun, derived, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import * as strings from '../../../../../../base/common/strings.js'; import { applyFontInfo } from '../../../../../browser/config/domFontInfo.js'; -import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { ICodeEditor, IViewZoneChangeAccessor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from '../../../../../common/config/editorOptions.js'; import { OffsetEdit, SingleOffsetEdit } from '../../../../../common/core/offsetEdit.js'; @@ -39,6 +39,10 @@ export class GhostTextView extends Disposable { constructor( private readonly _editor: ICodeEditor, private readonly _model: IGhostTextWidgetModel, + private readonly _options: IObservable<{ + extraClasses?: string[]; + syntaxHighlightingEnabled: boolean; + }>, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); @@ -47,7 +51,16 @@ export class GhostTextView extends Disposable { this._register(this._editorObs.setDecorations(this.decorations)); } - private readonly _useSyntaxHighlighting = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.syntaxHighlightingEnabled); + private readonly _useSyntaxHighlighting = this._options.map(o => o.syntaxHighlightingEnabled); + + private readonly _extraClassNames = derived(this, reader => { + const extraClasses = [...this._options.read(reader).extraClasses ?? []]; + if (this._useSyntaxHighlighting.read(reader)) { + extraClasses.push('syntax-highlighted'); + } + const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); + return extraClassNames; + }); private readonly uiState = derived(this, reader => { if (this._isDisposed.read(reader)) { return undefined; } @@ -59,8 +72,8 @@ export class GhostTextView extends Disposable { const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined; const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); - const extraClassName = syntaxHighlightingEnabled ? ' syntax-highlighted' : ''; - const { inlineTexts, additionalLines, hiddenRange } = computeGhostTextViewData(ghostText, textModel, 'ghost-text' + extraClassName); + const extraClassNames = this._extraClassNames.read(reader); + const { inlineTexts, additionalLines, hiddenRange } = computeGhostTextViewData(ghostText, textModel, 'ghost-text' + extraClassNames); const currentLine = textModel.getLineContent(ghostText.lineNumber); const edit = new OffsetEdit(inlineTexts.map(t => SingleOffsetEdit.insert(t.column - 1, t.text))); @@ -91,12 +104,12 @@ export class GhostTextView extends Disposable { const decorations: IModelDeltaDecoration[] = []; - const extraClassName = uiState.syntaxHighlightingEnabled ? ' syntax-highlighted' : ''; + const extraClassNames = this._extraClassNames.read(reader); if (uiState.replacedRange) { decorations.push({ range: uiState.replacedRange.toRange(uiState.lineNumber), - options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassName, description: 'GhostTextReplacement' } + options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassNames, description: 'GhostTextReplacement' } }); } @@ -115,7 +128,7 @@ export class GhostTextView extends Disposable { after: { content: p.text, tokens: p.tokens, - inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' + extraClassName, + inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' + extraClassNames, cursorStops: InjectedTextCursorStops.Left }, showIfCollapsed: true, @@ -142,6 +155,11 @@ export class GhostTextView extends Disposable { ) ); + public readonly height = derived(this, reader => { + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + return lineHeight + (this.additionalLinesWidget.viewZoneHeight.read(reader) ?? 0); + }); + public ownsViewZone(viewZoneId: string): boolean { return this.additionalLinesWidget.viewZoneId === viewZoneId; } @@ -215,8 +233,11 @@ function computeGhostTextViewData(ghostText: GhostText | GhostTextReplacement, t } export class AdditionalLinesWidget extends Disposable { - private _viewZoneId: string | undefined = undefined; - public get viewZoneId(): string | undefined { return this._viewZoneId; } + private _viewZoneInfo: { viewZoneId: string; heightInLines: number; lineNumber: number } | undefined; + public get viewZoneId(): string | undefined { return this._viewZoneInfo?.viewZoneId; } + + private _viewZoneHeight = observableValue('viewZoneHeight', undefined); + public get viewZoneHeight(): IObservable { return this._viewZoneHeight; } private readonly editorOptionsChanged = observableSignalFromEvent('editorOptionChanged', Event.filter( this.editor.onDidChangeConfiguration, @@ -260,10 +281,7 @@ export class AdditionalLinesWidget extends Disposable { private clear(): void { this.editor.changeViewZones((changeAccessor) => { - if (this._viewZoneId) { - changeAccessor.removeZone(this._viewZoneId); - this._viewZoneId = undefined; - } + this.removeActiveViewZone(changeAccessor); }); } @@ -276,24 +294,50 @@ export class AdditionalLinesWidget extends Disposable { const { tabSize } = textModel.getOptions(); this.editor.changeViewZones((changeAccessor) => { - if (this._viewZoneId) { - changeAccessor.removeZone(this._viewZoneId); - this._viewZoneId = undefined; - } + this.removeActiveViewZone(changeAccessor); const heightInLines = Math.max(additionalLines.length, minReservedLineCount); if (heightInLines > 0) { const domNode = document.createElement('div'); renderLines(domNode, tabSize, additionalLines, this.editor.getOptions()); - this._viewZoneId = changeAccessor.addZone({ - afterLineNumber: lineNumber, - heightInLines: heightInLines, - domNode, - afterColumnAffinity: PositionAffinity.Right - }); + this.addViewZone(changeAccessor, lineNumber, heightInLines, domNode); + } + }); + } + + private addViewZone(changeAccessor: IViewZoneChangeAccessor, afterLineNumber: number, heightInLines: number, domNode: HTMLElement): void { + const id = changeAccessor.addZone({ + afterLineNumber: afterLineNumber, + heightInLines: heightInLines, + domNode, + afterColumnAffinity: PositionAffinity.Right, + onComputedHeight: (height: number) => { + this._viewZoneHeight.set(height, undefined); // TODO: can a transaction be used to avoid flickering? } }); + + this.keepCursorStable(afterLineNumber, heightInLines); + + this._viewZoneInfo = { viewZoneId: id, heightInLines, lineNumber: afterLineNumber }; + } + + private removeActiveViewZone(changeAccessor: IViewZoneChangeAccessor): void { + if (this._viewZoneInfo) { + changeAccessor.removeZone(this._viewZoneInfo.viewZoneId); + + this.keepCursorStable(this._viewZoneInfo.lineNumber, -this._viewZoneInfo.heightInLines); + + this._viewZoneInfo = undefined; + this._viewZoneHeight.set(undefined, undefined); + } + } + + private keepCursorStable(lineNumber: number, heightInLines: number): void { + const cursorLineNumber = this.editor.getSelection()?.getStartPosition()?.lineNumber; + if (cursorLineNumber !== undefined && lineNumber < cursorLineNumber) { + this.editor.setScrollTop(this.editor.getScrollTop() + heightInLines * this.editor.getOption(EditorOption.lineHeight)); + } } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index fa303f3fc6e..cbbbb35734e 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -6,7 +6,7 @@ import { createStyleSheetFromObservable } from '../../../../../base/browser/domObservable.js'; import { readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { derived, mapObservableArrayCached, derivedDisposable, constObservable, derivedObservableWithCache, IObservable } from '../../../../../base/common/observable.js'; +import { derived, mapObservableArrayCached, derivedDisposable, constObservable, derivedObservableWithCache, IObservable, ISettableObservable } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; @@ -23,12 +23,14 @@ export class InlineCompletionsView extends Disposable { return model?.ghostTexts.read(reader) ?? []; }); private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); + private readonly _editorObs = observableCodeEditor(this._editor); private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance(readHotReloadableExport(GhostTextView, reader), this._editor, { ghostText: ghostText, minReservedLineCount: constObservable(0), targetTextModel: this._model.map(v => v?.textModel), - }) + }, + this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled }))) ).recomputeInitiallyAndOnChange(store) ).recomputeInitiallyAndOnChange(this._store); @@ -38,16 +40,16 @@ export class InlineCompletionsView extends Disposable { if (!this._everHadInlineEdit.read(reader)) { return undefined; } - return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model); + return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model, this._focusIsInMenu); }) .recomputeInitiallyAndOnChange(this._store); - private readonly _editorObs = observableCodeEditor(this._editor); private readonly _fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); constructor( private readonly _editor: ICodeEditor, private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts new file mode 100644 index 00000000000..7b17b5e757f --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../base/common/lifecycle.js'; +import { IObservable, constObservable, derived, derivedObservableWithCache } from '../../../../../../base/common/observable.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { Point } from '../../../../../browser/point.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { Position } from '../../../../../common/core/position.js'; +import { Range } from '../../../../../common/core/range.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; +import { createRectangle, getPrefixTrim, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; +import { InlineEditWithChanges } from './viewAndDiffProducer.js'; + +export class InlineEditsDeletionView extends Disposable implements IInlineEditsView { + private readonly _editorObs = observableCodeEditor(this._editor); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + private readonly _uiState: IObservable<{ + originalRange: LineRange; + deletions: Range[]; + } | undefined>, + ) { + super(); + + this._register(this._editorObs.createOverlayWidget({ + domNode: this._nonOverflowView.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const info = this._editorLayoutInfo.read(reader); + if (info === null) { return 0; } + return info.code1.x - info.codeStart1.x; + }), + })); + } + + private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); + + private readonly _originalStartPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; + }); + + private readonly _originalEndPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.endLineNumberExclusive, 1) : null; + }); + + private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); + private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); + + private readonly _originalDisplayRange = this._uiState.map(s => s?.originalRange); + private readonly _editorMaxContentWidthInRange = derived(this, reader => { + const originalDisplayRange = this._originalDisplayRange.read(reader); + if (!originalDisplayRange) { + return constObservable(0); + } + this._editorObs.versionId.read(reader); + + // Take the max value that we observed. + // Reset when either the edit changes or the editor text version. + return derivedObservableWithCache(this, (reader, lastValue) => { + const maxWidth = maxContentWidthInRange(this._editorObs, originalDisplayRange, reader); + return Math.max(maxWidth, lastValue ?? 0); + }); + }).map((v, r) => v.read(r)); + + private readonly _maxPrefixTrim = derived(reader => { + const state = this._uiState.read(reader); + if (!state) { + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } + return getPrefixTrim(state.deletions, state.originalRange, [], this._editor); + }); + + private readonly _editorLayoutInfo = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { + return null; + } + const state = this._uiState.read(reader); + if (!state) { + return null; + } + + const editorLayout = this._editorObs.layoutInfo.read(reader); + const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); + + const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; + + const range = inlineEdit.originalLineRange; + const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + + const codeLeft = editorLayout.contentLeft + this._maxPrefixTrim.read(reader).prefixLeftOffset; + + if (left <= codeLeft) { + return null; + } + + const code1 = new Point(left, selectionTop); + const codeStart1 = new Point(codeLeft, selectionTop); + const code2 = new Point(left, selectionBottom); + const codeStart2 = new Point(codeLeft, selectionBottom); + const codeHeight = selectionBottom - selectionTop; + + return { + code1, + codeStart1, + code2, + codeStart2, + codeHeight, + horizontalScrollOffset, + padding: 3, + borderRadius: 4, + }; + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _foregroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, derived(reader => { + const layoutInfoObs = mapOutFalsy(this._editorLayoutInfo).read(reader); + if (!layoutInfoObs) { return undefined; } + + const layoutInfo = layoutInfoObs.read(reader); + + // TODO: look into why 1px offset is needed + const rectangleOverlay = createRectangle( + { + topLeft: layoutInfo.codeStart1, + width: layoutInfo.code1.x - layoutInfo.codeStart1.x + 1, + height: layoutInfo.code2.y - layoutInfo.code1.y + 1, + }, + layoutInfo.padding, + layoutInfo.borderRadius, + { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } + ); + + return [ + n.svgElem('path', { + class: 'originalOverlay', + d: rectangleOverlay, + style: { + fill: 'var(--vscode-inlineEdit-originalBackground, transparent)', + stroke: 'var(--vscode-inlineEdit-originalBorder)', + strokeWidth: '1px', + } + }), + ]; + })).keepUpdated(this._store); + + private readonly _nonOverflowView = n.div({ + class: 'inline-edits-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + zIndex: '0', + display: this._display, + }, + }, [ + [this._foregroundSvg], + ]).keepUpdated(this._store); + + readonly isHovered = constObservable(false); +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts index 95168505a40..089c058b6d4 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts @@ -20,7 +20,8 @@ import { ChildNode, FirstFnArg, LiveElement, n } from './utils.js'; export class GutterIndicatorMenuContent { constructor( - private readonly _selectionOverride: IObservable<'jump' | 'accept' | undefined>, + private readonly _menuTitle: IObservable, + private readonly _tabAction: IObservable<'jump' | 'accept' | 'inactive'>, private readonly _close: (focusEditor: boolean) => void, private readonly _extensionCommands: IObservable, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -35,28 +36,25 @@ export class GutterIndicatorMenuContent { private _createHoverContent() { const activeElement = observableValue('active', undefined); - const activeElementOrDefault = derived(reader => this._selectionOverride.read(reader) ?? activeElement.read(reader)); - const createOptionArgs = (options: { id: string; title: string; icon: ThemeIcon; commandId: string; commandArgs?: unknown[] }): FirstFnArg => { + const createOptionArgs = (options: { id: string; title: string; icon: IObservable | ThemeIcon; commandId: string | IObservable; commandArgs?: unknown[] }): FirstFnArg => { return { title: options.title, icon: options.icon, - keybinding: this._getKeybinding(options.commandArgs ? undefined : options.commandId), - isActive: activeElementOrDefault.map(v => v === options.id), + keybinding: typeof options.commandId === 'string' ? this._getKeybinding(options.commandArgs ? undefined : options.commandId) : derived(reader => typeof options.commandId === 'string' ? undefined : this._getKeybinding(options.commandArgs ? undefined : options.commandId.read(reader)).read(reader)), + isActive: activeElement.map(v => v === options.id), onHoverChange: v => activeElement.set(v ? options.id : undefined, undefined), onAction: () => { this._close(true); - return this._commandService.executeCommand(options.commandId, ...(options.commandArgs ?? [])); + return this._commandService.executeCommand(typeof options.commandId === 'string' ? options.commandId : options.commandId.get(), ...(options.commandArgs ?? [])); }, }; }; // TODO make this menu contributable! return hoverContent([ - // TODO: make header dynamic, get from extension - header(localize('inlineEdit', "Inline Edit")), - option(createOptionArgs({ id: 'jump', title: localize('jump', "Jump"), icon: Codicon.arrowRight, commandId: new JumpToNextInlineEdit().id })), - option(createOptionArgs({ id: 'accept', title: localize('accept', "Accept"), icon: Codicon.check, commandId: new AcceptInlineCompletion().id })), + header(this._menuTitle), + option(createOptionArgs({ id: 'gotoAndAccept', title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`, icon: this._tabAction.map(action => action === 'accept' ? Codicon.check : Codicon.arrowRight), commandId: this._tabAction.map(action => action === 'accept' ? new AcceptInlineCompletion().id : new JumpToNextInlineEdit().id) })), option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: new HideInlineCompletion().id })), separator(), this._extensionCommands?.map(c => c && c.length > 0 ? [ @@ -71,7 +69,7 @@ export class GutterIndicatorMenuContent { if (!commandId) { return constObservable(undefined); } - return observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._keybindingService.lookupKeybinding(commandId, this._contextKeyService, true)); + return observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._keybindingService.lookupKeybinding(commandId)); // TODO: use contextkeyservice to use different renderings } } @@ -85,7 +83,7 @@ function hoverContent(content: ChildNode) { }, content); } -function header(title: string) { +function header(title: string | IObservable) { return n.div({ class: 'header', style: { @@ -100,7 +98,7 @@ function header(title: string) { function option(props: { title: string; - icon: ThemeIcon; + icon: IObservable | ThemeIcon; keybinding: IObservable; isActive?: IObservable; onHoverChange?: (isHovered: boolean) => void; @@ -123,7 +121,7 @@ function option(props: { fontSize: 16, display: 'flex', } - }, [renderIcon(props.icon)]), + }, [ThemeIcon.isThemeIcon(props.icon) ? renderIcon(props.icon) : props.icon.map(icon => renderIcon(icon))]), n.elem('span', {}, [props.title]), n.div({ style: { marginLeft: 'auto', opacity: '0.6' }, diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index e0a1ce126d0..5091bc0cc27 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -5,8 +5,8 @@ import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, constObservable, derived, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, ISettableObservable, autorun, constObservable, derived, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; @@ -23,6 +23,8 @@ import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; +import { trackFocus } from '../../../../../../base/browser/dom.js'; +import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; export const inlineEditIndicatorPrimaryForeground = registerColor( 'inlineEdit.gutterIndicator.primaryForeground', buttonForeground, @@ -72,9 +74,11 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, private readonly _model: IObservable, - private readonly _shouldShowHover: IObservable, + private readonly _isHoveringOverInlineEdit: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IHoverService private readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, ) { super(); @@ -86,10 +90,8 @@ export class InlineEditsGutterIndicator extends Disposable { })); this._register(autorun(reader => { - if (this._shouldShowHover.read(reader)) { - this._showHover(); - } else { - this._hoverService.hideHover(); + if (!accessibilityService.isMotionReduced()) { + this._indicator.element.classList.toggle('wiggle', this._isHoveringOverInlineEdit.read(reader)); } })); } @@ -116,14 +118,15 @@ export class InlineEditsGutterIndicator extends Disposable { const layout = this._editorObs.layoutInfo.read(reader); - const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height); + const bottomPadding = 1; + const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height - bottomPadding); const viewPortWithStickyScroll = fullViewPort.withTop(this._stickyScrollHeight.read(reader)); const targetVertRange = s.lineOffsetRange.read(reader); const space = 1; - const targetRect = Rect.fromRanges(OffsetRange.fromTo(space, layout.lineNumbersLeft + layout.lineNumbersWidth + 4), targetVertRange); + const targetRect = Rect.fromRanges(OffsetRange.fromTo(space + layout.glyphMarginLeft, layout.lineNumbersLeft + layout.lineNumbersWidth + 4), targetVertRange); const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); @@ -154,59 +157,72 @@ export class InlineEditsGutterIndicator extends Disposable { return 'inactive' as const; }); - private readonly _onClickAction = derived(this, reader => { - if (this._layout.map(d => d && d.docked).read(reader)) { - return { - selectionOverride: 'accept' as const, - action: () => { this._model.get()?.accept(); } - }; - } else { - return { - selectionOverride: 'jump' as const, - action: () => { this._model.get()?.jump(); } - }; - } - }); - private readonly _iconRef = n.ref(); private _hoverVisible: boolean = false; private readonly _isHoveredOverIcon = observableValue(this, false); - private readonly _hoverSelectionOverride = derived(this, reader => this._isHoveredOverIcon.read(reader) ? this._onClickAction.read(reader).selectionOverride : undefined); private _showHover(): void { if (this._hoverVisible) { return; } - const content = this._instantiationService.createInstance( + const displayName = derived(this, reader => { + const state = this._model.read(reader)?.inlineEditState; + const item = state?.read(reader); + const completionSource = item?.inlineCompletion?.source; + // TODO: expose the provider (typed) and expose the provider the edit belongs totyping and get correct edit + const displayName = (completionSource?.inlineCompletions as any).edits[0]?.provider?.displayName ?? localize('inlineEdit', "Inline Edit"); + return displayName; + }); + + const disposableStore = new DisposableStore(); + const content = disposableStore.add(this._instantiationService.createInstance( GutterIndicatorMenuContent, - this._hoverSelectionOverride, + displayName, + this._tabAction, (focusEditor) => { - h?.dispose(); if (focusEditor) { this._editorObs.editor.focus(); } + h?.dispose(); }, - this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.inlineCompletion.source.inlineCompletions.commands), - ).toDisposableLiveElement(); + this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.source.inlineCompletions.commands), + ).toDisposableLiveElement()); + + const focusTracker = disposableStore.add(trackFocus(content.element)); + disposableStore.add(focusTracker.onDidBlur(() => this._focusIsInMenu.set(false, undefined))); + disposableStore.add(focusTracker.onDidFocus(() => this._focusIsInMenu.set(true, undefined))); + disposableStore.add(toDisposable(() => this._focusIsInMenu.set(false, undefined))); + const h = this._hoverService.showHover({ target: this._iconRef.element, content: content.element, }) as HoverWidget | undefined; if (h) { this._hoverVisible = true; - h.onDispose(() => { - content.dispose(); + h.onDispose(() => { // TODO:@hediet fix leak + disposableStore.dispose(); this._hoverVisible = false; }); } else { - content.dispose(); + disposableStore.dispose(); } } private readonly _indicator = n.div({ class: 'inline-edits-view-gutter-indicator', - onclick: () => this._onClickAction.get().action(), + onclick: () => { + const model = this._model.get(); + if (!model) { return; } + const docked = this._layout.map(l => l && l.docked).get(); + this._editorObs.editor.focus(); + if (docked) { + model.accept(); + } else { + model.jump(); + } + }, + tabIndex: 0, style: { position: 'absolute', overflow: 'visible', @@ -266,7 +282,7 @@ export class InlineEditsGutterIndicator extends Disposable { transition: 'rotate 0.2s ease-in-out', } }, [ - renderIcon(Codicon.arrowRight) + renderIcon(Codicon.arrowRight) // TODO: allow setting css here, is this already supported? ]) ]), ])).keepUpdated(this._store); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 38343311a3e..aa4a49831e7 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, IObservable, observableFromEvent } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, constObservable, derived, IObservable, observableFromEvent } from '../../../../../../base/common/observable.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { rangeIsSingleLine } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.js'; @@ -15,24 +15,27 @@ import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { Range } from '../../../../../common/core/range.js'; import { AbstractText } from '../../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping } from '../../../../../common/diff/rangeMapping.js'; -import { IModelDeltaDecoration, ITextModel } from '../../../../../common/model.js'; +import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../../common/model.js'; import { ModelDecorationOptions } from '../../../../../common/model/textModel.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; import { classNames } from './utils.js'; export interface IOriginalEditorInlineDiffViewState { diff: DetailedLineRangeMapping[]; modifiedText: AbstractText; - mode: 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide'; + mode: 'mixedLines' | 'insertionInline' | 'interleavedLines' | 'sideBySide' | 'deletion'; modifiedCodeEditor: ICodeEditor; } -export class OriginalEditorInlineDiffView extends Disposable { +export class OriginalEditorInlineDiffView extends Disposable implements IInlineEditsView { public static supportsInlineDiffRendering(mapping: DetailedLineRangeMapping): boolean { return allowsTrueInlineDiffRendering(mapping); } + readonly isHovered = constObservable(false); + constructor( private readonly _originalEditor: ICodeEditor, private readonly _state: IObservable, @@ -111,7 +114,7 @@ export class OriginalEditorInlineDiffView extends Disposable { if (!diff) { return undefined; } const modified = diff.modifiedText; - const showInline = diff.mode === 'mixedLines' || diff.mode === 'ghostText'; + const showInline = diff.mode === 'mixedLines' || diff.mode === 'insertionInline'; const showEmptyDecorations = true; @@ -156,7 +159,7 @@ export class OriginalEditorInlineDiffView extends Disposable { }); for (const m of diff.diff) { - const showFullLineDecorations = true; + const showFullLineDecorations = diff.mode !== 'sideBySide'; if (showFullLineDecorations) { if (!m.original.isEmpty) { originalDecorations.push({ @@ -184,6 +187,7 @@ export class OriginalEditorInlineDiffView extends Disposable { for (const i of m.innerChanges || []) { // Don't show empty markers outside the line range if (m.original.contains(i.originalRange.startLineNumber)) { + const replacedText = this._originalEditor.getModel()?.getValueInRange(i.originalRange, EndOfLinePreference.LF); originalDecorations.push({ range: i.originalRange, options: { @@ -191,9 +195,11 @@ export class OriginalEditorInlineDiffView extends Disposable { shouldFillLineOnLineBreak: false, className: classNames( 'inlineCompletions-char-delete', - (i.originalRange.isEmpty() && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' + i.originalRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', + i.originalRange.isEmpty() && 'empty', + ((i.originalRange.isEmpty() || diff.mode === 'deletion' && replacedText === '\n') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' ), - inlineClassName: useInlineDiff ? 'strike-through' : null, + inlineClassName: useInlineDiff ? classNames('strike-through', 'inlineCompletions') : null, zIndex: 1 } }); @@ -214,7 +220,10 @@ export class OriginalEditorInlineDiffView extends Disposable { description: 'inserted-text', before: { content: insertedText, - inlineClassName: diff.mode === 'ghostText' ? 'ghost-text-decoration' : 'inlineCompletions-char-insert', + inlineClassName: classNames( + 'inlineCompletions-char-insert', + i.modifiedRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline' + ), }, zIndex: 2, showIfCollapsed: true, diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts new file mode 100644 index 00000000000..c6677c0c08f --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { $ } from '../../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, constObservable, derived, derivedWithStore, observableValue } from '../../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { Point } from '../../../../../browser/point.js'; +import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; +import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { Position } from '../../../../../common/core/position.js'; +import { Range } from '../../../../../common/core/range.js'; +import { ILanguageService } from '../../../../../common/languages/language.js'; +import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; +import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; +import { GhostText, GhostTextPart } from '../../model/ghostText.js'; +import { GhostTextView } from '../ghostText/ghostTextView.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; +import { createRectangle, mapOutFalsy, n } from './utils.js'; + +export class InlineEditsInsertionView extends Disposable implements IInlineEditsView { + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _state = derived(this, reader => { + const state = this._input.read(reader); + if (!state) { return undefined; } + + const textModel = this._editor.getModel()!; + + if (state.startColumn === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith('\n') && !state.text.startsWith('\n')) { + const endOfLineColumn = textModel.getLineLength(state.lineNumber - 1) + 1; + return { lineNumber: state.lineNumber - 1, column: endOfLineColumn, text: '\n' + state.text.slice(0, -1) }; + } + + return { lineNumber: state.lineNumber, column: state.startColumn, text: state.text }; + }); + + private readonly _ghostText = derived(reader => { + const state = this._state.read(reader); + if (!state) { return undefined; } + return new GhostText(state.lineNumber, [new GhostTextPart(state.column, state.text, false)]); + }); + + protected readonly _ghostTextView = this._register(this._instantiationService.createInstance(GhostTextView, + this._editor, + { + ghostText: this._ghostText, + minReservedLineCount: constObservable(0), + targetTextModel: this._editorObs.model.map(model => model ?? undefined), + }, + observableValue(this, { syntaxHighlightingEnabled: true, extraClasses: ['inline-edit'] }), + )); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _input: IObservable<{ + lineNumber: number; + startColumn: number; + text: string; + } | undefined>, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(this._editorObs.createOverlayWidget({ + domNode: this._nonOverflowView.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const info = this._overlayLayout.read(reader); + if (info === null) { return 0; } + return info.code1.x - info.codeStart1.x; + }), + })); + } + + private readonly _display = derived(this, reader => !!this._state.read(reader) ? 'block' : 'none'); + + private readonly _editorMaxContentWidthInRange = derived(this, reader => { + const state = this._state.read(reader); + if (!state) { + return 0; + } + this._editorObs.versionId.read(reader); + const textModel = this._editor.getModel()!; + + const cleanText = state.text.replace('\r\n', '\n'); + const textBeforeInsertion = cleanText.startsWith('\n') ? '' : textModel.getValueInRange(new Range(state.lineNumber, 1, state.lineNumber, state.column)); + const textAfterInsertion = textModel.getValueInRange(new Range(state.lineNumber, state.column, state.lineNumber, textModel.getLineLength(state.lineNumber) + 1)); + const text = textBeforeInsertion + cleanText + textAfterInsertion; + const lines = text.split('\n'); + + const renderOptions = RenderOptions.fromEditor(this._editor).withSetWidth(false); + const lineWidths = lines.map(line => { + const t = textModel.tokenization.tokenizeLinesAt(state.lineNumber, [line])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).toLineTokens(line, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(line, this._languageService.languageIdCodec); + } + + return renderLines(new LineSource([tokens]), renderOptions, [], $('div'), true).minWidthInPx - 20; // TODO: always too much padding included, why? + }); + + // Take the max value that we observed. + // Reset when either the edit changes or the editor text version. + return Math.max(...lineWidths); + }); + + private readonly _overlayLayout = derivedWithStore(this, (reader, store) => { + this._ghostText.read(reader); + const state = this._state.read(reader); + if (!state) { + return null; + } + + // Update the overlay when the position changes + this._editorObs.observePosition(observableValue(this, new Position(state.lineNumber, state.column)), store).read(reader); + + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const scrollTop = this._editorObs.scrollTop.read(reader); + const editorLayout = this._editorObs.layoutInfo.read(reader); + const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); + + const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; + + let height = this._ghostTextView.height.read(reader); + let top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; + if (state.text.startsWith('\n')) { + height -= lineHeight; + top += lineHeight; + } + + const codeLeft = editorLayout.contentLeft; + const bottom = top + height; + + if (left <= codeLeft) { + return null; + } + + const code1 = new Point(left, top); + const codeStart1 = new Point(codeLeft, top); + const code2 = new Point(left, bottom); + const codeStart2 = new Point(codeLeft, bottom); + + return { + code1, + codeStart1, + code2, + codeStart2, + horizontalScrollOffset, + padding: 2, + borderRadius: 4, + }; + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _foregroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, derived(reader => { + const overlayLayoutObs = mapOutFalsy(this._overlayLayout).read(reader); + if (!overlayLayoutObs) { return undefined; } + + const layoutInfo = overlayLayoutObs.read(reader); + + const rectangleOverlay = createRectangle( + { + topLeft: layoutInfo.codeStart1, + width: layoutInfo.code1.x - layoutInfo.codeStart1.x, + height: layoutInfo.code2.y - layoutInfo.code1.y, + }, + layoutInfo.padding, + layoutInfo.borderRadius, + { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } + ); + + return [ + n.svgElem('path', { + class: 'originalOverlay', + d: rectangleOverlay, + style: { + fill: 'var(--vscode-inlineEdit-modifiedChangedLineBackground, transparent)', + stroke: 'var(--vscode-inlineEdit-modifiedBorder)', + strokeWidth: '1px', + } + }), + ]; + })).keepUpdated(this._store); + + private readonly _nonOverflowView = n.div({ + class: 'inline-edits-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + zIndex: '0', + display: this._display, + }, + }, [ + [this._foregroundSvg], + ]).keepUpdated(this._store); + + readonly isHovered = constObservable(false); +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index e1feaa2eb8b..afd6599ebb9 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -2,25 +2,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getWindow } from '../../../../../../base/browser/dom.js'; +import { $, getWindow } from '../../../../../../base/browser/dom.js'; import { ActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { Color } from '../../../../../../base/common/color.js'; import { structuralEquals } from '../../../../../../base/common/equals.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { IObservable, IReader, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent } from '../../../../../../base/common/observable.js'; import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { diffInserted, diffRemoved } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { darken, lighten, registerColor } from '../../../../../../platform/theme/common/colorUtils.js'; +import { diffInserted, diffInsertedLine, diffRemoved, editorHoverBorder } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; import { EmbeddedCodeEditorWidget } from '../../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; -import { editorLineHighlightBorder } from '../../../../../common/core/editorColorRegistry.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { Position } from '../../../../../common/core/position.js'; @@ -30,7 +29,7 @@ import { ITextModel } from '../../../../../common/model.js'; import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionContextKeys } from '../../controller/inlineCompletionContextKeys.js'; import { CustomizedMenuWorkbenchToolBar } from '../../hintsWidget/inlineCompletionsHintsWidget.js'; -import { PathBuilder, StatusBarViewItem, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; +import { PathBuilder, StatusBarViewItem, createRectangle, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; import { localize } from '../../../../../../nls.js'; @@ -63,7 +62,12 @@ export const originalChangedTextOverlayColor = registerColor( export const modifiedChangedLineBackgroundColor = registerColor( 'inlineEdit.modifiedChangedLineBackground', - Color.transparent, + { + light: transparent(diffInsertedLine, 0.5), + dark: transparent(diffInsertedLine, 0.5), + hcDark: diffInsertedLine, + hcLight: diffInsertedLine + }, localize('inlineEdit.modifiedChangedLineBackground', 'Background color for the changed lines in the modified text of inline edits.'), true ); @@ -77,10 +81,10 @@ export const modifiedChangedTextOverlayColor = registerColor( export const originalBorder = registerColor( 'inlineEdit.originalBorder', { - light: darken(editorLineHighlightBorder, 0.15), - dark: lighten(editorLineHighlightBorder, 0.50), - hcDark: editorLineHighlightBorder, - hcLight: editorLineHighlightBorder + light: editorHoverBorder, + dark: editorHoverBorder, + hcDark: editorHoverBorder, + hcLight: editorHoverBorder }, localize('inlineEdit.originalBorder', 'Border color for the original text in inline edits.') ); @@ -88,15 +92,39 @@ export const originalBorder = registerColor( export const modifiedBorder = registerColor( 'inlineEdit.modifiedBorder', { - light: darken(editorLineHighlightBorder, 0.15), - dark: lighten(editorLineHighlightBorder, 0.50), - hcDark: editorLineHighlightBorder, - hcLight: editorLineHighlightBorder + light: editorHoverBorder, + dark: editorHoverBorder, + hcDark: editorHoverBorder, + hcLight: editorHoverBorder }, localize('inlineEdit.modifiedBorder', 'Border color for the modified text in inline edits.') ); -export class InlineEditsSideBySideDiff extends Disposable { +export interface IInlineEditsView { + isHovered: IObservable; +} + +const PADDING = 4; +const ENABLE_OVERFLOW = false; + +export class InlineEditsSideBySideDiff extends Disposable implements IInlineEditsView { + + // This is an approximation and should be improved by using the real parameters used bellow + static fitsInsideViewport(editor: ICodeEditor, edit: InlineEditWithChanges, reader: IReader): boolean { + const editorObs = observableCodeEditor(editor); + const editorWidth = editorObs.layoutInfoWidth.read(reader); + const editorContentLeft = editorObs.layoutInfoContentLeft.read(reader); + const editorVerticalScrollBar = editor.getLayoutInfo().verticalScrollbarWidth; + const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + + const maxOriginalContent = maxContentWidthInRange(editorObs, edit.originalLineRange, undefined/* do not reconsider on each layout info change */); + const maxModifiedContent = edit.lineEdit.newLines.reduce((max, line) => Math.max(max, line.length * w), 0); + const endOfEditorPadding = 20; // padding after last line of editor + const editorsPadding = edit.modifiedLineRange.length <= edit.originalLineRange.length ? PADDING * 3 + endOfEditorPadding : 60 + endOfEditorPadding * 2; + + return maxOriginalContent + maxModifiedContent + editorsPadding < editorWidth - editorContentLeft - editorVerticalScrollBar; + } + private readonly _editorObs = observableCodeEditor(this._editor); constructor( @@ -147,11 +175,13 @@ export class InlineEditsSideBySideDiff extends Disposable { return; } - this.previewEditor.layout({ height: layoutInfo.editHeight, width: layoutInfo.previewEditorWidth }); + const editorTopLeft = layoutInfo.editStart1.deltaY(layoutInfo.padding); + const editorBottomLeft = layoutInfo.editStart2.deltaY(-layoutInfo.padding); - const topEdit = layoutInfo.edit1; - this._editorContainer.element.style.top = `${topEdit.y}px`; - this._editorContainer.element.style.left = `${topEdit.x}px`; + this.previewEditor.layout({ height: editorBottomLeft.y - editorTopLeft.y, width: layoutInfo.previewEditorWidth + 15 /* Make sure editor does not scroll horizontally */ }); + this._editorContainer.element.style.top = `${editorTopLeft.y}px`; + this._editorContainer.element.style.left = `${editorTopLeft.x}px`; + this._editorContainer.element.style.width = `${layoutInfo.previewEditorWidth}px`; // Set width to clip view zone })); /*const toolbarDropdownVisible = observableFromEvent(this, this._toolbar.onDidChangeDropdownVisibility, (e) => e ?? false); @@ -168,8 +198,6 @@ export class InlineEditsSideBySideDiff extends Disposable { this._previewEditorObs.editor.setScrollLeft(layoutInfo.desiredPreviewEditorScrollLeft); })); - - this._editorContainerTopLeft.set(this._previewEditorLayoutInfo.map(i => i?.edit1), undefined); } private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); @@ -177,11 +205,9 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly previewRef = n.ref(); private readonly toolbarRef = n.ref(); - private readonly _editorContainerTopLeft = observableValue | undefined>(this, undefined); - private readonly _editorContainer = n.div({ - class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')], - style: { position: 'absolute' }, + class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.useGutterIndicator && 'showHover')], + style: { position: 'absolute', overflow: 'hidden' }, }, [ n.div({ class: 'preview', style: {}, ref: this.previewRef }), n.div({ class: 'toolbar', style: {}, ref: this.toolbarRef }), @@ -261,6 +287,7 @@ export class InlineEditsSideBySideDiff extends Disposable { overviewRulerLanes: 0, lineDecorationsWidth: 0, lineNumbersMinChars: 0, + revealHorizontalRightPadding: 0, bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: false }, scrollBeyondLastLine: false, scrollbar: { @@ -284,6 +311,7 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly _previewEditorObs = observableCodeEditor(this.previewEditor); + private _activeViewZones: string[] = []; private readonly _updatePreviewEditor = derived(reader => { this._editorContainer.readEffect(reader); @@ -316,6 +344,24 @@ export class InlineEditsSideBySideDiff extends Disposable { this.previewEditor.setHiddenAreas(hiddenAreas, undefined, true); + // TODO: is this the proper way to handle viewzones? + const previousViewZones = [...this._activeViewZones]; + this._activeViewZones = []; + + const reducedLinesCount = (range.endLineNumberExclusive - range.startLineNumber) - uiState.newTextLineCount; + this.previewEditor.changeViewZones((changeAccessor) => { + previousViewZones.forEach(id => changeAccessor.removeZone(id)); + + if (reducedLinesCount > 0) { + this._activeViewZones.push(changeAccessor.addZone({ + afterLineNumber: range.startLineNumber + uiState.newTextLineCount - 1, + heightInLines: reducedLinesCount, + showInHiddenAreas: true, + domNode: $('div.diagonal-fill.inline-edits-view-zone'), + })); + } + }); + }).recomputeInitiallyAndOnChange(this._store); private readonly _previewEditorWidth = derived(this, reader => { @@ -323,7 +369,7 @@ export class InlineEditsSideBySideDiff extends Disposable { if (!edit) { return 0; } this._updatePreviewEditor.read(reader); - return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader) + 10; + return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader); }); private readonly _cursorPosIfTouchesEdit = derived(this, reader => { @@ -385,8 +431,8 @@ export class InlineEditsSideBySideDiff extends Disposable { const editorContentAreaWidth = editorLayout.contentWidth - editorLayout.verticalScrollbarWidth; const editorBoundingClientRect = this._editor.getContainerDomNode().getBoundingClientRect(); const clientContentAreaRight = editorLayout.contentLeft + editorLayout.contentWidth + editorBoundingClientRect.left; - const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).outerWidth - clientContentAreaRight; - const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).outerWidth - editorBoundingClientRect.right; + const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).innerWidth - clientContentAreaRight; + const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).innerWidth - editorBoundingClientRect.right; const desiredMinimumWidth = Math.min(editorLayout.contentWidth * 0.3, previewContentWidth, 100); const IN_EDITOR_DISPLACEMENT = 0; const maximumAvailableWidth = IN_EDITOR_DISPLACEMENT + remainingWidthRightOfContent; @@ -394,7 +440,7 @@ export class InlineEditsSideBySideDiff extends Disposable { const cursorPos = this._cursorPosIfTouchesEdit.read(reader); const maxPreviewEditorLeft = Math.max( - // We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an ammount to ensure some mimum desired width + // We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an amount to ensure some minimum desired width editorContentAreaWidth + horizontalScrollOffset - IN_EDITOR_DISPLACEMENT - Math.max(0, desiredMinimumWidth - maximumAvailableWidth), // But we don't want that the moving left ends up covering the cursor, so this will push it to the right again Math.min( @@ -419,29 +465,54 @@ export class InlineEditsSideBySideDiff extends Disposable { } const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); - const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this._editorObs.scrollTop.read(reader); + // TODO: const { prefixLeftOffset } = getPrefixTrim(inlineEdit.edit.edits.map(e => e.range), inlineEdit.originalLineRange, [], this._editor); const codeLeft = editorLayout.contentLeft; - const code1 = new Point(left, selectionTop); - const codeStart1 = new Point(codeLeft, selectionTop); - const code2 = new Point(left, selectionBottom); - const codeStart2 = new Point(codeLeft, selectionBottom); + let code1 = new Point(left, selectionTop); + let codeStart1 = new Point(codeLeft, selectionTop); + let code2 = new Point(left, selectionBottom); + let codeStart2 = new Point(codeLeft, selectionBottom); + + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; const codeHeight = selectionBottom - selectionTop; + const previewEditorHeight = Math.max(codeHeight, editHeight); - const codeEditDistRange = inlineEdit.modifiedLineRange.length === inlineEdit.originalLineRange.length + const editIsSameHeight = codeHeight === previewEditorHeight; + const codeEditDistRange = editIsSameHeight ? new OffsetRange(4, 61) : new OffsetRange(60, 61); const clipped = dist === 0; - const codeEditDist = codeEditDistRange.clip(dist); - const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; + const codeEditDist = editIsSameHeight ? PADDING : codeEditDistRange.clip(dist); // TODO: Is there a better way to specify the distance? const previewEditorWidth = Math.min(previewContentWidth, remainingWidthRightOfEditor + editorLayout.width - editorLayout.contentLeft - codeEditDist); - const edit1 = new Point(left + codeEditDist, selectionTop); - const edit2 = new Point(left + codeEditDist, selectionTop + editHeight); + let editStart1 = new Point(left + codeEditDist, selectionTop); + let edit1 = editStart1.deltaX(previewEditorWidth); + let editStart2 = new Point(left + codeEditDist, selectionTop + previewEditorHeight); + let edit2 = editStart2.deltaX(previewEditorWidth); + + // padding + const isInsertion = codeHeight === 0; + if (!isInsertion) { + codeStart1 = codeStart1.deltaY(-PADDING).deltaX(-PADDING); + code1 = code1.deltaY(-PADDING); + codeStart2 = codeStart2.deltaY(PADDING).deltaX(-PADDING); + code2 = code2.deltaY(PADDING); + + editStart1 = editStart1.deltaY(-PADDING); + edit1 = edit1.deltaY(-PADDING).deltaX(PADDING); + editStart2 = editStart2.deltaY(PADDING); + edit2 = edit2.deltaY(PADDING).deltaX(PADDING); + } else { + // Align top of edit with insertion line + edit1 = edit1.deltaX(PADDING); + editStart2 = editStart2.deltaY(2 * PADDING); + edit2 = edit2.deltaY(2 * PADDING).deltaX(PADDING); + } return { code1, @@ -449,14 +520,19 @@ export class InlineEditsSideBySideDiff extends Disposable { code2, codeStart2, codeHeight, + codeScrollLeft: horizontalScrollOffset, + editStart1, edit1, + editStart2, edit2, editHeight, maxContentWidth, shouldShowShadow: clipped, desiredPreviewEditorScrollLeft, - previewEditorWidth + previewEditorWidth, + padding: PADDING, + borderRadius: PADDING }; }); @@ -464,6 +540,9 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly _stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); private readonly _shouldOverflow = derived(reader => { + if (!ENABLE_OVERFLOW) { + return false; + } const range = this._edit.read(reader)?.originalLineRange; if (!range) { return false; @@ -480,22 +559,25 @@ export class InlineEditsSideBySideDiff extends Disposable { return true; }); - private readonly _extendedModifiedPath = derived(reader => { const layoutInfo = this._previewEditorLayoutInfo.read(reader); if (!layoutInfo) { return undefined; } - const width = layoutInfo.previewEditorWidth; - const extendedModifiedPathBuilder = new PathBuilder() - .moveTo(layoutInfo.code1) - .lineTo(layoutInfo.edit1) - .lineTo(layoutInfo.edit1.deltaX(width)) - .lineTo(layoutInfo.edit2.deltaX(width)) - .lineTo(layoutInfo.edit2); - if (layoutInfo.edit2.y !== layoutInfo.code2.y) { - extendedModifiedPathBuilder.curveTo2(layoutInfo.edit2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); + + const path = new PathBuilder() + .moveTo(layoutInfo.code2) + .lineTo(layoutInfo.code1) + .lineTo(layoutInfo.editStart1) + .lineTo(layoutInfo.edit1.deltaX(-layoutInfo.borderRadius)) + .curveTo(layoutInfo.edit1, layoutInfo.edit1.deltaY(layoutInfo.borderRadius)) + .lineTo(layoutInfo.edit2.deltaY(-layoutInfo.borderRadius)) + .curveTo(layoutInfo.edit2, layoutInfo.edit2.deltaX(-layoutInfo.borderRadius)) + .lineTo(layoutInfo.editStart2); + + if (layoutInfo.editStart2.y !== layoutInfo.code2.y) { + path.curveTo2(layoutInfo.editStart2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); } - extendedModifiedPathBuilder.lineTo(layoutInfo.code2); - return extendedModifiedPathBuilder.build(); + path.lineTo(layoutInfo.code2); + return path.build(); }); private readonly _originalBackgroundColor = observableFromEvent(this, this._themeService.onDidColorThemeChange, () => { @@ -531,7 +613,7 @@ export class InlineEditsSideBySideDiff extends Disposable { }), ]).keepUpdated(this._store); - private readonly _foregroundBackgroundSvg = n.svg({ + private readonly _modifiedBackgroundSvg = n.svg({ transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, [ @@ -540,11 +622,38 @@ export class InlineEditsSideBySideDiff extends Disposable { d: this._extendedModifiedPath, style: { fill: 'var(--vscode-editor-background, transparent)', + strokeWidth: '0px', + } + }), + ]).keepUpdated(this._store); + + private readonly _foregroundBackgroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, [ + n.svgElem('path', { + class: 'extendedModifiedBackgroundCoverUp', + d: this._extendedModifiedPath, + style: { + fill: 'var(--vscode-inlineEdit-modifiedChangedLineBackground, transparent)', strokeWidth: '1px', } }), ]).keepUpdated(this._store); + private readonly _middleBorderWithShadow = n.div({ + class: ['middleBorderWithShadow'], + style: { + position: 'absolute', + display: this._previewEditorLayoutInfo.map(i => i?.shouldShowShadow ? 'block' : 'none'), + width: '6px', + boxShadow: 'var(--vscode-scrollbar-shadow) -6px 0 6px -6px inset', + left: this._previewEditorLayoutInfo.map(i => i ? i.code1.x - 6 : 0), + top: this._previewEditorLayoutInfo.map(i => i ? i.code1.y : 0), + height: this._previewEditorLayoutInfo.map(i => i ? i.code2.y - i.code1.y : 0), + }, + }, []).keepUpdated(this._store); + private readonly _foregroundSvg = n.svg({ transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, @@ -552,17 +661,15 @@ export class InlineEditsSideBySideDiff extends Disposable { const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); if (!layoutInfoObs) { return undefined; } - const shadowWidth = 6; return [ n.svgElem('path', { class: 'originalOverlay', - d: layoutInfoObs.map(layoutInfo => new PathBuilder() - .moveTo(layoutInfo.code2) - .lineTo(layoutInfo.codeStart2) - .lineTo(layoutInfo.codeStart1) - .lineTo(layoutInfo.code1) - .build() - ), + d: layoutInfoObs.map(layoutInfo => createRectangle( + { topLeft: layoutInfo.codeStart1, width: layoutInfo.code1.x - layoutInfo.codeStart1.x, height: layoutInfo.code2.y - layoutInfo.code1.y }, + 0, + { topLeft: layoutInfo.borderRadius, bottomLeft: layoutInfo.borderRadius, topRight: 0, bottomRight: 0 }, + { hideRight: true, hideLeft: layoutInfo.codeScrollLeft !== 0 } + )), style: { fill: 'var(--vscode-inlineEdit-originalBackground, transparent)', stroke: 'var(--vscode-inlineEdit-originalBorder)', @@ -579,46 +686,19 @@ export class InlineEditsSideBySideDiff extends Disposable { strokeWidth: '1px', } }), - - ...(!layoutInfoObs.map(i => i.shouldShowShadow).read(reader) - ? [ - n.svgElem('path', { - class: 'middleBorder', - d: layoutInfoObs.map(layoutInfo => new PathBuilder() - .moveTo(layoutInfo.code1) - .lineTo(layoutInfo.code2) - .build() - ), - style: { - stroke: 'var(--vscode-inlineEdit-modifiedBorder)', - strokeWidth: '1px' - } - }) - ] - : [ - n.svgElem('defs', {}, [ - n.svgElem('linearGradient', { id: 'gradient', x1: '0%', x2: '100%', }, [ - n.svgElem('stop', { - offset: '0%', - style: { stopColor: 'var(--vscode-inlineEdit-modifiedBorder)', stopOpacity: '0', } - }), - n.svgElem('stop', { - offset: '100%', - style: { stopColor: 'var(--vscode-inlineEdit-modifiedBorder)', stopOpacity: '1', } - }) - ]) - ]), - n.svgElem('rect', { - class: 'middleBorderWithShadow', - x: layoutInfoObs.map(layoutInfo => layoutInfo.code1.x - shadowWidth), - y: layoutInfoObs.map(layoutInfo => layoutInfo.code1.y), - width: shadowWidth, - height: layoutInfoObs.map(layoutInfo => layoutInfo.code2.y - layoutInfo.code1.y), - fill: 'url(#gradient)', - style: { strokeWidth: '0', stroke: 'transparent', } - }) - ] - ) + n.svgElem('path', { + class: 'middleBorder', + d: layoutInfoObs.map(layoutInfo => new PathBuilder() + .moveTo(layoutInfo.code1) + .lineTo(layoutInfo.code2) + .build() + ), + style: { + display: layoutInfoObs.map(i => i.shouldShowShadow ? 'none' : 'block'), + stroke: 'var(--vscode-inlineEdit-modifiedBorder)', + strokeWidth: '1px' + } + }) ]; })).keepUpdated(this._store); @@ -634,7 +714,7 @@ export class InlineEditsSideBySideDiff extends Disposable { }, }, [ this._backgroundSvg, - derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg]), + derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._modifiedBackgroundSvg, this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow]), ]).keepUpdated(this._store); private readonly _overflowView = n.div({ @@ -645,6 +725,6 @@ export class InlineEditsSideBySideDiff extends Disposable { display: this._display, }, }, [ - derived(this, reader => this._shouldOverflow.read(reader) ? [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg] : []), + derived(this, reader => this._shouldOverflow.read(reader) ? [this._modifiedBackgroundSvg, this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow] : []), ]).keepUpdated(this._store); } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 57e0eb3393e..3c80494c8ba 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -14,6 +14,7 @@ import { OS } from '../../../../../../base/common/platform.js'; import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; import { MenuEntryActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; import { Rect } from '../../../../../browser/rect.js'; @@ -24,8 +25,9 @@ import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js'; import { RangeMapping } from '../../../../../common/diff/rangeMapping.js'; +import { indentOfLine } from '../../../../../common/model/textModel.js'; -export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader): number { +export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { editor.layoutInfo.read(reader); editor.value.read(reader); @@ -66,6 +68,22 @@ export function getOffsetForPos(editor: ObservableCodeEditor, pos: Position, rea return lineContentWidth; } +export function getPrefixTrim(diffRanges: Range[], originalLinesRange: LineRange, modifiedLines: string[], editor: ICodeEditor): { prefixTrim: number; prefixLeftOffset: number } { + const textModel = editor.getModel(); + if (!textModel) { + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } + + const replacementStart = diffRanges.map(r => r.isSingleLine() ? r.startColumn - 1 : 0); + const originalIndents = originalLinesRange.mapToLineArray(line => indentOfLine(textModel.getLineContent(line))); + const modifiedIndents = modifiedLines.map(line => indentOfLine(line)); + const prefixTrim = Math.min(...replacementStart, ...originalIndents, ...modifiedIndents); + + const prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1); + + return { prefixTrim, prefixLeftOffset }; +} + export class StatusBarViewItem extends MenuEntryActionViewItem { protected readonly _updateLabelListener = this._register(this._contextKeyService.onDidChangeContext(() => { this.updateLabel(); @@ -163,6 +181,94 @@ export class PathBuilder { } } +// Arguments are a bit messy currently, could be improved +export function createRectangle( + layout: { topLeft: Point; width: number; height: number }, + padding: number | { top: number; right: number; bottom: number; left: number }, + borderRadius: number | { topLeft: number; topRight: number; bottomLeft: number; bottomRight: number }, + options: { hideLeft?: boolean; hideRight?: boolean; hideTop?: boolean; hideBottom?: boolean } = {} +): string { + + const topLeftInner = layout.topLeft; + const topRightInner = topLeftInner.deltaX(layout.width); + const bottomLeftInner = topLeftInner.deltaY(layout.height); + const bottomRightInner = bottomLeftInner.deltaX(layout.width); + + // padding + const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = typeof padding === 'number' ? + { top: padding, bottom: padding, left: padding, right: padding } + : padding; + + // corner radius + const { topLeft: radiusTL, topRight: radiusTR, bottomLeft: radiusBL, bottomRight: radiusBR } = typeof borderRadius === 'number' ? + { topLeft: borderRadius, topRight: borderRadius, bottomLeft: borderRadius, bottomRight: borderRadius } : + borderRadius; + + const totalHeight = layout.height + paddingTop + paddingBottom; + const totalWidth = layout.width + paddingLeft + paddingRight; + + // The path is drawn from bottom left at the end of the rounded corner in a clockwise direction + // Before: before the rounded corner + // After: after the rounded corner + const topLeft = topLeftInner.deltaX(-paddingLeft).deltaY(-paddingTop); + const topRight = topRightInner.deltaX(paddingRight).deltaY(-paddingTop); + const topLeftBefore = topLeft.deltaY(Math.min(radiusTL, totalHeight / 2)); + const topLeftAfter = topLeft.deltaX(Math.min(radiusTL, totalWidth / 2)); + const topRightBefore = topRight.deltaX(-Math.min(radiusTR, totalWidth / 2)); + const topRightAfter = topRight.deltaY(Math.min(radiusTR, totalHeight / 2)); + + const bottomLeft = bottomLeftInner.deltaX(-paddingLeft).deltaY(paddingBottom); + const bottomRight = bottomRightInner.deltaX(paddingRight).deltaY(paddingBottom); + const bottomLeftBefore = bottomLeft.deltaX(Math.min(radiusBL, totalWidth / 2)); + const bottomLeftAfter = bottomLeft.deltaY(-Math.min(radiusBL, totalHeight / 2)); + const bottomRightBefore = bottomRight.deltaY(-Math.min(radiusBR, totalHeight / 2)); + const bottomRightAfter = bottomRight.deltaX(-Math.min(radiusBR, totalWidth / 2)); + + const path = new PathBuilder(); + + if (!options.hideLeft) { + path.moveTo(bottomLeftAfter).lineTo(topLeftBefore); + } + + if (!options.hideLeft && !options.hideTop) { + path.curveTo(topLeft, topLeftAfter); + } else { + path.moveTo(topLeftAfter); + } + + if (!options.hideTop) { + path.lineTo(topRightBefore); + } + + if (!options.hideTop && !options.hideRight) { + path.curveTo(topRight, topRightAfter); + } else { + path.moveTo(topRightAfter); + } + + if (!options.hideRight) { + path.lineTo(bottomRightBefore); + } + + if (!options.hideRight && !options.hideBottom) { + path.curveTo(bottomRight, bottomRightAfter); + } else { + path.moveTo(bottomRightAfter); + } + + if (!options.hideBottom) { + path.lineTo(bottomLeftBefore); + } + + if (!options.hideBottom && !options.hideLeft) { + path.curveTo(bottomLeft, bottomLeftAfter); + } else { + path.moveTo(bottomLeftAfter); + } + + return path.build(); +} + type Value = T | IObservable; type ValueOrList = Value | ValueOrList[]; type ValueOrList2 = ValueOrList | ValueOrList>; @@ -392,11 +498,13 @@ function setClassName(domNode: Element, className: string) { function resolve(value: ValueOrList, reader: IReader | undefined, cb: (val: T) => void): void { if (isObservable(value)) { cb(value.read(reader)); + return; } if (Array.isArray(value)) { for (const v of value) { resolve(v, reader, cb); } + return; } cb(value as any); } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 0a46a149042..747e0c0e50c 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -132,6 +132,14 @@ border: none; } } + + .monaco-editor-background { + background-color: var(--vscode-inlineEdit-modifiedChangedLineBackground) + } + } + + .inline-edits-view-zone.diagonal-fill { + opacity: 0.5; } } } @@ -164,6 +172,64 @@ .inlineCompletions-char-insert.diff-range-empty { border-left: solid var(--vscode-inlineEdit-modifiedChangedTextBackground) 3px; } + + .inlineCompletions-char-delete.single-line-inline, + .inlineCompletions-char-insert.single-line-inline { + border-radius: 4px; + border: 1px solid var(--vscode-editorHoverWidget-border); + padding: 2px; + } + + .inlineCompletions-char-delete.single-line-inline.empty, + .inlineCompletions-char-insert.single-line-inline.empty { + display: none; + } + + /* Adjustments due to being a decoration */ + .inlineCompletions-char-delete.single-line-inline { + margin: -2px 0 0 -2px; + } + + .inlineCompletions.strike-through { + text-decoration-thickness: 1px; + } + + /* line replacement bubbles */ + + .inlineCompletions-modified-bubble{ + background: var(--vscode-inlineEdit-modifiedChangedTextBackground); + } + + .inlineCompletions-original-bubble{ + background: var(--vscode-inlineEdit-originalChangedTextBackground); + border-radius: 4px; + } + + .inlineCompletions-modified-bubble, + .inlineCompletions-original-bubble { + pointer-events: none; + } + + .inlineCompletions-modified-bubble.start { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + + .inlineCompletions-modified-bubble.end { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } + + .inline-edit.ghost-text, + .inline-edit.ghost-text-decoration, + .inline-edit.ghost-text-decoration-preview, + .inline-edit.suggest-preview-text .ghost-text { + &.syntax-highlighted { + opacity: 1 !important; + } + background: var(--vscode-inlineEdit-modifiedChangedTextBackground) !important; + font-style: normal !important; + } } .monaco-menu-option { @@ -184,3 +250,33 @@ outline-offset: -1px; } } + +.inline-edits-view-gutter-indicator .codicon { + margin-top: 1px; /* TODO: Move into gutter DOM initialization */ +} + +@keyframes wiggle { + 0% { + transform: rotate(0) scale(1); + } + + 15%, + 45% { + transform: rotate(.04turn) scale(1.1); + } + + 30%, + 60% { + transform: rotate(-.04turn) scale(1.2); + } + + 100% { + transform: rotate(0) scale(1); + } +} + +.inline-edits-view-gutter-indicator.wiggle .icon { + animation-duration: .8s; + animation-iteration-count: 1; + animation-name: wiggle; +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 4282852af53..b401367da27 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, IObservable, IReader, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, derived, IObservable, IReader, ISettableObservable, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -16,27 +16,37 @@ import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { InlineEditsDeletionView } from './deletionView.js'; import { InlineEditsGutterIndicator } from './gutterIndicatorView.js'; import { IInlineEditsIndicatorState, InlineEditsIndicator } from './indicatorView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineDiffView.js'; +import { InlineEditsInsertionView } from './insertionView.js'; import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; import './view.css'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; -import { WordInsertView, WordReplacementView } from './wordReplacementView.js'; +import { LineReplacementView, WordInsertView, WordReplacementView } from './wordReplacementView.js'; export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _useMixedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useMixedLinesDiff); - private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); - private readonly _useWordReplacementView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordReplacementView); - private readonly _useWordInsertionView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordInsertionView); + private readonly _useMixedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useMixedLinesDiff); + private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useInterleavedLinesDiff); + private readonly _useCodeShifting = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.codeShifting); + private readonly _useMultiLineGhostText = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useMultiLineGhostText); + + private _previousView: { + id: string; + view: ReturnType; + userJumpedToIt: boolean; + editorWidth: number; + } | undefined; constructor( private readonly _editor: ICodeEditor, private readonly _edit: IObservable, private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -62,6 +72,10 @@ export class InlineEditsView extends Disposable { let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); const state = this.determineRenderState(edit, reader, diff, new StringText(newText)); + if (!state) { + this._model.get()?.stop(); + return undefined; + } if (state.kind === 'sideBySide') { const indentationAdjustmentEdit = createReindentEdit(newText, edit.modifiedLineRange); @@ -78,7 +92,12 @@ export class InlineEditsView extends Disposable { )!; this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); - this._previewTextModel.setValue(newText); + + const previousNewText = this._previewTextModel.getValue(); + if (previousNewText !== newText) { + // Only update the model if the text has changed to avoid flickering + this._previewTextModel.setValue(newText); + } return { state, @@ -102,17 +121,35 @@ export class InlineEditsView extends Disposable { this._editor, this._edit, this._previewTextModel, - this._uiState.map(s => s && s.state.kind === 'sideBySide' ? ({ + this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({ edit: s.edit, newTextLineCount: s.newTextLineCount, originalDisplayRange: s.originalDisplayRange, }) : undefined), )); + protected readonly _deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView, + this._editor, + this._edit, + this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ + originalRange: s.state.originalRange, + deletions: s.state.deletions, + }) : undefined), + )); + + protected readonly _insertion = this._register(this._instantiationService.createInstance(InlineEditsInsertionView, + this._editor, + this._uiState.map(s => s && s.state?.kind === 'insertionMultiLine' ? ({ + lineNumber: s.state.lineNumber, + startColumn: s.state.column, + text: s.state.text, + }) : undefined), + )); + private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); - if (!e) { return undefined; } - if (e.state.kind === 'wordReplacements') { + if (!e || !e.state) { return undefined; } + if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertionMultiLine') { return undefined; } return { @@ -125,15 +162,27 @@ export class InlineEditsView extends Disposable { protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); - protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { + protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { if (e.range.isEmpty()) { return store.add(this._instantiationService.createInstance(WordInsertView, this._editorObs, e)); } else { - return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e)); + return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e, [e])); } }).recomputeInitiallyAndOnChange(this._store); - private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); + protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables + return store.add(this._instantiationService.createInstance(LineReplacementView, this._editorObs, e.originalRange, e.modifiedRange, e.modifiedLines, e.replacements)); + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useGutterIndicator); + + private readonly _inlineEditsIsHovered = derived(this, reader => { + return this._sideBySide.isHovered.read(reader) + || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) + || this._deletion.isHovered.read(reader) + || this._inlineDiffView.isHovered.read(reader) + || this._lineReplacementView.read(reader).some(v => v.isHovered.read(reader)); + }); protected readonly _indicator = this._register(autorunWithStore((reader, store) => { if (this._useGutterIndicator.read(reader)) { @@ -142,14 +191,15 @@ export class InlineEditsView extends Disposable { this._editorObs, this._uiState.map(s => s && s.originalDisplayRange), this._model, - this._sideBySide.isHovered, + this._inlineEditsIsHovered, + this._focusIsInMenu, )); } else { store.add(new InlineEditsIndicator( this._editorObs, derived(reader => { const state = this._uiState.read(reader); - if (!state) { return undefined; } + if (!state || !state.state) { return undefined; } const range = state.originalDisplayRange; const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; @@ -159,51 +209,136 @@ export class InlineEditsView extends Disposable { } })); - private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { + private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): string { + // Check if we can use the previous view if it is the same InlineCompletion as previously shown + const canUseCache = this._previousView?.id === edit.inlineCompletion.id; + const reconsiderViewAfterJump = edit.userJumpedToIt !== this._previousView?.userJumpedToIt && + ( + (this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView?.view !== 'mixedLines') || + (this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView?.view !== 'interleavedLines') + ); + const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editor.getLayoutInfo().width && + ( + this._previousView?.view === 'sideBySide' || + this._previousView?.view === 'lineReplacement' + ); + + if (canUseCache && !reconsiderViewAfterJump && !reconsiderViewEditorWidthChange) { + return this._previousView!.view; + } + + // Determine the view based on the edit / diff + if (edit.isCollapsed) { - return { kind: 'collapsed' as const }; + return 'collapsed'; } + const inner = diff.flatMap(d => d.innerChanges ?? []); + const isSingleInnerEdit = inner.length === 1; if ( - this._useMixedLinesDiff.read(reader) === 'forStableInsertions' - && isInsertionAfterPosition(diff, edit.cursorPosition) + isSingleInnerEdit && ( + this._useMixedLinesDiff.read(reader) === 'forStableInsertions' + && this._useCodeShifting.read(reader) + && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) + || isSingleLineDeletion(diff) + ) ) { - return { kind: 'ghostText' as const }; - } - - if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { - const inner = diff.flatMap(d => d.innerChanges!); - if (inner.every( - m => (m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' - || !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible') - && TextLength.ofRange(m.originalRange).columnCount < 100 - && TextLength.ofRange(m.modifiedRange).columnCount < 100 - )) { - return { - kind: 'wordReplacements' as const, - replacements: inner.map(i => - new SingleTextEdit(i.originalRange, newText.getValueOfRange(i.modifiedRange)) - ) - }; - } + return 'insertionInline'; + } + + const innerValues = inner.map(m => ({ original: newText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); + if (innerValues.every(({ original, modified }) => modified.trim() === '' && original.length > 0 && (original.length > modified.length || original.trim() !== ''))) { + return 'deletion'; + } + + if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader) && this._useCodeShifting.read(reader)) { + return 'insertionMultiLine'; + } + + const numOriginalLines = edit.originalLineRange.length; + const numModifiedLines = edit.modifiedLineRange.length; + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { + return 'wordReplacements'; + } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { + return 'lineReplacement'; } if ( (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) && diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) ) { - return { kind: 'mixedLines' as const }; + return 'mixedLines'; } if (this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) { - return { kind: 'interleavedLines' as const }; + return 'interleavedLines'; } - return { kind: 'sideBySide' as const }; + return 'sideBySide'; + } + + private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { + + const view = this.determineView(edit, reader, diff, newText); + + this._previousView = { id: edit.inlineCompletion.id, view, userJumpedToIt: edit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width }; + + switch (view) { + case 'collapsed': return { kind: 'collapsed' as const }; + case 'insertionInline': return { kind: 'insertionInline' as const }; + case 'mixedLines': return { kind: 'mixedLines' as const }; + case 'interleavedLines': return { kind: 'interleavedLines' as const }; + case 'sideBySide': return { kind: 'sideBySide' as const }; + } + + const inner = diff.flatMap(d => d.innerChanges ?? []); + + if (view === 'deletion') { + return { + kind: 'deletion' as const, + originalRange: edit.originalLineRange, + deletions: inner.map(m => m.originalRange), + }; + } + + if (view === 'insertionMultiLine') { + const change = inner[0]; + return { + kind: 'insertionMultiLine' as const, + lineNumber: change.originalRange.startLineNumber, + column: change.originalRange.startColumn, + text: newText.getValueOfRange(change.modifiedRange), + }; + } + + const replacements = inner.map(m => new SingleTextEdit(m.originalRange, newText.getValueOfRange(m.modifiedRange))); + if (replacements.length === 0) { + return undefined; + } + + if (view === 'wordReplacements') { + return { + kind: 'wordReplacements' as const, + replacements + }; + } + + if (view === 'lineReplacement') { + return { + kind: 'lineReplacement' as const, + originalRange: edit.originalLineRange, + modifiedRange: edit.modifiedLineRange, + modifiedLines: edit.modifiedLineRange.mapToLineArray(line => newText.getLineAt(line)), + replacements: inner.map(m => ({ originalRange: m.originalRange, modifiedRange: m.modifiedRange })), + }; + } + + return undefined; } } -function isInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Position | null) { +function isSingleLineInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Position | null) { if (!position) { return false; } @@ -229,3 +364,36 @@ function isInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Po return false; } } + +function isSingleMultiLineInsertion(diff: DetailedLineRangeMapping[]) { + const inner = diff.flatMap(d => d.innerChanges ?? []); + if (inner.length !== 1) { + return false; + } + + const change = inner[0]; + if (!change.originalRange.isEmpty()) { + return false; + } + + if (change.modifiedRange.startLineNumber === change.modifiedRange.endLineNumber) { + return false; + } + + return true; +} + +function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { + return diff.every(m => m.innerChanges!.every(r => isDeletion(r))); + + function isDeletion(r: RangeMapping) { + if (!r.modifiedRange.isEmpty()) { + return false; + } + const isDeletionWithinLine = r.originalRange.startLineNumber === r.originalRange.endLineNumber; + if (!isDeletionWithinLine) { + return false; + } + return true; + } +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts index f56b4d2c986..a0e9d4d1268 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equals.js'; import { createHotClass } from '../../../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { derivedDisposable, ObservablePromise, derived, IObservable, derivedOpts } from '../../../../../../base/common/observable.js'; +import { derivedDisposable, ObservablePromise, derived, IObservable, derivedOpts, ISettableObservable } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { IDiffProviderFactoryService } from '../../../../../browser/widget/diffEditor/diffProviderFactoryService.js'; @@ -99,13 +99,14 @@ export class InlineEditsViewAndDiffProducer extends Disposable { private readonly _editor: ICodeEditor, private readonly _edit: IObservable, private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, @IModelService private readonly _modelService: IModelService ) { super(); - this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEdit, this._model)); + this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEdit, this._model, this._focusIsInMenu)); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 19f28075c33..fe5996d1db9 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { constObservable, derived } from '../../../../../../base/common/observable.js'; +import { Disposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { constObservable, derived, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { editorHoverStatusBarBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -18,20 +18,28 @@ import { SingleTextEdit } from '../../../../../common/core/textEdit.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; -import { mapOutFalsy, n, rectToProps } from './utils.js'; +import { getPrefixTrim, mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; +import { Range } from '../../../../../common/core/range.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; +import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../common/model.js'; +import { $ } from '../../../../../../base/browser/dom.js'; +import { observableValue } from '../../../../../../base/common/observableInternal/base.js'; +import { IViewZoneChangeAccessor } from '../../../../../browser/editorBrowser.js'; export const transparentHoverBackground = registerColor( 'inlineEdit.wordReplacementView.background', { light: transparent(editorHoverStatusBarBackground, 0.1), - dark: transparent(editorHoverStatusBarBackground, 0.5), + dark: transparent(editorHoverStatusBarBackground, 0.1), hcLight: transparent(editorHoverStatusBarBackground, 0.1), hcDark: transparent(editorHoverStatusBarBackground, 0.1), }, localize('inlineEdit.wordReplacementView.background', 'Background color for the inline edit word replacement view.') ); -export class WordReplacementView extends Disposable { +export class WordReplacementView extends Disposable implements IInlineEditsView { private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); private readonly _end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); @@ -53,33 +61,67 @@ export class WordReplacementView extends Disposable { renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), [], this._line, true); }); + private readonly _editLocations = mapObservableArrayCached(this, constObservable(this._innerEdits), (edit, store) => { + const start = this._editor.observePosition(constObservable(edit.range.getStartPosition()), store); + const end = this._editor.observePosition(constObservable(edit.range.getEndPosition()), store); + return { start, end, edit }; + }).recomputeInitiallyAndOnChange(this._store); + private readonly _layout = derived(this, reader => { this._text.read(reader); - const start = this._start.read(reader); - const end = this._end.read(reader); - if (!start || !end) { + const widgetStart = this._start.read(reader); + const widgetEnd = this._end.read(reader); + + if (!widgetStart || !widgetEnd || widgetStart.x > widgetEnd.x) { return undefined; } + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); - if (start.x > end.x) { - return undefined; - } - const original = Rect.fromLeftTopWidthHeight(start.x + contentLeft - this._editor.scrollLeft.read(reader), start.y, end.x - start.x, lineHeight); + const scrollLeft = this._editor.scrollLeft.read(reader); const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; - const modified = Rect.fromLeftTopWidthHeight(original.left + 20, original.top + lineHeight + 5, this._edit.text.length * w + 5, original.height); - const background = Rect.hull([original, modified]).withMargin(4); + const modifiedLeftOffset = 20; + const modifiedTopOffset = 5; + const PADDING = 4; + + const originalLine = Rect.fromLeftTopWidthHeight(widgetStart.x + contentLeft - scrollLeft, widgetStart.y, widgetEnd.x - widgetStart.x, lineHeight); + const modifiedLine = Rect.fromLeftTopWidthHeight(originalLine.left + modifiedLeftOffset, originalLine.top + lineHeight + modifiedTopOffset, this._edit.text.length * w + 5, originalLine.height); + const background = Rect.hull([originalLine, modifiedLine]).withMargin(PADDING); + + let textLengthDelta = 0; + const editLocations = this._editLocations.read(reader); + const innerEdits = []; + for (const editLocation of editLocations) { + const editStart = editLocation.start.read(reader); + const editEnd = editLocation.end.read(reader); + const edit = editLocation.edit; + + if (!editStart || !editEnd || editStart.x > editEnd.x) { + return; + } + + const original = Rect.fromLeftTopWidthHeight(editStart.x + contentLeft - scrollLeft, editStart.y, editEnd.x - editStart.x, lineHeight); + const modified = Rect.fromLeftTopWidthHeight(original.left + modifiedLeftOffset + textLengthDelta * w, original.top + lineHeight + modifiedTopOffset, edit.text.length * w + 5, original.height); + + textLengthDelta += edit.text.length - (edit.range.endColumn - edit.range.startColumn); + + innerEdits.push({ original, modified }); + } + + const lowerBackground = background.intersectVertical(new OffsetRange(originalLine.bottom, Number.MAX_SAFE_INTEGER)); + const lowerText = new Rect(lowerBackground.left + modifiedLeftOffset + 6, lowerBackground.top + modifiedTopOffset, lowerBackground.right, lowerBackground.bottom); // TODO: left seems slightly off? zooming? return { - original, - modified, + originalLine, + modifiedLine, background, - lowerBackground: background.intersectVertical(new OffsetRange(original.bottom, Number.MAX_SAFE_INTEGER)), + innerEdits, + lowerBackground, + lowerText, + padding: PADDING }; }); - - private readonly _div = n.div({ class: 'word-replacement', }, [ @@ -89,94 +131,449 @@ export class WordReplacementView extends Disposable { return []; } + const layoutProps = layout.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + let contentLeft = this._editor.layoutInfoContentLeft.read(reader); + let contentWidth = this._editor.contentWidth.read(reader); + const contentHeight = this._editor.editor.getContentHeight(); + + if (scrollLeft === 0) { + contentLeft -= layoutProps.padding; + contentWidth += layoutProps.padding; + } + + const edits = layoutProps.innerEdits.map(edit => ({ modified: edit.modified.moveLeft(contentLeft), original: edit.original.moveLeft(contentLeft) })); + return [ n.div({ style: { position: 'absolute', - ...rectToProps(reader => layout.read(reader).lowerBackground), - borderRadius: '4px', - background: 'var(--vscode-editor-background)' - } - }, []), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).modified), - borderRadius: '4px', - padding: '0px', - textAlign: 'center', - background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', - fontFamily: this._editor.getOption(EditorOption.fontFamily), - fontSize: this._editor.getOption(EditorOption.fontSize), - fontWeight: this._editor.getOption(EditorOption.fontWeight), + top: 0, + left: contentLeft, + width: contentWidth, + height: contentHeight, + overflow: 'hidden', + pointerEvents: 'none', } }, [ - this._line, - ]), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).original), - borderRadius: '4px', - boxSizing: 'border-box', - background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', - } - }, []), - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).background), - borderRadius: '4px', + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground.moveLeft(contentLeft)), + borderRadius: '4px', + background: 'var(--vscode-editor-background)', + boxShadow: 'var(--vscode-scrollbar-shadow) 0 6px 6px -6px' + }, + }, []), + n.div({ + style: { + position: 'absolute', + padding: '0px', + boxSizing: 'border-box', + ...rectToProps(reader => layout.read(reader).lowerText.moveLeft(contentLeft)), + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + pointerEvents: 'none', + } + }, [this._line]), + ...edits.map(edit => n.div({ + style: { + position: 'absolute', + top: edit.modified.top, + left: edit.modified.left, + width: edit.modified.width, + height: edit.modified.height, + borderRadius: '4px', - border: '1px solid var(--vscode-editorHoverWidget-border)', - //background: 'rgba(122, 122, 122, 0.12)', looks better - background: 'var(--vscode-inlineEdit-wordReplacementView-background)', - } - }, []), + background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + pointerEvents: 'none', + } + }), []), + ...edits.map(edit => n.div({ + style: { + position: 'absolute', + top: edit.original.top, + left: edit.original.left, + width: edit.original.width, + height: edit.original.height, + borderRadius: '4px', + boxSizing: 'border-box', + background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', + pointerEvents: 'none', + } + }, [])), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background.moveLeft(contentLeft)), + borderRadius: '4px', - n.svg({ - width: 11, - height: 13, - viewBox: '0 0 11 13', - fill: 'none', + border: '1px solid var(--vscode-editorHoverWidget-border)', + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + pointerEvents: 'none', + boxSizing: 'border-box', + } + }, []), + + n.svg({ + width: 11, + height: 13, + viewBox: '0 0 11 13', + fill: 'none', + style: { + position: 'absolute', + left: derived(reader => layout.read(reader).modifiedLine.moveLeft(contentLeft).left - 15), + top: derived(reader => layout.read(reader).modifiedLine.top), + } + }, [ + n.svgElem('path', { + d: 'M1 0C1 2.98966 1 4.92087 1 7.49952C1 8.60409 1.89543 9.5 3 9.5H10.5', + stroke: 'var(--vscode-editorHoverWidget-foreground)', + }), + n.svgElem('path', { + d: 'M6 6.5L9.99999 9.49998L6 12.5', + stroke: 'var(--vscode-editorHoverWidget-foreground)', + }) + ]), + + ]) + ]; + }) + ]).keepUpdated(this._store); + + readonly isHovered = derived(this, reader => { + return this._div.getIsHovered(this._store).read(reader); + }); + + constructor( + private readonly _editor: ObservableCodeEditor, + /** Must be single-line in both sides */ + private readonly _edit: SingleTextEdit, + private readonly _innerEdits: SingleTextEdit[], + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(this._editor.createOverlayWidget({ + domNode: this._div.element, + minContentWidthInPx: constObservable(0), + position: constObservable({ preference: { top: 0, left: 0 } }), + allowEditorOverflow: false, + })); + } +} + +export class LineReplacementView extends Disposable implements IInlineEditsView { + + private readonly _originalBubblesDecorationCollection = this._editor.editor.createDecorationsCollection(); + private readonly _originalBubblesDecorationOptions: IModelDecorationOptions = { + description: 'inlineCompletions-original-bubble', + className: 'inlineCompletions-original-bubble', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + }; + + private readonly _maxPrefixTrim = getPrefixTrim(this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]), this._originalRange, this._modifiedLines, this._editor.editor); + + private readonly _modifiedLineElements = derived(reader => { + const lines = []; + let requiredWidth = 0; + + const maxPrefixTrim = this._maxPrefixTrim.prefixTrim; + const modifiedBubbles = rangesToBubbleRanges(this._replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); + + const textModel = this._editor.model.get()!; + const startLineNumber = this._modifiedRange.startLineNumber; + for (let i = 0; i < this._modifiedRange.length; i++) { + const line = document.createElement('div'); + const lineNumber = startLineNumber + i; + const modLine = this._modifiedLines[i].replace(/\t\n/g, '').slice(maxPrefixTrim); + + const t = textModel.tokenization.tokenizeLinesAt(lineNumber, [modLine])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).toLineTokens(modLine, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(modLine, this._languageService.languageIdCodec); + } + + const decorations = []; + for (const modified of modifiedBubbles.filter(b => b.startLineNumber === lineNumber)) { + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.endColumn), 'inlineCompletions-modified-bubble', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.startColumn + 1), 'start', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, modified.endColumn - 1, 1, modified.endColumn), 'end', InlineDecorationType.Regular)); + } + + const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), decorations, line, true); + + requiredWidth = Math.max(requiredWidth, result.minWidthInPx); + + lines.push(line); + } + + return { lines, requiredWidth: requiredWidth - 10 }; // TODO: Width is always too large, why? + }); + + private readonly _viewZoneInfo = observableValue<{ height: number; lineNumber: number } | undefined>('viewZoneInfo', undefined); + + private readonly _layout = derived(this, reader => { + const { requiredWidth } = this._modifiedLineElements.read(reader); + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + const scrollTop = this._editor.scrollTop.read(reader); + const editorLeftOffset = contentLeft - scrollLeft; + const PADDING = 4; + + const textModel = this._editor.editor.getModel()!; + const { prefixLeftOffset } = this._maxPrefixTrim; + + const originalLineWidths = this._originalRange.mapToLineArray(line => this._editor.editor.getOffsetForColumn(line, textModel.getLineMaxColumn(line)) - prefixLeftOffset); + const maxLineWidth = Math.max(...originalLineWidths, requiredWidth); + + const startLineNumber = this._originalRange.startLineNumber; + const endLineNumber = this._originalRange.endLineNumberExclusive - 1; + const topOfOriginalLines = this._editor.editor.getTopForLineNumber(startLineNumber) - scrollTop; + const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; + + // Box Widget positioning + const originalLinesOverlay = Rect.fromLeftTopWidthHeight( + editorLeftOffset + prefixLeftOffset, + topOfOriginalLines, + maxLineWidth, + bottomOfOriginalLines - topOfOriginalLines + PADDING + ); + const modifiedLinesOverlay = Rect.fromLeftTopWidthHeight( + originalLinesOverlay.left, + originalLinesOverlay.bottom + PADDING, + originalLinesOverlay.width, + this._modifiedRange.length * lineHeight + ); + const background = Rect.hull([originalLinesOverlay, modifiedLinesOverlay]).withMargin(PADDING); + + const lowerBackground = background.intersectVertical(new OffsetRange(originalLinesOverlay.bottom, Number.MAX_SAFE_INTEGER)); + const lowerText = new Rect(lowerBackground.left + PADDING, lowerBackground.top + PADDING, lowerBackground.right, lowerBackground.bottom); + + // Add ViewZone if needed + const shouldShowViewZone = this._editor.editor.getOption(EditorOption.inlineSuggest).edits.codeShifting; + if (shouldShowViewZone) { + const viewZoneHeight = lowerBackground.height + 2 * PADDING; + const viewZoneLineNumber = this._originalRange.endLineNumberExclusive; + const activeViewZone = this._viewZoneInfo.get(); + if (!activeViewZone || activeViewZone.lineNumber !== viewZoneLineNumber || activeViewZone.height !== viewZoneHeight) { + this._viewZoneInfo.set({ height: viewZoneHeight, lineNumber: viewZoneLineNumber }, undefined); + } + } else if (this._viewZoneInfo.get()) { + this._viewZoneInfo.set(undefined, undefined); + } + + return { + originalLinesOverlay, + modifiedLinesOverlay, + background, + lowerBackground, + lowerText, + padding: PADDING, + minContentWidthRequired: maxLineWidth + PADDING * 2, + }; + }); + + private _previousViewZoneInfo: { height: number; lineNumber: number; id: string } | undefined = undefined; + protected readonly _viewZone = derived(this, reader => { + const viewZoneInfo = this._viewZoneInfo.read(reader); + this._editor.editor.changeViewZones((changeAccessor) => { + this.removePreviousViewZone(changeAccessor); + if (!viewZoneInfo) { return; } + this.addViewZone(viewZoneInfo, changeAccessor); + }); + }).recomputeInitiallyAndOnChange(this._store); + + private removePreviousViewZone(changeAccessor: IViewZoneChangeAccessor) { + if (!this._previousViewZoneInfo) { + return; + } + + changeAccessor.removeZone(this._previousViewZoneInfo.id); + + const cursorLineNumber = this._editor.cursorLineNumber.get(); + if (cursorLineNumber !== null && cursorLineNumber >= this._previousViewZoneInfo.lineNumber) { + this._editor.editor.setScrollTop(this._editor.scrollTop.get() - this._previousViewZoneInfo.height); + } + + this._previousViewZoneInfo = undefined; + } + + private addViewZone(viewZoneInfo: { height: number; lineNumber: number }, changeAccessor: IViewZoneChangeAccessor) { + const activeViewZone = changeAccessor.addZone({ + afterLineNumber: viewZoneInfo.lineNumber - 1, + heightInPx: viewZoneInfo.height, // move computation to layout? + domNode: $('div'), + }); + + const cursorLineNumber = this._editor.cursorLineNumber.get(); + if (cursorLineNumber !== null && cursorLineNumber >= viewZoneInfo.lineNumber) { + this._editor.editor.setScrollTop(this._editor.scrollTop.get() + viewZoneInfo.height); + } + + this._previousViewZoneInfo = { height: viewZoneInfo.height, lineNumber: viewZoneInfo.lineNumber, id: activeViewZone }; + } + + private readonly _div = n.div({ + class: 'line-replacement', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + if (!layout) { + return []; + } + + const layoutProps = layout.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + let contentLeft = this._editor.layoutInfoContentLeft.read(reader); + let contentWidth = this._editor.contentWidth.read(reader); + const contentHeight = this._editor.editor.getContentHeight(); + + if (scrollLeft === 0) { + contentLeft -= layoutProps.padding; + contentWidth += layoutProps.padding; + } + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + const modifiedLines = this._modifiedLineElements.read(reader).lines; + modifiedLines.forEach(l => { + l.style.width = `${layout.read(reader).lowerText.width}px`; + l.style.height = `${lineHeight}px`; + l.style.position = 'relative'; + }); + + return [ + n.div({ style: { position: 'absolute', - left: derived(reader => layout.read(reader).modified.left - 15), - top: derived(reader => layout.read(reader).modified.top), + top: 0, + left: contentLeft, + width: contentWidth, + height: contentHeight, + overflow: 'hidden', + pointerEvents: 'none', } }, [ - n.svgElem('path', { - d: 'M1 0C1 2.98966 1 4.92087 1 7.49952C1 8.60409 1.89543 9.5 3 9.5H10.5', - stroke: 'var(--vscode-editorHoverWidget-foreground)', + n.div({ // overlay to make sure the code is not visible between original and modified lines + style: { + position: 'absolute', + top: layoutProps.lowerBackground.top - layoutProps.padding, + left: layoutProps.lowerBackground.left - contentLeft, + width: layoutProps.lowerBackground.width, + height: layoutProps.padding * 2, + background: 'var(--vscode-editor-background)', + }, }), - n.svgElem('path', { - d: 'M6 6.5L9.99999 9.49998L6 12.5', - stroke: 'var(--vscode-editorHoverWidget-foreground)', - }) - ]), + n.div({ // styling for the modified lines widget + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground.moveLeft(contentLeft)), + borderRadius: '4px', + background: 'var(--vscode-editor-background)', + boxShadow: 'var(--vscode-scrollbar-shadow) 0 6px 6px -6px', + borderTop: '1px solid var(--vscode-editorHoverWidget-border)', + overflow: 'hidden', + }, + }, [ + n.div({ // adds background color to modified lines widget (may be transparent) + style: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + background: 'var(--vscode-inlineEdit-modifiedChangedLineBackground)', + }, + }) + ]), + n.div({ + style: { + position: 'absolute', + padding: '0px', + boxSizing: 'border-box', + ...rectToProps(reader => layout.read(reader).lowerText.moveLeft(contentLeft)), + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + pointerEvents: 'none', + } + }, [...modifiedLines]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background.moveLeft(contentLeft)), + borderRadius: '4px', + border: '1px solid var(--vscode-editorHoverWidget-border)', + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + pointerEvents: 'none', + boxSizing: 'border-box', + } + }, []), + ]) ]; }) ]).keepUpdated(this._store); + readonly isHovered = derived(this, reader => { + return this._div.getIsHovered(this._store).read(reader); + }); + constructor( private readonly _editor: ObservableCodeEditor, - /** Must be single-line in both sides */ - private readonly _edit: SingleTextEdit, + private readonly _originalRange: LineRange, + private readonly _modifiedRange: LineRange, + private readonly _modifiedLines: string[], + private readonly _replacements: readonly Replacement[], @ILanguageService private readonly _languageService: ILanguageService, ) { super(); + this._register(toDisposable(() => this._originalBubblesDecorationCollection.clear())); + this._register(toDisposable(() => this._editor.editor.changeViewZones(accessor => this.removePreviousViewZone(accessor)))); + + const originalBubbles = rangesToBubbleRanges(this._replacements.map(r => r.originalRange)); + this._originalBubblesDecorationCollection.set(originalBubbles.map(r => ({ range: r, options: this._originalBubblesDecorationOptions }))); + this._register(this._editor.createOverlayWidget({ domNode: this._div.element, - minContentWidthInPx: constObservable(0), + minContentWidthInPx: derived(reader => { // TODO: is this helping? + return this._layout.read(reader)?.minContentWidthRequired ?? 0; + }), position: constObservable({ preference: { top: 0, left: 0 } }), allowEditorOverflow: false, })); } } -export class WordInsertView extends Disposable { +function rangesToBubbleRanges(ranges: Range[]): Range[] { + const result: Range[] = []; + while (ranges.length) { + let range = ranges.shift()!; + if (range.startLineNumber !== range.endLineNumber) { + ranges.push(new Range(range.startLineNumber + 1, 1, range.endLineNumber, range.endColumn)); + range = new Range(range.startLineNumber, range.startColumn, range.startLineNumber, Number.MAX_SAFE_INTEGER); // TODO: this is not correct + } + + result.push(range); + } + return result; + +} + +export interface Replacement { + originalRange: Range; + modifiedRange: Range; +} + +export class WordInsertView extends Disposable implements IInlineEditsView { private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); private readonly _layout = derived(this, reader => { @@ -268,6 +665,8 @@ export class WordInsertView extends Disposable { }) ]).keepUpdated(this._store); + readonly isHovered = constObservable(false); + constructor( private readonly _editor: ObservableCodeEditor, /** Must be single-line in both sides */ diff --git a/code/src/vs/editor/contrib/links/browser/getLinks.ts b/code/src/vs/editor/contrib/links/browser/getLinks.ts index cd2e19c756e..be8a31af119 100644 --- a/code/src/vs/editor/contrib/links/browser/getLinks.ts +++ b/code/src/vs/editor/contrib/links/browser/getLinks.ts @@ -70,6 +70,8 @@ export class Link implements ILink { export class LinksList { + static readonly Empty = new LinksList([]); + readonly links: Link[]; private readonly _disposables = new DisposableStore(); @@ -137,27 +139,31 @@ export class LinksList { } -export function getLinks(providers: LanguageFeatureRegistry, model: ITextModel, token: CancellationToken): Promise { - +export async function getLinks(providers: LanguageFeatureRegistry, model: ITextModel, token: CancellationToken): Promise { const lists: [ILinksList, LinkProvider][] = []; // ask all providers for links in parallel - const promises = providers.ordered(model).reverse().map((provider, i) => { - return Promise.resolve(provider.provideLinks(model, token)).then(result => { + const promises = providers.ordered(model).reverse().map(async (provider, i) => { + try { + const result = await provider.provideLinks(model, token); if (result) { lists[i] = [result, provider]; } - }, onUnexpectedExternalError); - }); - - return Promise.all(promises).then(() => { - const result = new LinksList(coalesce(lists)); - if (!token.isCancellationRequested) { - return result; + } catch (err) { + onUnexpectedExternalError(err); } - result.dispose(); - return new LinksList([]); }); + + await Promise.all(promises); + + let res = new LinksList(coalesce(lists)); + + if (token.isCancellationRequested) { + res.dispose(); + res = LinksList.Empty; + } + + return res; } diff --git a/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index e1bf843a994..0ad604159e1 100644 --- a/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -570,7 +570,7 @@ export class SuggestWidget implements IDisposable { try { this._list.splice(0, this._list.length, this._completionModel.items); this._setState(isFrozen ? State.Frozen : State.Open); - this._list.reveal(selectionIndex, 0); + this._list.reveal(selectionIndex, 0, selectionIndex === 0 ? 0 : this.getLayoutInfo().itemHeight * 0.33); this._list.setFocus(noFocus ? [] : [selectionIndex]); } finally { this._onDidFocus.resume(); diff --git a/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index d46bbff59c3..91f910e3776 100644 --- a/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -156,6 +156,10 @@ export class StandaloneQuickInputService implements IQuickInputService { cancel(): Promise { return this.activeService.cancel(); } + + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { + return this.activeService.setAlignment(alignment); + } } export class QuickInputEditorContribution implements IEditorContribution { diff --git a/code/src/vs/editor/standalone/browser/standaloneServices.ts b/code/src/vs/editor/standalone/browser/standaloneServices.ts index 0e6637019c1..a19d7603763 100644 --- a/code/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/code/src/vs/editor/standalone/browser/standaloneServices.ts @@ -712,7 +712,8 @@ export class StandaloneConfigurationService implements IConfigurationService { defaults: emptyModel, policy: emptyModel, application: emptyModel, - user: emptyModel, + userLocal: emptyModel, + userRemote: emptyModel, workspace: emptyModel, folders: [] }; diff --git a/code/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts b/code/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts index be9b21fbdd9..20a2f082f3c 100644 --- a/code/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts +++ b/code/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts @@ -6,21 +6,20 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { Event } from '../../../base/common/event.js'; import { ITextModel } from '../../common/model.js'; -import { ITextModelTreeSitter, ITreeSitterParseResult, ITreeSitterParserService } from '../../common/services/treeSitterParserService.js'; -import { Range } from '../../common/core/range.js'; +import { ITextModelTreeSitter, ITreeSitterParseResult, ITreeSitterParserService, TreeUpdateEvent } from '../../common/services/treeSitterParserService.js'; /** * The monaco build doesn't like the dynamic import of tree sitter in the real service. * We use a dummy sertive here to make the build happy. */ export class StandaloneTreeSitterParserService implements ITreeSitterParserService { - getTextModelTreeSitter(textModel: ITextModel): ITextModelTreeSitter | undefined { + async getTextModelTreeSitter(model: ITextModel, parseImmediately?: boolean): Promise { return undefined; } async getTree(content: string, languageId: string): Promise { return undefined; } - onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }> = Event.None; + onDidUpdateTree: Event = Event.None; readonly _serviceBrand: undefined; onDidAddLanguage: Event<{ id: string; language: Parser.Language }> = Event.None; diff --git a/code/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts b/code/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index 5b2c44367c0..2b0e55aab2c 100644 --- a/code/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts +++ b/code/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -17,13 +17,13 @@ const blackInt = 0x000000FF; const nullCharMetadata = 0x0; let lastUniqueGlyph: string | undefined; -function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number] { +function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number, x: number] { if (!lastUniqueGlyph) { lastUniqueGlyph = 'a'; } else { lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); } - return [lastUniqueGlyph, blackInt, nullCharMetadata]; + return [lastUniqueGlyph, blackInt, nullCharMetadata, 0]; } class TestGlyphRasterizer implements IGlyphRasterizer { @@ -60,6 +60,9 @@ class TestGlyphRasterizer implements IGlyphRasterizer { fontBoundingBoxDescent: 0, }; } + getTextMetrics(text: string): TextMetrics { + return null!; + } } suite('TextureAtlas', () => { diff --git a/code/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts b/code/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts index 5828d479ab7..c36d8ddadc1 100644 --- a/code/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts +++ b/code/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts @@ -5,7 +5,7 @@ import { deepStrictEqual } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { DecorationCssRuleExtractor } from '../../../browser/gpu/decorationCssRuleExtractor.js'; +import { DecorationCssRuleExtractor } from '../../../browser/gpu/css/decorationCssRuleExtractor.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { $, getActiveDocument } from '../../../../base/browser/dom.js'; diff --git a/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts index d90cca2af99..5475195b7db 100644 --- a/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts +++ b/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from '../../../browser/services/treeSitter/treeSitterParserService.js'; +import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from '../../../common/services/treeSitter/treeSitterParserService.js'; import type { Parser } from '@vscode/tree-sitter-wasm'; import { createTextModel } from '../../common/testTextModel.js'; import { timeout } from '../../../../base/common/async.js'; diff --git a/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts index e8406d868f8..7bb14f13871 100644 --- a/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts +++ b/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -86,7 +86,7 @@ suite("CodeEditorWidget", () => { 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', - "running derived: selection: [1,4 -> 1,4], value: 4", + 'running derived: selection: [1,4 -> 1,4], value: 4', ]); })); @@ -100,7 +100,7 @@ suite("CodeEditorWidget", () => { 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', - "running derived: selection: [1,4 -> 1,4], value: 4", + 'running derived: selection: [1,4 -> 1,4], value: 4', ]); editor.setPosition(new Position(1, 5), "test"); diff --git a/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts b/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts index b3e6c91be13..019344128bc 100644 --- a/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts +++ b/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts @@ -3,16 +3,98 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestDecoder } from '../utils/testDecoder.js'; +import assert from 'assert'; import { Range } from '../../../common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { newWriteableStream } from '../../../../base/common/stream.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { Line } from '../../../common/codecs/linesCodec/tokens/line.js'; +import { TestDecoder, TTokensConsumeMethod } from '../utils/testDecoder.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { newWriteableStream, WriteableStream } from '../../../../base/common/stream.js'; import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; import { LinesDecoder, TLineToken } from '../../../common/codecs/linesCodec/linesDecoder.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +/** + * Note! This decoder is also often used to test common logic of abstract {@linkcode BaseDecoder} + * class, because the {@linkcode LinesDecoder} is one of the simplest non-abstract decoders we have. + */ +suite('LinesDecoder', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Test the core logic with specific method of consuming + * tokens that are produced by a lines decoder instance. + */ + suite('core logic', () => { + testLinesDecoder('async-generator', disposables); + testLinesDecoder('consume-all-method', disposables); + testLinesDecoder('on-data-event', disposables); + }); + + suite('settled promise', () => { + test('throws if accessed on not-yet-started decoder instance', () => { + const test = disposables.add(new TestLinesDecoder()); + + assert.throws( + () => { + // testing the field access that throws here, so + // its OK to not use the returned value afterwards + // eslint-disable-next-line local/code-no-unused-expressions + test.decoder.settled; + }, + [ + 'Cannot get `settled` promise of a stream that has not been started.', + 'Please call `start()` first.', + ].join(' '), + ); + }); + }); + + suite('start', () => { + test('throws if the decoder object is already `disposed`', () => { + const test = disposables.add(new TestLinesDecoder()); + const { decoder } = test; + decoder.dispose(); + + assert.throws( + decoder.start.bind(decoder), + 'Cannot start stream that has already disposed.', + ); + }); + + test('throws if the decoder object is already `ended`', async () => { + const inputStream = newWriteableStream(null); + const test = disposables.add(new TestLinesDecoder(inputStream)); + const { decoder } = test; + + setTimeout(() => { + test.sendData([ + 'hello', + 'world :wave:', + ]); + }, 5); + + const receivedTokens = await decoder.start() + .consumeAll(); + + // a basic sanity check for received tokens + assert.strictEqual( + receivedTokens.length, + 3, + 'Must produce the correct number of tokens.', + ); + + // validate that calling `start()` after stream has ended throws + assert.throws( + decoder.start.bind(decoder), + 'Cannot start stream that has already ended.', + ); + }); + }); +}); + + /** * A reusable test utility that asserts that a `LinesDecoder` instance * correctly decodes `inputData` into a stream of `TLineToken` tokens. @@ -21,7 +103,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c * * ```typescript * // create a new test utility instance - * const test = testDisposables.add(new TestLinesDecoder()); + * const test = disposables.add(new TestLinesDecoder()); * * // run the test * await test.run( @@ -33,108 +115,124 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c * ); */ export class TestLinesDecoder extends TestDecoder { - constructor() { - const stream = newWriteableStream(null); + constructor( + inputStream?: WriteableStream, + ) { + const stream = (inputStream) + ? inputStream + : newWriteableStream(null); + const decoder = new LinesDecoder(stream); super(stream, decoder); } } -suite('LinesDecoder', () => { - suite('produces expected tokens', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); +/** + * Common reusable test utility to validate {@linkcode LinesDecoder} logic with + * the provided {@linkcode tokensConsumeMethod} way of consuming decoder-produced tokens. + * + * @throws if a test fails, please see thrown error for failure details. + * @param tokensConsumeMethod The way to consume tokens produced by the decoder. + * @param disposables Test disposables store. + */ +function testLinesDecoder( + tokensConsumeMethod: TTokensConsumeMethod, + disposables: Pick, +) { + suite(tokensConsumeMethod, () => { + suite('produces expected tokens', () => { + test('input starts with line data', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('input starts with line data', async () => { - const test = testDisposables.add(new TestLinesDecoder()); + await test.run( + ' hello world\nhow are you doing?\n\n 😊 \r ', + [ + new Line(1, ' hello world'), + new NewLine(new Range(1, 13, 1, 14)), + new Line(2, 'how are you doing?'), + new NewLine(new Range(2, 19, 2, 20)), + new Line(3, ''), + new NewLine(new Range(3, 1, 3, 2)), + new Line(4, ' 😊 '), + new CarriageReturn(new Range(4, 5, 4, 6)), + new Line(5, ' '), + ], + ); + }); - await test.run( - ' hello world\nhow are you doing?\n\n 😊 \r ', - [ - new Line(1, ' hello world'), - new NewLine(new Range(1, 13, 1, 14)), - new Line(2, 'how are you doing?'), - new NewLine(new Range(2, 19, 2, 20)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ' 😊 '), - new CarriageReturn(new Range(4, 5, 4, 6)), - new Line(5, ' '), - ], - ); - }); + test('input starts with a new line', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('input starts with a new line', async () => { - const test = testDisposables.add(new TestLinesDecoder()); + await test.run( + '\nsome text on this line\n\n\nanother 💬 on this line\r\n🤫\n', + [ + new Line(1, ''), + new NewLine(new Range(1, 1, 1, 2)), + new Line(2, 'some text on this line'), + new NewLine(new Range(2, 23, 2, 24)), + new Line(3, ''), + new NewLine(new Range(3, 1, 3, 2)), + new Line(4, ''), + new NewLine(new Range(4, 1, 4, 2)), + new Line(5, 'another 💬 on this line'), + new CarriageReturn(new Range(5, 24, 5, 25)), + new NewLine(new Range(5, 25, 5, 26)), + new Line(6, '🤫'), + new NewLine(new Range(6, 3, 6, 4)), + ], + ); + }); - await test.run( - '\nsome text on this line\n\n\nanother 💬 on this line\r\n🤫\n', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, 'some text on this line'), - new NewLine(new Range(2, 23, 2, 24)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ''), - new NewLine(new Range(4, 1, 4, 2)), - new Line(5, 'another 💬 on this line'), - new CarriageReturn(new Range(5, 24, 5, 25)), - new NewLine(new Range(5, 25, 5, 26)), - new Line(6, '🤫'), - new NewLine(new Range(6, 3, 6, 4)), - ], - ); - }); + test('input starts and ends with multiple new lines', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('input starts and ends with multiple new lines', async () => { - const test = testDisposables.add(new TestLinesDecoder()); + await test.run( + '\n\n\r\nciao! 🗯️\t💭 💥 come\tva?\n\n\n\n\n', + [ + new Line(1, ''), + new NewLine(new Range(1, 1, 1, 2)), + new Line(2, ''), + new NewLine(new Range(2, 1, 2, 2)), + new Line(3, ''), + new CarriageReturn(new Range(3, 1, 3, 2)), + new NewLine(new Range(3, 2, 3, 3)), + new Line(4, 'ciao! 🗯️\t💭 💥 come\tva?'), + new NewLine(new Range(4, 25, 4, 26)), + new Line(5, ''), + new NewLine(new Range(5, 1, 5, 2)), + new Line(6, ''), + new NewLine(new Range(6, 1, 6, 2)), + new Line(7, ''), + new NewLine(new Range(7, 1, 7, 2)), + new Line(8, ''), + new NewLine(new Range(8, 1, 8, 2)), + ], + ); + }); - await test.run( - '\n\n\r\nciao! 🗯️\t💭 💥 come\tva?\n\n\n\n\n', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, ''), - new NewLine(new Range(2, 1, 2, 2)), - new Line(3, ''), - new CarriageReturn(new Range(3, 1, 3, 2)), - new NewLine(new Range(3, 2, 3, 3)), - new Line(4, 'ciao! 🗯️\t💭 💥 come\tva?'), - new NewLine(new Range(4, 25, 4, 26)), - new Line(5, ''), - new NewLine(new Range(5, 1, 5, 2)), - new Line(6, ''), - new NewLine(new Range(6, 1, 6, 2)), - new Line(7, ''), - new NewLine(new Range(7, 1, 7, 2)), - new Line(8, ''), - new NewLine(new Range(8, 1, 8, 2)), - ], - ); - }); + test('single carriage return is treated as new line', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('single carriage return is treated as new line', async () => { - const test = testDisposables.add(new TestLinesDecoder()); - - await test.run( - '\r\rhaalo! 💥💥 how\'re you?\r ?!\r\n\r\n ', - [ - new Line(1, ''), - new CarriageReturn(new Range(1, 1, 1, 2)), - new Line(2, ''), - new CarriageReturn(new Range(2, 1, 2, 2)), - new Line(3, 'haalo! 💥💥 how\'re you?'), - new CarriageReturn(new Range(3, 24, 3, 25)), - new Line(4, ' ?!'), - new CarriageReturn(new Range(4, 4, 4, 5)), - new NewLine(new Range(4, 5, 4, 6)), - new Line(5, ''), - new CarriageReturn(new Range(5, 1, 5, 2)), - new NewLine(new Range(5, 2, 5, 3)), - new Line(6, ' '), - ], - ); + await test.run( + '\r\rhaalo! 💥💥 how\'re you?\r ?!\r\n\r\n ', + [ + new Line(1, ''), + new CarriageReturn(new Range(1, 1, 1, 2)), + new Line(2, ''), + new CarriageReturn(new Range(2, 1, 2, 2)), + new Line(3, 'haalo! 💥💥 how\'re you?'), + new CarriageReturn(new Range(3, 24, 3, 25)), + new Line(4, ' ?!'), + new CarriageReturn(new Range(4, 4, 4, 5)), + new NewLine(new Range(4, 5, 4, 6)), + new Line(5, ''), + new CarriageReturn(new Range(5, 1, 5, 2)), + new NewLine(new Range(5, 2, 5, 3)), + new Line(6, ' '), + ], + ); + }); }); }); -}); +} diff --git a/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts b/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts new file mode 100644 index 00000000000..bff4b428ae1 --- /dev/null +++ b/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestDecoder } from '../utils/testDecoder.js'; +import { Range } from '../../../common/core/range.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { newWriteableStream } from '../../../../base/common/stream.js'; +import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; +import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; +import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; +import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalTab.js'; +import { MarkdownLink } from '../../../common/codecs/markdownCodec/tokens/markdownLink.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { MarkdownDecoder, TMarkdownToken } from '../../../common/codecs/markdownCodec/markdownDecoder.js'; +import { FormFeed } from '../../../common/codecs/simpleCodec/tokens/formFeed.js'; +import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; +import assert from 'assert'; + +/** + * A reusable test utility that asserts that a `TestMarkdownDecoder` instance + * correctly decodes `inputData` into a stream of `TMarkdownToken` tokens. + * + * ## Examples + * + * ```typescript + * // create a new test utility instance + * const test = testDisposables.add(new TestMarkdownDecoder()); + * + * // run the test + * await test.run( + * ' hello [world](/etc/hosts)!', + * [ + * new Space(new Range(1, 1, 1, 2)), + * new Word(new Range(1, 2, 1, 7), 'hello'), + * new Space(new Range(1, 7, 1, 8)), + * new MarkdownLink(1, 8, '[world]', '(/etc/hosts)'), + * new Word(new Range(1, 27, 1, 28), '!'), + * new NewLine(new Range(1, 28, 1, 29)), + * ], + * ); + */ +export class TestMarkdownDecoder extends TestDecoder { + constructor() { + const stream = newWriteableStream(null); + + super(stream, new MarkdownDecoder(stream)); + } +} + +suite('MarkdownDecoder', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('produces expected tokens', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + await test.run( + ' hello world\nhow are\t you [caption text](./some/file/path/refer🎨nce.md)?\v\n\n[(example)](another/path/with[-and-]-chars/folder)\t \n\t[#file:something.txt](/absolute/path/to/something.txt)', + [ + // first line + new Space(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 7), 'hello'), + new Space(new Range(1, 7, 1, 8)), + new Word(new Range(1, 8, 1, 13), 'world'), + new NewLine(new Range(1, 13, 1, 14)), + // second line + new Word(new Range(2, 1, 2, 4), 'how'), + new Space(new Range(2, 4, 2, 5)), + new Word(new Range(2, 5, 2, 8), 'are'), + new Tab(new Range(2, 8, 2, 9)), + new Space(new Range(2, 9, 2, 10)), + new Word(new Range(2, 10, 2, 13), 'you'), + new Space(new Range(2, 13, 2, 14)), + new MarkdownLink(2, 14, '[caption text]', '(./some/file/path/refer🎨nce.md)'), + new Word(new Range(2, 60, 2, 61), '?'), + new VerticalTab(new Range(2, 61, 2, 62)), + new NewLine(new Range(2, 62, 2, 63)), + // third line + new NewLine(new Range(3, 1, 3, 2)), + // fourth line + new MarkdownLink(4, 1, '[(example)]', '(another/path/with[-and-]-chars/folder)'), + new Tab(new Range(4, 51, 4, 52)), + new Space(new Range(4, 52, 4, 53)), + new NewLine(new Range(4, 53, 4, 54)), + // fifth line + new Tab(new Range(5, 1, 5, 2)), + new MarkdownLink(5, 2, '[#file:something.txt]', '(/absolute/path/to/something.txt)'), + ], + ); + }); + + test('handles complex cases', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // tests that the link caption contain a chat prompt `#file:` reference, while + // the file path can contain other `graphical characters` + '\v\t[#file:./another/path/to/file.txt](./real/filepath/file◆name.md)', + // tests that the link file path contain a chat prompt `#file:` reference, + // `spaces`, `emojies`, and other `graphical characters` + ' [reference ∘ label](/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)', + // tests that link caption and file path can contain `parentheses`, `spaces`, and + // `emojies` + '\f[!(hello)!](./w(())rld/nice-🦚-filen(a)me.git))\n\t', + // tests that the link caption can be empty, while the file path can contain `square brackets` + '[](./s[]me/pa[h!) ', + ]; + + await test.run( + inputLines, + [ + // `1st` line + new VerticalTab(new Range(1, 1, 1, 2)), + new Tab(new Range(1, 2, 1, 3)), + new MarkdownLink(1, 3, '[#file:./another/path/to/file.txt]', '(./real/filepath/file◆name.md)'), + new NewLine(new Range(1, 67, 1, 68)), + // `2nd` line + new Space(new Range(2, 1, 2, 2)), + new MarkdownLink(2, 2, '[reference ∘ label]', '(/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)'), + new NewLine(new Range(2, 67, 2, 68)), + // `3rd` line + new FormFeed(new Range(3, 1, 3, 2)), + new MarkdownLink(3, 2, '[!(hello)!]', '(./w(())rld/nice-🦚-filen(a)me.git)'), + new RightParenthesis(new Range(3, 48, 3, 49)), + new NewLine(new Range(3, 49, 3, 50)), + // `4th` line + new Tab(new Range(4, 1, 4, 2)), + new NewLine(new Range(4, 2, 4, 3)), + // `5th` line + new MarkdownLink(5, 1, '[]', '(./s[]me/pa[h!)'), + new Space(new Range(5, 18, 5, 19)), + ], + ); + }); + + suite('broken links', () => { + test('incomplete/invalid links', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // incomplete link reference with empty caption + '[ ](./real/file path/file⇧name.md', + // space between caption and reference is disallowed + '[link text] (./file path/name.txt)', + ]; + + await test.run( + inputLines, + [ + // `1st` line + new LeftBracket(new Range(1, 1, 1, 2)), + new Space(new Range(1, 2, 1, 3)), + new RightBracket(new Range(1, 3, 1, 4)), + new LeftParenthesis(new Range(1, 4, 1, 5)), + new Word(new Range(1, 5, 1, 5 + 11), './real/file'), + new Space(new Range(1, 16, 1, 17)), + new Word(new Range(1, 17, 1, 17 + 17), 'path/file⇧name.md'), + new NewLine(new Range(1, 34, 1, 35)), + // `2nd` line + new LeftBracket(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 4), 'link'), + new Space(new Range(2, 6, 2, 7)), + new Word(new Range(2, 7, 2, 7 + 4), 'text'), + new RightBracket(new Range(2, 11, 2, 12)), + new Space(new Range(2, 12, 2, 13)), + new LeftParenthesis(new Range(2, 13, 2, 14)), + new Word(new Range(2, 14, 2, 14 + 6), './file'), + new Space(new Range(2, 20, 2, 21)), + new Word(new Range(2, 21, 2, 21 + 13), 'path/name.txt'), + new RightParenthesis(new Range(2, 34, 2, 35)), + ], + ); + }); + + suite('stop characters inside caption/reference (new lines)', () => { + for (const stopCharacter of [CarriageReturn, NewLine]) { + let characterName = ''; + + if (stopCharacter === CarriageReturn) { + characterName = '\\r'; + } + if (stopCharacter === NewLine) { + characterName = '\\n'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', + ); + + test(`stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + // stop character inside link reference + `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + // stop character between line caption and link reference is disallowed + `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + ]; + + + await test.run( + inputLines, + [ + // `1st` input line + new LeftBracket(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 2 + 3), 'haa'), + new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new Word(new Range(2, 1, 2, 1 + 3), 'loů'), + new RightBracket(new Range(2, 4, 2, 5)), + new LeftParenthesis(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.txt'), + new RightParenthesis(new Range(2, 24, 2, 25)), + new NewLine(new Range(2, 25, 2, 26)), + // `2nd` input line + new LeftBracket(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 2 + 3), 'ref'), + new Space(new Range(3, 5, 3, 6)), + new Word(new Range(3, 6, 3, 6 + 4), 'text'), + new RightBracket(new Range(3, 10, 3, 11)), + new LeftParenthesis(new Range(3, 11, 3, 12)), + new Word(new Range(3, 12, 3, 12 + 8), '/etc/pat'), + new stopCharacter(new Range(3, 20, 3, 21)), // <- stop character + new Word(new Range(4, 1, 4, 1 + 12), 'h/to/file.md'), + new RightParenthesis(new Range(4, 13, 4, 14)), + new NewLine(new Range(4, 14, 4, 15)), + // `3nd` input line + new LeftBracket(new Range(5, 1, 5, 2)), + new Word(new Range(5, 2, 5, 2 + 4), 'text'), + new RightBracket(new Range(5, 6, 5, 7)), + new stopCharacter(new Range(5, 7, 5, 8)), // <- stop character + new LeftParenthesis(new Range(6, 1, 6, 2)), + new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), + new Space(new Range(6, 7, 6, 8)), + new Word(new Range(6, 8, 6, 8 + 12), 'path/file.md'), + new RightParenthesis(new Range(6, 20, 6, 21)), + ], + ); + }); + } + }); + + /** + * Same as above but these stop characters do not move the caret to the next line. + */ + suite('stop characters inside caption/reference (same line)', () => { + for (const stopCharacter of [VerticalTab, FormFeed]) { + let characterName = ''; + + if (stopCharacter === VerticalTab) { + characterName = '\\v'; + } + if (stopCharacter === FormFeed) { + characterName = '\\f'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', + ); + + test(`stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + // stop character inside link reference + `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + // stop character between line caption and link reference is disallowed + `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + ]; + + + await test.run( + inputLines, + [ + // `1st` input line + new LeftBracket(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 2 + 3), 'haa'), + new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new Word(new Range(1, 6, 1, 6 + 3), 'loů'), + new RightBracket(new Range(1, 9, 1, 10)), + new LeftParenthesis(new Range(1, 10, 1, 11)), + new Word(new Range(1, 11, 1, 11 + 18), './real/💁/name.txt'), + new RightParenthesis(new Range(1, 29, 1, 30)), + new NewLine(new Range(1, 30, 1, 31)), + // `2nd` input line + new LeftBracket(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 3), 'ref'), + new Space(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 4), 'text'), + new RightBracket(new Range(2, 10, 2, 11)), + new LeftParenthesis(new Range(2, 11, 2, 12)), + new Word(new Range(2, 12, 2, 12 + 8), '/etc/pat'), + new stopCharacter(new Range(2, 20, 2, 21)), // <- stop character + new Word(new Range(2, 21, 2, 21 + 12), 'h/to/file.md'), + new RightParenthesis(new Range(2, 33, 2, 34)), + new NewLine(new Range(2, 34, 2, 35)), + // `3nd` input line + new LeftBracket(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 2 + 4), 'text'), + new RightBracket(new Range(3, 6, 3, 7)), + new stopCharacter(new Range(3, 7, 3, 8)), // <- stop character + new LeftParenthesis(new Range(3, 8, 3, 9)), + new Word(new Range(3, 9, 3, 9 + 5), '/etc/'), + new Space(new Range(3, 14, 3, 15)), + new Word(new Range(3, 15, 3, 15 + 12), 'path/file.md'), + new RightParenthesis(new Range(3, 27, 3, 28)), + ], + ); + }); + } + }); + }); +}); diff --git a/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts b/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts index 2e57a1c8219..b0804a2fe5f 100644 --- a/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts +++ b/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts @@ -8,6 +8,7 @@ import { Range } from '../../../common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { newWriteableStream } from '../../../../base/common/stream.js'; import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; +import { Hash } from '../../../common/codecs/simpleCodec/tokens/hash.js'; import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; @@ -16,6 +17,8 @@ import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalT import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { SimpleDecoder, TSimpleToken } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; /** * A reusable test utility that asserts that a `SimpleDecoder` instance @@ -57,7 +60,7 @@ suite('SimpleDecoder', () => { ); await test.run( - ' hello world\nhow are\t you?\v\n\n (test) [!@#$%^&*_+=]\f \n\t\t🤗❤ \t\n hey\vthere\r\n\r\n', + ' hello world\nhow are\t you?\v\n\n (test) [!@#$%^🦄&*_+=]\f \n\t\t🤗❤ \t\n hey\vthere\r\n\r\n', [ // first line new Space(new Range(1, 1, 1, 2)), @@ -80,14 +83,20 @@ suite('SimpleDecoder', () => { new Space(new Range(4, 1, 4, 2)), new Space(new Range(4, 2, 4, 3)), new Space(new Range(4, 3, 4, 4)), - new Word(new Range(4, 4, 4, 10), '(test)'), + new LeftParenthesis(new Range(4, 4, 4, 5)), + new Word(new Range(4, 5, 4, 5 + 4), 'test'), + new RightParenthesis(new Range(4, 9, 4, 10)), new Space(new Range(4, 10, 4, 11)), new Space(new Range(4, 11, 4, 12)), - new Word(new Range(4, 12, 4, 25), '[!@#$%^&*_+=]'), - new FormFeed(new Range(4, 25, 4, 26)), - new Space(new Range(4, 26, 4, 27)), - new Space(new Range(4, 27, 4, 28)), - new NewLine(new Range(4, 28, 4, 29)), + new LeftBracket(new Range(4, 12, 4, 13)), + new Word(new Range(4, 13, 4, 13 + 2), '!@'), + new Hash(new Range(4, 15, 4, 16)), + new Word(new Range(4, 16, 4, 16 + 10), '$%^🦄&*_+='), + new RightBracket(new Range(4, 26, 4, 27)), + new FormFeed(new Range(4, 27, 4, 28)), + new Space(new Range(4, 28, 4, 29)), + new Space(new Range(4, 29, 4, 30)), + new NewLine(new Range(4, 30, 4, 31)), // fifth line new Tab(new Range(5, 1, 5, 2)), new Tab(new Range(5, 2, 5, 3)), diff --git a/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts index e44b41bd6d6..3cf6744390a 100644 --- a/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts +++ b/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -52,7 +52,7 @@ suite('PositionOffsetTransformer', () => { }); test('getOffset', () => { - for (let i = 0; i < str.length + 2; i++) { + for (let i = 0; i < str.length + 1; i++) { assert.strictEqual(t.getOffset(t.getPosition(i)), i); } }); diff --git a/code/src/vs/editor/test/common/model/tokenStore.test.ts b/code/src/vs/editor/test/common/model/tokenStore.test.ts new file mode 100644 index 00000000000..328fff4be8b --- /dev/null +++ b/code/src/vs/editor/test/common/model/tokenStore.test.ts @@ -0,0 +1,730 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TextModel } from '../../../common/model/textModel.js'; +import { TokenStore } from '../../../common/model/tokenStore.js'; + +suite('TokenStore', () => { + let textModel: TextModel; + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + textModel = { + getValueLength: () => 11 + } as TextModel; + }); + + test('constructs with empty model', () => { + const store = new TokenStore(textModel); + assert.ok(store.root); + assert.strictEqual(store.root.length, textModel.getValueLength()); + }); + + test('builds store with single token', () => { + const store = new TokenStore(textModel); + store.buildStore([{ + startOffsetInclusive: 0, + length: 5, + token: 1 + }]); + assert.strictEqual(store.root.length, 5); + }); + + test('builds store with multiple tokens', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 4, token: 3 } + ]); + assert.ok(store.root); + assert.strictEqual(store.root.length, 10); + }); + + test('creates balanced tree structure', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 2, token: 1 }, + { startOffsetInclusive: 2, length: 2, token: 2 }, + { startOffsetInclusive: 4, length: 2, token: 3 }, + { startOffsetInclusive: 6, length: 2, token: 4 } + ]); + + const root = store.root as any; + assert.ok(root.children); + assert.strictEqual(root.children.length, 2); + assert.strictEqual(root.children[0].length, 4); + assert.strictEqual(root.children[1].length, 4); + }); + + test('creates deep tree structure', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 1, token: 1 }, + { startOffsetInclusive: 1, length: 1, token: 2 }, + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 }, + { startOffsetInclusive: 5, length: 1, token: 6 }, + { startOffsetInclusive: 6, length: 1, token: 7 }, + { startOffsetInclusive: 7, length: 1, token: 8 } + ]); + + const root = store.root as any; + assert.ok(root.children); + assert.strictEqual(root.children.length, 2); + assert.ok(root.children[0].children); + assert.strictEqual(root.children[0].children.length, 2); + assert.ok(root.children[0].children[0].children); + assert.strictEqual(root.children[0].children[0].children.length, 2); + }); + + test('updates single token in middle', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(3, [ + { startOffsetInclusive: 3, length: 3, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[1].token, 4); + assert.strictEqual(tokens.children[2].token, 3); + }); + + test('updates multiple consecutive tokens', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(6, [ + { startOffsetInclusive: 3, length: 3, token: 4 }, + { startOffsetInclusive: 6, length: 3, token: 5 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[1].token, 4); + assert.strictEqual(tokens.children[2].token, 5); + }); + + test('updates tokens at start of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(3, [ + { startOffsetInclusive: 0, length: 3, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 4); + assert.strictEqual(tokens.children[1].token, 2); + assert.strictEqual(tokens.children[2].token, 3); + }); + + test('updates tokens at end of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(3, [ + { startOffsetInclusive: 6, length: 3, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[1].token, 2); + assert.strictEqual(tokens.children[2].token, 4); + }); + + test('updates length of tokens', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(6, [ + { startOffsetInclusive: 3, length: 5, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[0].length, 3); + assert.strictEqual(tokens.children[1].token, 4); + assert.strictEqual(tokens.children[1].length, 5); + }); + + test('update deeply nested tree with new token length in the middle', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 1, token: 1 }, + { startOffsetInclusive: 1, length: 1, token: 2 }, + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 }, + { startOffsetInclusive: 5, length: 1, token: 6 }, + { startOffsetInclusive: 6, length: 1, token: 7 }, + { startOffsetInclusive: 7, length: 1, token: 8 } + ]); + + // Update token in the middle (position 3-4) to span 3-6 + store.update(3, [ + { startOffsetInclusive: 3, length: 3, token: 9 } + ]); + + const root = store.root as any; + // Verify the structure remains balanced + assert.strictEqual(root.children.length, 3); + assert.strictEqual(root.children[0].children.length, 2); + + // Verify the lengths are updated correctly + assert.strictEqual(root.children[0].length, 2); // First 2 tokens + assert.strictEqual(root.children[1].length, 4); // Token 3 + our new longer token + assert.strictEqual(root.children[2].length, 2); // Last 2 tokens + }); + + test('update deeply nested tree with a range of tokens that causes tokens to split', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 4, token: 3 }, + { startOffsetInclusive: 10, length: 5, token: 4 }, + { startOffsetInclusive: 15, length: 4, token: 5 }, + { startOffsetInclusive: 19, length: 3, token: 6 }, + { startOffsetInclusive: 22, length: 5, token: 7 }, + { startOffsetInclusive: 27, length: 3, token: 8 } + ]); + + // Update token in the middle which causes tokens to split + store.update(8, [ + { startOffsetInclusive: 12, length: 4, token: 9 }, + { startOffsetInclusive: 16, length: 4, token: 10 } + ]); + + const root = store.root as any; + // Verify the structure remains balanced + assert.strictEqual(root.children.length, 2); + assert.strictEqual(root.children[0].children.length, 2); + + // Verify the lengths are updated correctly + assert.strictEqual(root.children[0].length, 12); + assert.strictEqual(root.children[1].length, 18); + }); + + test('getTokensInRange returns tokens in middle of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + const tokens = store.getTokensInRange(3, 6); + assert.deepStrictEqual(tokens, [{ startOffsetInclusive: 3, length: 3, token: 2 }]); + }); + + test('getTokensInRange returns tokens at start of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + const tokens = store.getTokensInRange(0, 3); + assert.deepStrictEqual(tokens, [{ startOffsetInclusive: 0, length: 3, token: 1 }]); + }); + + test('getTokensInRange returns tokens at end of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + const tokens = store.getTokensInRange(6, 9); + assert.deepStrictEqual(tokens, [{ startOffsetInclusive: 6, length: 3, token: 3 }]); + }); + + test('getTokensInRange returns multiple tokens across nodes', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 1, token: 1 }, + { startOffsetInclusive: 1, length: 1, token: 2 }, + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 }, + { startOffsetInclusive: 5, length: 1, token: 6 } + ]); + + const tokens = store.getTokensInRange(2, 5); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 } + ]); + }); + + test('Realistic scenario one', () => { + // inspired by this snippet, with the update adding a space in the constructor's curly braces: + // /* + // */ + // class XY { + // constructor() {} + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 164164 }, + { startOffsetInclusive: 3, length: 1, token: 32836 }, + { startOffsetInclusive: 4, length: 3, token: 164164 }, + { startOffsetInclusive: 7, length: 2, token: 32836 }, + { startOffsetInclusive: 9, length: 5, token: 196676 }, + { startOffsetInclusive: 14, length: 1, token: 32836 }, + { startOffsetInclusive: 15, length: 2, token: 557124 }, + { startOffsetInclusive: 17, length: 4, token: 32836 }, + { startOffsetInclusive: 21, length: 1, token: 32836 }, + { startOffsetInclusive: 22, length: 11, token: 196676 }, + { startOffsetInclusive: 33, length: 7, token: 32836 }, + { startOffsetInclusive: 40, length: 3, token: 32836 } + ]); + + store.update(33, [ + { startOffsetInclusive: 9, length: 5, token: 196676 }, + { startOffsetInclusive: 14, length: 1, token: 32836 }, + { startOffsetInclusive: 15, length: 2, token: 557124 }, + { startOffsetInclusive: 17, length: 4, token: 32836 }, + { startOffsetInclusive: 21, length: 1, token: 32836 }, + { startOffsetInclusive: 22, length: 11, token: 196676 }, + { startOffsetInclusive: 33, length: 8, token: 32836 }, + { startOffsetInclusive: 41, length: 3, token: 32836 } + ]); + + }); + test('Realistic scenario two', () => { + // inspired by this snippet, with the update deleteing the space in the body of class x + // class x { + // + // } + // class y { + + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 4, token: 32836 }, + { startOffsetInclusive: 11, length: 3, token: 32836 }, + { startOffsetInclusive: 14, length: 3, token: 32836 }, + { startOffsetInclusive: 17, length: 5, token: 196676 }, + { startOffsetInclusive: 22, length: 1, token: 32836 }, + { startOffsetInclusive: 23, length: 1, token: 557124 }, + { startOffsetInclusive: 24, length: 4, token: 32836 }, + { startOffsetInclusive: 28, length: 2, token: 32836 }, + { startOffsetInclusive: 30, length: 1, token: 32836 } + ]); + const tokens0 = store.getTokensInRange(0, 16); + assert.deepStrictEqual(tokens0, [ + { token: 196676, startOffsetInclusive: 0, length: 5 }, + { token: 32836, startOffsetInclusive: 5, length: 1 }, + { token: 557124, startOffsetInclusive: 6, length: 1 }, + { token: 32836, startOffsetInclusive: 7, length: 4 }, + { token: 32836, startOffsetInclusive: 11, length: 3 }, + { token: 32836, startOffsetInclusive: 14, length: 2 } + ]); + + store.update(14, [ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 4, token: 32836 }, + { startOffsetInclusive: 11, length: 2, token: 32836 }, + { startOffsetInclusive: 13, length: 3, token: 32836 } + ]); + + const tokens = store.getTokensInRange(0, 16); + assert.deepStrictEqual(tokens, [ + { token: 196676, startOffsetInclusive: 0, length: 5 }, + { token: 32836, startOffsetInclusive: 5, length: 1 }, + { token: 557124, startOffsetInclusive: 6, length: 1 }, + { token: 32836, startOffsetInclusive: 7, length: 4 }, + { token: 32836, startOffsetInclusive: 11, length: 2 }, + { token: 32836, startOffsetInclusive: 13, length: 3 } + ]); + }); + test('Realistic scenario three', () => { + // inspired by this snippet, with the update adding a space after the { in the constructor + // /*-- + // --*/ + // class TreeViewPane { + // constructor( + // options: IViewletViewOptions, + // ) { + // } + // } + + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 164164 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 5, token: 164164 }, + { startOffsetInclusive: 11, length: 2, token: 32836 }, + { startOffsetInclusive: 13, length: 5, token: 196676 }, + { startOffsetInclusive: 18, length: 1, token: 32836 }, + { startOffsetInclusive: 19, length: 12, token: 557124 }, + { startOffsetInclusive: 31, length: 4, token: 32836 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 11, token: 196676 }, + { startOffsetInclusive: 47, length: 3, token: 32836 }, + { startOffsetInclusive: 50, length: 2, token: 32836 }, + { startOffsetInclusive: 52, length: 7, token: 327748 }, + { startOffsetInclusive: 59, length: 1, token: 98372 }, + { startOffsetInclusive: 60, length: 1, token: 32836 }, + { startOffsetInclusive: 61, length: 19, token: 557124 }, + { startOffsetInclusive: 80, length: 1, token: 32836 }, + { startOffsetInclusive: 81, length: 2, token: 32836 }, + { startOffsetInclusive: 83, length: 6, token: 32836 }, + { startOffsetInclusive: 89, length: 4, token: 32836 }, + { startOffsetInclusive: 93, length: 3, token: 32836 } + ]); + const tokens0 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens0, [ + { token: 196676, startOffsetInclusive: 36, length: 11 }, + { token: 32836, startOffsetInclusive: 47, length: 3 }, + { token: 32836, startOffsetInclusive: 50, length: 2 }, + { token: 327748, startOffsetInclusive: 52, length: 7 } + ]); + + store.update(82, [ + { startOffsetInclusive: 13, length: 5, token: 196676 }, + { startOffsetInclusive: 18, length: 1, token: 32836 }, + { startOffsetInclusive: 19, length: 12, token: 557124 }, + { startOffsetInclusive: 31, length: 4, token: 32836 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 11, token: 196676 }, + { startOffsetInclusive: 47, length: 3, token: 32836 }, + { startOffsetInclusive: 50, length: 2, token: 32836 }, + { startOffsetInclusive: 52, length: 7, token: 327748 }, + { startOffsetInclusive: 59, length: 1, token: 98372 }, + { startOffsetInclusive: 60, length: 1, token: 32836 }, + { startOffsetInclusive: 61, length: 19, token: 557124 }, + { startOffsetInclusive: 80, length: 1, token: 32836 }, + { startOffsetInclusive: 81, length: 2, token: 32836 }, + { startOffsetInclusive: 83, length: 7, token: 32836 }, + { startOffsetInclusive: 90, length: 4, token: 32836 }, + { startOffsetInclusive: 94, length: 3, token: 32836 } + ]); + + const tokens = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens, [ + { token: 196676, startOffsetInclusive: 36, length: 11 }, + { token: 32836, startOffsetInclusive: 47, length: 3 }, + { token: 32836, startOffsetInclusive: 50, length: 2 }, + { token: 327748, startOffsetInclusive: 52, length: 7 } + ]); + }); + test('Realistic scenario four', () => { + // inspired by this snippet, with the update adding a new line after the return true; + // function x() { + // return true; + // } + + // class Y { + // private z = false; + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 8, token: 196676 }, + { startOffsetInclusive: 8, length: 1, token: 32836 }, + { startOffsetInclusive: 9, length: 1, token: 524356 }, + { startOffsetInclusive: 10, length: 6, token: 32836 }, + { startOffsetInclusive: 16, length: 1, token: 32836 }, + { startOffsetInclusive: 17, length: 6, token: 589892 }, + { startOffsetInclusive: 23, length: 1, token: 32836 }, + { startOffsetInclusive: 24, length: 4, token: 196676 }, + { startOffsetInclusive: 28, length: 1, token: 32836 }, + { startOffsetInclusive: 29, length: 2, token: 32836 }, + { startOffsetInclusive: 31, length: 3, token: 32836 }, // This is the closing curly brace + newline chars + { startOffsetInclusive: 34, length: 2, token: 32836 }, + { startOffsetInclusive: 36, length: 5, token: 196676 }, + { startOffsetInclusive: 41, length: 1, token: 32836 }, + { startOffsetInclusive: 42, length: 1, token: 557124 }, + { startOffsetInclusive: 43, length: 4, token: 32836 }, + { startOffsetInclusive: 47, length: 1, token: 32836 }, + { startOffsetInclusive: 48, length: 7, token: 196676 }, + { startOffsetInclusive: 55, length: 1, token: 32836 }, + { startOffsetInclusive: 56, length: 1, token: 327748 }, + { startOffsetInclusive: 57, length: 1, token: 32836 }, + { startOffsetInclusive: 58, length: 1, token: 98372 }, + { startOffsetInclusive: 59, length: 1, token: 32836 }, + { startOffsetInclusive: 60, length: 5, token: 196676 }, + { startOffsetInclusive: 65, length: 1, token: 32836 }, + { startOffsetInclusive: 66, length: 2, token: 32836 }, + { startOffsetInclusive: 68, length: 1, token: 32836 } + ]); + const tokens0 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens0, [ + { startOffsetInclusive: 36, length: 5, token: 196676 }, + { startOffsetInclusive: 41, length: 1, token: 32836 }, + { startOffsetInclusive: 42, length: 1, token: 557124 }, + { startOffsetInclusive: 43, length: 4, token: 32836 }, + { startOffsetInclusive: 47, length: 1, token: 32836 }, + { startOffsetInclusive: 48, length: 7, token: 196676 }, + { startOffsetInclusive: 55, length: 1, token: 32836 }, + { startOffsetInclusive: 56, length: 1, token: 327748 }, + { startOffsetInclusive: 57, length: 1, token: 32836 }, + { startOffsetInclusive: 58, length: 1, token: 98372 } + ]); + + // insert a tab + new line after `return true;` (like hitting enter after the ;) + store.update(32, [ + { startOffsetInclusive: 0, length: 8, token: 196676 }, + { startOffsetInclusive: 8, length: 1, token: 32836 }, + { startOffsetInclusive: 9, length: 1, token: 524356 }, + { startOffsetInclusive: 10, length: 6, token: 32836 }, + { startOffsetInclusive: 16, length: 1, token: 32836 }, + { startOffsetInclusive: 17, length: 6, token: 589892 }, + { startOffsetInclusive: 23, length: 1, token: 32836 }, + { startOffsetInclusive: 24, length: 4, token: 196676 }, + { startOffsetInclusive: 28, length: 1, token: 32836 }, + { startOffsetInclusive: 29, length: 2, token: 32836 }, + { startOffsetInclusive: 31, length: 3, token: 32836 }, // This is the new line, which consists of 3 characters: \t\r\n + { startOffsetInclusive: 34, length: 2, token: 32836 } + ]); + + const tokens1 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens1, [ + { startOffsetInclusive: 36, length: 2, token: 32836 }, + { startOffsetInclusive: 38, length: 2, token: 32836 }, + { startOffsetInclusive: 40, length: 5, token: 196676 }, + { startOffsetInclusive: 45, length: 1, token: 32836 }, + { startOffsetInclusive: 46, length: 1, token: 557124 }, + { startOffsetInclusive: 47, length: 4, token: 32836 }, + { startOffsetInclusive: 51, length: 1, token: 32836 }, + { startOffsetInclusive: 52, length: 7, token: 196676 } + ]); + + // Delete the tab character + store.update(37, [ + { startOffsetInclusive: 0, length: 8, token: 196676 }, + { startOffsetInclusive: 8, length: 1, token: 32836 }, + { startOffsetInclusive: 9, length: 1, token: 524356 }, + { startOffsetInclusive: 10, length: 6, token: 32836 }, + { startOffsetInclusive: 16, length: 1, token: 32836 }, + { startOffsetInclusive: 17, length: 6, token: 589892 }, + { startOffsetInclusive: 23, length: 1, token: 32836 }, + { startOffsetInclusive: 24, length: 4, token: 196676 }, + { startOffsetInclusive: 28, length: 1, token: 32836 }, + { startOffsetInclusive: 29, length: 2, token: 32836 }, + { startOffsetInclusive: 31, length: 2, token: 32836 }, // This is the changed line: \t\r\n to \r\n + { startOffsetInclusive: 33, length: 3, token: 32836 } + ]); + + const tokens2 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens2, [ + { startOffsetInclusive: 36, length: 1, token: 32836 }, + { startOffsetInclusive: 37, length: 2, token: 32836 }, + { startOffsetInclusive: 39, length: 5, token: 196676 }, + { startOffsetInclusive: 44, length: 1, token: 32836 }, + { startOffsetInclusive: 45, length: 1, token: 557124 }, + { startOffsetInclusive: 46, length: 4, token: 32836 }, + { startOffsetInclusive: 50, length: 1, token: 32836 }, + { startOffsetInclusive: 51, length: 7, token: 196676 }, + { startOffsetInclusive: 58, length: 1, token: 32836 } + ]); + + }); + + test('Insert new line and remove tabs (split tokens)', () => { + // class A { + // a() { + // } + // } + // + // interface I { + // + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 3, token: 32836 }, + { startOffsetInclusive: 10, length: 1, token: 32836 }, + { startOffsetInclusive: 11, length: 1, token: 524356 }, + { startOffsetInclusive: 12, length: 5, token: 32836 }, + { startOffsetInclusive: 17, length: 3, token: 32836 }, // This is the closing curly brace line of a() + { startOffsetInclusive: 20, length: 2, token: 32836 }, + { startOffsetInclusive: 22, length: 1, token: 32836 }, + { startOffsetInclusive: 23, length: 9, token: 196676 }, + { startOffsetInclusive: 32, length: 1, token: 32836 }, + { startOffsetInclusive: 33, length: 1, token: 557124 }, + { startOffsetInclusive: 34, length: 3, token: 32836 }, + { startOffsetInclusive: 37, length: 1, token: 32836 }, + { startOffsetInclusive: 38, length: 1, token: 32836 } + ]); + + const tokens0 = store.getTokensInRange(23, 39); + assert.deepStrictEqual(tokens0, [ + { startOffsetInclusive: 23, length: 9, token: 196676 }, + { startOffsetInclusive: 32, length: 1, token: 32836 }, + { startOffsetInclusive: 33, length: 1, token: 557124 }, + { startOffsetInclusive: 34, length: 3, token: 32836 }, + { startOffsetInclusive: 37, length: 1, token: 32836 }, + { startOffsetInclusive: 38, length: 1, token: 32836 } + ]); + + // Insert a new line after a() { }, which will add 2 tabs + store.update(21, [ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 3, token: 32836 }, + { startOffsetInclusive: 10, length: 1, token: 32836 }, + { startOffsetInclusive: 11, length: 1, token: 524356 }, + { startOffsetInclusive: 12, length: 5, token: 32836 }, + { startOffsetInclusive: 17, length: 3, token: 32836 }, + { startOffsetInclusive: 20, length: 3, token: 32836 }, + { startOffsetInclusive: 23, length: 1, token: 32836 } + ]); + + const tokens1 = store.getTokensInRange(26, 42); + assert.deepStrictEqual(tokens1, [ + { startOffsetInclusive: 26, length: 9, token: 196676 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 1, token: 557124 }, + { startOffsetInclusive: 37, length: 3, token: 32836 }, + { startOffsetInclusive: 40, length: 1, token: 32836 }, + { startOffsetInclusive: 41, length: 1, token: 32836 } + ]); + + // Insert another new line at the cursor, which will also cause the 2 tabs to be deleted + store.update(24, [ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 3, token: 32836 }, + { startOffsetInclusive: 10, length: 1, token: 32836 }, + { startOffsetInclusive: 11, length: 1, token: 524356 }, + { startOffsetInclusive: 12, length: 5, token: 32836 }, + { startOffsetInclusive: 17, length: 3, token: 32836 }, + { startOffsetInclusive: 20, length: 1, token: 32836 }, + { startOffsetInclusive: 21, length: 2, token: 32836 }, + { startOffsetInclusive: 23, length: 1, token: 32836 } + ]); + + const tokens2 = store.getTokensInRange(26, 42); + assert.deepStrictEqual(tokens2, [ + { startOffsetInclusive: 26, length: 9, token: 196676 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 1, token: 557124 }, + { startOffsetInclusive: 37, length: 3, token: 32836 }, + { startOffsetInclusive: 40, length: 1, token: 32836 }, + { startOffsetInclusive: 41, length: 1, token: 32836 } + ]); + }); + + test('delete removes tokens in the middle', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + store.delete(3, 3); // delete 3 chars starting at offset 3 + const tokens = store.getTokensInRange(0, 9); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 3 } + ]); + }); + + test('delete merges partially affected token', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 1 }, + { startOffsetInclusive: 5, length: 5, token: 2 } + ]); + store.delete(3, 4); // removes 4 chars within token 1 and partially token 2 + const tokens = store.getTokensInRange(0, 10); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 4, token: 1 }, + // token 2 is now shifted left by 4 + { startOffsetInclusive: 4, length: 3, token: 2 } + ]); + }); + + test('replace a token with a slightly larger token', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 1 }, + { startOffsetInclusive: 5, length: 1, token: 2 }, + { startOffsetInclusive: 6, length: 1, token: 2 }, + { startOffsetInclusive: 7, length: 17, token: 2 }, + { startOffsetInclusive: 24, length: 1, token: 2 }, + { startOffsetInclusive: 25, length: 5, token: 2 }, + { startOffsetInclusive: 30, length: 1, token: 2 }, + { startOffsetInclusive: 31, length: 1, token: 2 }, + { startOffsetInclusive: 32, length: 5, token: 2 } + ]); + store.update(17, [{ startOffsetInclusive: 7, length: 19, token: 0 }]); // removes 4 chars within token 1 and partially token 2 + const tokens = store.getTokensInRange(0, 39); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 5, token: 1 }, + { startOffsetInclusive: 5, length: 1, token: 2 }, + { startOffsetInclusive: 6, length: 1, token: 2 }, + { startOffsetInclusive: 7, length: 19, token: 0 }, + { startOffsetInclusive: 26, length: 1, token: 2 }, + { startOffsetInclusive: 27, length: 5, token: 2 }, + { startOffsetInclusive: 32, length: 1, token: 2 }, + { startOffsetInclusive: 33, length: 1, token: 2 }, + { startOffsetInclusive: 34, length: 5, token: 2 } + ]); + }); + + test('replace a character from a large token', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 2, token: 1 }, + { startOffsetInclusive: 2, length: 5, token: 2 }, + { startOffsetInclusive: 7, length: 1, token: 3 } + ]); + store.delete(1, 3); + const tokens = store.getTokensInRange(0, 7); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 2, token: 1 }, + { startOffsetInclusive: 2, length: 1, token: 2 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 1, token: 3 } + ]); + }); +}); + diff --git a/code/src/vs/editor/test/common/services/testTreeSitterService.ts b/code/src/vs/editor/test/common/services/testTreeSitterService.ts index fd54b59ee5c..e2ca50462ec 100644 --- a/code/src/vs/editor/test/common/services/testTreeSitterService.ts +++ b/code/src/vs/editor/test/common/services/testTreeSitterService.ts @@ -6,17 +6,16 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { Event } from '../../../../base/common/event.js'; import { ITextModel } from '../../../common/model.js'; -import { ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter } from '../../../common/services/treeSitterParserService.js'; -import { Range } from '../../../common/core/range.js'; +import { ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, TreeUpdateEvent } from '../../../common/services/treeSitterParserService.js'; export class TestTreeSitterParserService implements ITreeSitterParserService { - getTextModelTreeSitter(textModel: ITextModel): ITextModelTreeSitter | undefined { + async getTextModelTreeSitter(model: ITextModel, parseImmediately?: boolean): Promise { throw new Error('Method not implemented.'); } getTree(content: string, languageId: string): Promise { throw new Error('Method not implemented.'); } - onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }> = Event.None; + onDidUpdateTree: Event = Event.None; onDidAddLanguage: Event<{ id: string; language: Parser.Language }> = Event.None; _serviceBrand: undefined; getOrInitLanguage(languageId: string): Parser.Language | undefined { diff --git a/code/src/vs/editor/test/common/utils/testDecoder.ts b/code/src/vs/editor/test/common/utils/testDecoder.ts index e9ee9ce1067..a998a64e2cc 100644 --- a/code/src/vs/editor/test/common/utils/testDecoder.ts +++ b/code/src/vs/editor/test/common/utils/testDecoder.ts @@ -7,22 +7,16 @@ import assert from 'assert'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { randomInt } from '../../../../base/common/numbers.js'; import { BaseToken } from '../../../common/codecs/baseToken.js'; +import { assertDefined } from '../../../../base/common/types.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { WriteableStream } from '../../../../base/common/stream.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; /** - * (pseudo)Random boolean generator. - * - * ## Examples - * - * ```typsecript - * randomBoolean(); // generates either `true` or `false` - * ``` + * Kind of decoder tokens consume methods are different ways + * consume tokens that a decoder produces out of a byte stream. */ -const randomBoolean = (): boolean => { - return Math.random() > 0.5; -}; +export type TTokensConsumeMethod = 'async-generator' | 'consume-all-method' | 'on-data-event'; /** * A reusable test utility that asserts that the given decoder @@ -49,57 +43,166 @@ const randomBoolean = (): boolean => { export class TestDecoder> extends Disposable { constructor( private readonly stream: WriteableStream, - private readonly decoder: D, + public readonly decoder: D, ) { super(); this._register(this.decoder); } + /** + * Write provided {@linkcode inputData} data to the input byte stream + * asynchronously in the background in small random-length chunks. + * + * @param inputData Input data to send. + */ + public sendData( + inputData: string | string[], + ): this { + // if input data was passed as an array of lines, + // join them into a single string with newlines + if (Array.isArray(inputData)) { + inputData = inputData.join('\n'); + } + + // write the input data to the stream in multiple random-length + // chunks to simulate real input stream data flows + let inputDataBytes = VSBuffer.fromString(inputData); + const interval = setInterval(() => { + if (inputDataBytes.byteLength <= 0) { + clearInterval(interval); + this.stream.end(); + + return; + } + + const dataToSend = inputDataBytes.slice(0, randomInt(inputDataBytes.byteLength)); + this.stream.write(dataToSend); + inputDataBytes = inputDataBytes.slice(dataToSend.byteLength); + }, randomInt(5)); + + return this; + } + /** * Run the test sending the `inputData` data to the stream and asserting * that the decoder produces the `expectedTokens` sequence of tokens. + * + * @param inputData Input data of the input byte stream. + * @param expectedTokens List of expected tokens the test token must produce. + * @param tokensConsumeMethod *Optional* method of consuming the decoder stream. + * Defaults to a random method (see {@linkcode randomTokensConsumeMethod}). */ public async run( - inputData: string, + inputData: string | string[], expectedTokens: readonly T[], + tokensConsumeMethod: TTokensConsumeMethod = this.randomTokensConsumeMethod(), ): Promise { - // write the data to the stream after a short delay to ensure - // that the the data is sent after the reading loop below - setTimeout(() => { - let inputDataBytes = VSBuffer.fromString(inputData); - - // write the input data to the stream in multiple random-length chunks - while (inputDataBytes.byteLength > 0) { - const dataToSend = inputDataBytes.slice(0, randomInt(inputDataBytes.byteLength)); - this.stream.write(dataToSend); - inputDataBytes = inputDataBytes.slice(dataToSend.byteLength); - } + try { + // initiate the data sending flow + this.sendData(inputData); - this.stream.end(); - }, 25); + // consume the decoder tokens based on specified + // (or randomly generated) tokens consume method + const receivedTokens: T[] = []; + switch (tokensConsumeMethod) { + // test the `async iterator` code path + case 'async-generator': { + for await (const token of this.decoder) { + if (token === null) { + break; + } - // randomly use either the `async iterator` or the `.consume()` - // variants of getting tokens, they both must yield equal results - const receivedTokens: T[] = []; - if (randomBoolean()) { - // test the `async iterator` code path - for await (const token of this.decoder) { - if (token === null) { + receivedTokens.push(token); + } + + break; + } + // test the `.consumeAll()` method code path + case 'consume-all-method': { + receivedTokens.push(...(await this.decoder.consumeAll())); break; } + // test the `.onData()` event consume flow + case 'on-data-event': { + this.decoder.onData((token) => { + receivedTokens.push(token); + }); + + // in this case we also test the `settled` promise of the decoder + await this.decoder.settled; + + break; + } + // ensure that the switch block is exhaustive + default: { + throw new Error(`Unknown consume method '${tokensConsumeMethod}'.`); + } + } + + // validate the received tokens + this.validateReceivedTokens( + receivedTokens, + expectedTokens, + ); + } catch (error) { + assertDefined( + error, + `An non-nullable error must be thrown.`, + ); + assert( + error instanceof Error, + `An error error instance must be thrown.`, + ); + + // add the tokens consume method to the error message so we + // would know which method of consuming the tokens failed exactly + error.message = `[${tokensConsumeMethod}] ${error.message}`; + } + } + + /** + * Randomly generate a tokens consume method type for the test. + */ + private randomTokensConsumeMethod(): TTokensConsumeMethod { + const testConsumeMethodIndex = randomInt(2); - receivedTokens.push(token); + switch (testConsumeMethodIndex) { + // test the `async iterator` code path + case 0: { + return 'async-generator'; + } + // test the `.consumeAll()` method code path + case 1: { + return 'consume-all-method'; + } + // test the `.onData()` event consume flow + case 2: { + return 'on-data-event'; + } + // ensure that the switch block is exhaustive + default: { + throw new Error(`Unknown consume method index '${testConsumeMethodIndex}'.`); } - } else { - // test the `.consume()` code path - receivedTokens.push(...(await this.decoder.consumeAll())); } + } + /** + * Validate that received tokens list is equal to the expected one. + */ + private validateReceivedTokens( + receivedTokens: readonly T[], + expectedTokens: readonly T[], + ) { for (let i = 0; i < expectedTokens.length; i++) { const expectedToken = expectedTokens[i]; const receivedtoken = receivedTokens[i]; + assertDefined( + receivedtoken, + `Expected token '${i}' to be '${expectedToken}', got 'undefined'.`, + ); + assert( receivedtoken.equals(expectedToken), `Expected token '${i}' to be '${expectedToken}', got '${receivedtoken}'.`, diff --git a/code/src/vs/editor/test/node/classification/typescript-test.ts b/code/src/vs/editor/test/node/classification/typescript-test.ts deleted file mode 100644 index f1a8a21c22d..00000000000 --- a/code/src/vs/editor/test/node/classification/typescript-test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/// -/* eslint-disable */ -const x01 = "string"; -/// ^^^^^^^^ string - -const x02 = '\''; -/// ^^^^ string - -const x03 = '\n\'\t'; -/// ^^^^^^^^ string - -const x04 = 'this is\ -/// ^^^^^^^^^ string\ -a multiline string'; -/// <------------------- string - -const x05 = x01;// just some text -/// ^^^^^^^^^^^^^^^^^ comment - -const x06 = x05;/* multi -/// ^^^^^^^^ comment -line *comment */ -/// <---------------- comment - -const x07 = 4 / 5; - -const x08 = `howdy`; -/// ^^^^^^^ string - -const x09 = `\'\"\``; -/// ^^^^^^^^ string - -const x10 = `$[]`; -/// ^^^^^ string - -const x11 = `${x07 +/**/3}px`; -/// ^^^ string -/// ^^^^ comment -/// ^^^^ string - -const x12 = `${x07 + (function () { return 5; })()/**/}px`; -/// ^^^ string -/// ^^^^ comment -/// ^^^^ string - -const x13 = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; -/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ regex - -const x14 = /\./g; -/// ^^^^^ regex - - -const x15 = Math.abs(x07) / x07; // speed -/// ^^^^^^^^ comment - -const x16 = / x07; /.test('3'); -/// ^^^^^^^^ regex -/// ^^^ string - -const x17 = `.monaco-dialog-modal-block${true ? '.dimmed' : ''}`; -/// ^^^^^^^^^^^^^^^^^^^^^^ string -/// ^^^^^^^^^ string -/// ^^^^ string - -const x18 = Math.min((14 <= 0.5 ? 123 / (2 * 1) : ''.length / (2 - (2 * 1))), 1); -/// ^^ string - -const x19 = `${3 / '5'.length} km/h)`; -/// ^^^ string -/// ^^^ string -/// ^^^^^^^ string diff --git a/code/src/vs/editor/test/node/classification/typescript.test.ts b/code/src/vs/editor/test/node/classification/typescript.test.ts deleted file mode 100644 index 4166eadcd23..00000000000 --- a/code/src/vs/editor/test/node/classification/typescript.test.ts +++ /dev/null @@ -1,145 +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 assert from 'assert'; -import { StandardTokenType } from '../../../common/encodedTokenAttributes.js'; -import * as fs from 'fs'; -// import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; -// import { parse } from 'vs/editor/common/modes/tokenization/typescript'; -import { toStandardTokenType } from '../../../common/languages/supports/tokenization.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; - -interface IParseFunc { - (text: string): number[]; -} - -interface IAssertion { - testLineNumber: number; - startOffset: number; - length: number; - tokenType: StandardTokenType; -} - -interface ITest { - content: string; - assertions: IAssertion[]; -} - -function parseTest(fileName: string): ITest { - interface ILineWithAssertions { - line: string; - assertions: ILineAssertion[]; - } - - interface ILineAssertion { - testLineNumber: number; - startOffset: number; - length: number; - expectedTokenType: StandardTokenType; - } - - const testContents = fs.readFileSync(fileName).toString(); - const lines = testContents.split(/\r\n|\n/); - const magicToken = lines[0]; - - let currentElement: ILineWithAssertions = { - line: lines[1], - assertions: [] - }; - - const parsedTest: ILineWithAssertions[] = []; - for (let i = 2; i < lines.length; i++) { - const line = lines[i]; - if (line.substr(0, magicToken.length) === magicToken) { - // this is an assertion line - const m1 = line.substr(magicToken.length).match(/^( +)([\^]+) (\w+)\\?$/); - if (m1) { - currentElement.assertions.push({ - testLineNumber: i + 1, - startOffset: magicToken.length + m1[1].length, - length: m1[2].length, - expectedTokenType: toStandardTokenType(m1[3]) - }); - } else { - const m2 = line.substr(magicToken.length).match(/^( +)<(-+) (\w+)\\?$/); - if (m2) { - currentElement.assertions.push({ - testLineNumber: i + 1, - startOffset: 0, - length: m2[2].length, - expectedTokenType: toStandardTokenType(m2[3]) - }); - } else { - throw new Error(`Invalid test line at line number ${i + 1}.`); - } - } - } else { - // this is a line to be parsed - parsedTest.push(currentElement); - currentElement = { - line: line, - assertions: [] - }; - } - } - parsedTest.push(currentElement); - - const assertions: IAssertion[] = []; - - let offset = 0; - for (let i = 0; i < parsedTest.length; i++) { - const parsedTestLine = parsedTest[i]; - for (let j = 0; j < parsedTestLine.assertions.length; j++) { - const assertion = parsedTestLine.assertions[j]; - assertions.push({ - testLineNumber: assertion.testLineNumber, - startOffset: offset + assertion.startOffset, - length: assertion.length, - tokenType: assertion.expectedTokenType - }); - } - offset += parsedTestLine.line.length + 1; - } - - const content: string = parsedTest.map(parsedTestLine => parsedTestLine.line).join('\n'); - - return { content, assertions }; -} - -// @ts-expect-error -function executeTest(fileName: string, parseFunc: IParseFunc): void { - const { content, assertions } = parseTest(fileName); - const actual = parseFunc(content); - - let actualIndex = 0; - const actualCount = actual.length / 3; - for (let i = 0; i < assertions.length; i++) { - const assertion = assertions[i]; - while (actualIndex < actualCount && actual[3 * actualIndex] + actual[3 * actualIndex + 1] <= assertion.startOffset) { - actualIndex++; - } - assert.ok( - actual[3 * actualIndex] <= assertion.startOffset, - `Line ${assertion.testLineNumber} : startOffset : ${actual[3 * actualIndex]} <= ${assertion.startOffset}` - ); - assert.ok( - actual[3 * actualIndex] + actual[3 * actualIndex + 1] >= assertion.startOffset + assertion.length, - `Line ${assertion.testLineNumber} : length : ${actual[3 * actualIndex]} + ${actual[3 * actualIndex + 1]} >= ${assertion.startOffset} + ${assertion.length}.` - ); - assert.strictEqual( - actual[3 * actualIndex + 2], - assertion.tokenType, - `Line ${assertion.testLineNumber} : tokenType`); - } -} - -suite('Classification', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('TypeScript', () => { - // executeTest(getPathFromAmdModule(require, 'vs/editor/test/node/classification/typescript-test.ts').replace(/\bout\b/, 'src'), parse); - }); -}); diff --git a/code/src/vs/monaco.d.ts b/code/src/vs/monaco.d.ts index bd27d479941..1968fac5a56 100644 --- a/code/src/vs/monaco.d.ts +++ b/code/src/vs/monaco.d.ts @@ -2114,6 +2114,10 @@ declare namespace monaco.editor { * Create a valid range. */ validateRange(range: IRange): Range; + /** + * Verifies the range is valid. + */ + isValidRange(range: IRange): boolean; /** * Converts the position to a zero-based offset. * @@ -2899,7 +2903,7 @@ declare namespace monaco.editor { export interface IModelContentChange { /** - * The range that got replaced. + * The old range that got replaced. */ readonly range: IRange; /** @@ -4600,16 +4604,6 @@ declare namespace monaco.editor { * Font family for inline suggestions. */ fontFamily?: string | 'default'; - edits?: { - experimental?: { - enabled?: boolean; - useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; - useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; - useWordInsertionView?: 'never' | 'whenPossible'; - useWordReplacementView?: 'never' | 'whenPossible'; - useGutterIndicator?: boolean; - }; - }; } type RequiredRecursive = { @@ -6901,6 +6895,17 @@ declare namespace monaco.languages { removeText?: number; } + export interface SyntaxNode { + startIndex: number; + endIndex: number; + } + + export interface QueryCapture { + name: string; + text?: string; + node: SyntaxNode; + } + /** * The state of the tokenizer between two lines. * It is useful to store flags such as in multiline comment, etc. @@ -7320,6 +7325,7 @@ declare namespace monaco.languages { * The current provider is only requested for completions if no provider with a preferred group id returned a result. */ yieldsToGroupIds?: InlineCompletionProviderGroupId[]; + displayName?: string; toString?(): string; } @@ -8060,7 +8066,7 @@ declare namespace monaco.languages { export interface CodeLensList { lenses: CodeLens[]; - dispose(): void; + dispose?(): void; } export interface CodeLensProvider { @@ -8136,31 +8142,6 @@ declare namespace monaco.languages { provideDocumentRangeSemanticTokens(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } - export interface DocumentContextItem { - readonly uri: Uri; - readonly version: number; - readonly ranges: IRange[]; - } - - export interface MappedEditsContext { - /** The outer array is sorted by priority - from highest to lowest. The inner arrays contain elements of the same priority. */ - readonly documents: DocumentContextItem[][]; - } - - export interface MappedEditsProvider { - /** - * Provider maps code blocks from the chat into a workspace edit. - * - * @param document The document to provide mapped edits for. - * @param codeBlocks Code blocks that come from an LLM's reply. - * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. - * @param context The context for providing mapped edits. - * @param token A cancellation token. - * @returns A provider result of text edits. - */ - provideMappedEdits(document: editor.ITextModel, codeBlocks: string[], context: MappedEditsContext, token: CancellationToken): Promise; - } - export interface IInlineEdit { text: string; range: IRange; @@ -8181,6 +8162,7 @@ declare namespace monaco.languages { } export interface InlineEditProvider { + displayName?: string; provideInlineEdit(model: editor.ITextModel, context: IInlineEditContext, token: CancellationToken): ProviderResult; freeInlineEdit(edit: T): void; } diff --git a/code/src/vs/platform/accessibility/browser/accessibleView.ts b/code/src/vs/platform/accessibility/browser/accessibleView.ts index da30ffe3a25..b438dedc42e 100644 --- a/code/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/code/src/vs/platform/accessibility/browser/accessibleView.ts @@ -35,6 +35,7 @@ export const enum AccessibleViewProviderId { ReplHelp = 'replHelp', RunAndDebug = 'runAndDebug', Walkthrough = 'walkthrough', + SourceControl = 'scm' } export const enum AccessibleViewType { diff --git a/code/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/code/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts index fd643b97def..2f98c85f893 100644 --- a/code/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts +++ b/code/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -8,7 +8,7 @@ import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider import { ContextKeyExpression } from '../../contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../instantiation/common/instantiation.js'; -export interface IAccessibleViewImplentation { +export interface IAccessibleViewImplementation { type: AccessibleViewType; priority: number; name: string; @@ -20,9 +20,9 @@ export interface IAccessibleViewImplentation { } export const AccessibleViewRegistry = new class AccessibleViewRegistry { - _implementations: IAccessibleViewImplentation[] = []; + _implementations: IAccessibleViewImplementation[] = []; - register(implementation: IAccessibleViewImplentation): IDisposable { + register(implementation: IAccessibleViewImplementation): IDisposable { this._implementations.push(implementation); return { dispose: () => { @@ -34,7 +34,7 @@ export const AccessibleViewRegistry = new class AccessibleViewRegistry { }; } - getImplementations(): IAccessibleViewImplentation[] { + getImplementations(): IAccessibleViewImplementation[] { return this._implementations; } }; diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/foldedAreas.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/foldedAreas.mp3 index 802a7a67514..deaae4622d0 100644 Binary files a/code/src/vs/platform/accessibilitySignal/browser/media/foldedAreas.mp3 and b/code/src/vs/platform/accessibilitySignal/browser/media/foldedAreas.mp3 differ diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/quickFixes.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/quickFixes.mp3 index 22ad474b9cf..c73669dd8f3 100644 Binary files a/code/src/vs/platform/accessibilitySignal/browser/media/quickFixes.mp3 and b/code/src/vs/platform/accessibilitySignal/browser/media/quickFixes.mp3 differ diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 index f21230080be..68a9cc83565 100644 Binary files a/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 and b/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 differ diff --git a/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 517bd2f66de..f90896f91a3 100644 --- a/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -169,6 +169,7 @@ export interface IMenuEntryActionViewItemOptions { draggable?: boolean; keybinding?: string; hoverDelegate?: IHoverDelegate; + keybindingNotRenderedWithLabel?: boolean; } export class MenuEntryActionViewItem extends ActionViewItem { @@ -187,7 +188,7 @@ export class MenuEntryActionViewItem { + return markAsSingleton(toDisposable(() => { if (this._commands.delete(command.id)) { this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(MenuId.CommandPalette)); } - }); + })); } getCommand(id: string): ICommandAction | undefined { @@ -422,10 +424,10 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { } const rm = list.push(item); this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(id)); - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { rm(); this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(id)); - }); + })); } appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem }>): IDisposable { diff --git a/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts b/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts index ea7abedf6f0..3ef1cb28108 100644 --- a/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts +++ b/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts @@ -98,7 +98,7 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar const windowState: IWindowState = {}; const overrides: IDefaultBrowserWindowOptionsOverrides = {}; - const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=800,height=600 + const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=1024,height=768 for (const feature of features) { const [key, value] = feature.split('='); switch (key) { diff --git a/code/src/vs/platform/commands/common/commands.ts b/code/src/vs/platform/commands/common/commands.ts index 53e32c4842e..b6e7bc6fe50 100644 --- a/code/src/vs/platform/commands/common/commands.ts +++ b/code/src/vs/platform/commands/common/commands.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; import { IJSONSchema } from '../../../base/common/jsonSchema.js'; -import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js'; import { LinkedList } from '../../../base/common/linkedList.js'; import { TypeConstraint, validateConstraints } from '../../../base/common/types.js'; import { ILocalizedString } from '../../action/common/action.js'; @@ -121,7 +121,7 @@ export const CommandsRegistry: ICommandRegistry = new class implements ICommandR // tell the world about this command this._onDidRegisterCommand.fire(id); - return ret; + return markAsSingleton(ret); } registerCommandAlias(oldId: string, newId: string): IDisposable { diff --git a/code/src/vs/platform/configuration/common/configuration.ts b/code/src/vs/platform/configuration/common/configuration.ts index e9a9143fa22..177c4e43e9f 100644 --- a/code/src/vs/platform/configuration/common/configuration.ts +++ b/code/src/vs/platform/configuration/common/configuration.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IStringDictionary } from '../../../base/common/collections.js'; import { Event } from '../../../base/common/event.js'; import * as types from '../../../base/common/types.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; @@ -182,6 +183,7 @@ export interface IConfigurationModel { contents: any; keys: string[]; overrides: IOverrides[]; + raw?: IStringDictionary; } export interface IOverrides { @@ -194,7 +196,8 @@ export interface IConfigurationData { defaults: IConfigurationModel; policy: IConfigurationModel; application: IConfigurationModel; - user: IConfigurationModel; + userLocal: IConfigurationModel; + userRemote: IConfigurationModel; workspace: IConfigurationModel; folders: [UriComponents, IConfigurationModel][]; } diff --git a/code/src/vs/platform/configuration/common/configurationModels.ts b/code/src/vs/platform/configuration/common/configurationModels.ts index 400ae1033cb..4e220cc465d 100644 --- a/code/src/vs/platform/configuration/common/configurationModels.ts +++ b/code/src/vs/platform/configuration/common/configurationModels.ts @@ -38,7 +38,7 @@ export class ConfigurationModel implements IConfigurationModel { private readonly _contents: any, private readonly _keys: string[], private readonly _overrides: IOverrides[], - readonly raw: ReadonlyArray | ConfigurationModel> | undefined, + readonly raw: IStringDictionary | ReadonlyArray | ConfigurationModel> | undefined, private readonly logService: ILogService ) { } @@ -46,8 +46,8 @@ export class ConfigurationModel implements IConfigurationModel { private _rawConfiguration: ConfigurationModel | undefined; get rawConfiguration(): ConfigurationModel { if (!this._rawConfiguration) { - if (this.raw?.length) { - const rawConfigurationModels = this.raw.map(raw => { + if (this.raw) { + const rawConfigurationModels = (Array.isArray(this.raw) ? this.raw : [this.raw]).map(raw => { if (raw instanceof ConfigurationModel) { return raw; } @@ -147,10 +147,10 @@ export class ConfigurationModel implements IConfigurationModel { const contents = objects.deepClone(this.contents); const overrides = objects.deepClone(this.overrides); const keys = [...this.keys]; - const raws = this.raw?.length ? [...this.raw] : [this]; + const raws = this.raw ? Array.isArray(this.raw) ? [...this.raw] : [this.raw] : [this]; for (const other of others) { - raws.push(...(other.raw?.length ? other.raw : [other])); + raws.push(...(other.raw ? Array.isArray(other.raw) ? other.raw : [other.raw] : [other])); if (other.isEmpty()) { continue; } @@ -172,7 +172,7 @@ export class ConfigurationModel implements IConfigurationModel { } } } - return new ConfigurationModel(contents, keys, overrides, raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws, this.logService); + return new ConfigurationModel(contents, keys, overrides, !raws.length || raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws, this.logService); } private createOverrideConfigurationModel(identifier: string): ConfigurationModel { @@ -727,7 +727,7 @@ export class Configuration { } getValue(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): any { - const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(section, overrides, workspace); + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides, workspace); return consolidateConfigurationModel.getValue(section); } @@ -755,7 +755,7 @@ export class Configuration { } inspect(key: string, overrides: IConfigurationOverrides, workspace: Workspace | undefined): IConfigurationValue { - const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(key, overrides, workspace); + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides, workspace); const folderConfigurationModel = this.getFolderConfigurationModelForResource(overrides.resource, workspace); const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration; const overrideIdentifiers = new Set(); @@ -944,7 +944,12 @@ export class Configuration { private _userConfiguration: ConfigurationModel | null = null; get userConfiguration(): ConfigurationModel { if (!this._userConfiguration) { - this._userConfiguration = this._remoteUserConfiguration.isEmpty() ? this._localUserConfiguration : this._localUserConfiguration.merge(this._remoteUserConfiguration); + if (this._remoteUserConfiguration.isEmpty()) { + this._userConfiguration = this._localUserConfiguration; + } else { + const merged = this._localUserConfiguration.merge(this._remoteUserConfiguration); + this._userConfiguration = new ConfigurationModel(merged.contents, merged.keys, merged.overrides, undefined, this.logService); + } } return this._userConfiguration; } @@ -965,13 +970,17 @@ export class Configuration { return this._folderConfigurations; } - private getConsolidatedConfigurationModel(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { + private getConsolidatedConfigurationModel(overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { let configurationModel = this.getConsolidatedConfigurationModelForResource(overrides, workspace); if (overrides.overrideIdentifier) { configurationModel = configurationModel.override(overrides.overrideIdentifier); } - if (!this._policyConfiguration.isEmpty() && this._policyConfiguration.getValue(section) !== undefined) { - configurationModel = configurationModel.merge(this._policyConfiguration); + if (!this._policyConfiguration.isEmpty()) { + // clone by merging + configurationModel = configurationModel.merge(); + for (const key of this._policyConfiguration.keys) { + configurationModel.setValue(key, this._policyConfiguration.getValue(key)); + } } return configurationModel; } @@ -1030,7 +1039,7 @@ export class Configuration { defaults: { contents: this._defaultConfiguration.contents, overrides: this._defaultConfiguration.overrides, - keys: this._defaultConfiguration.keys + keys: this._defaultConfiguration.keys, }, policy: { contents: this._policyConfiguration.contents, @@ -1040,12 +1049,20 @@ export class Configuration { application: { contents: this.applicationConfiguration.contents, overrides: this.applicationConfiguration.overrides, - keys: this.applicationConfiguration.keys + keys: this.applicationConfiguration.keys, + raw: Array.isArray(this.applicationConfiguration.raw) ? undefined : this.applicationConfiguration.raw }, - user: { - contents: this.userConfiguration.contents, - overrides: this.userConfiguration.overrides, - keys: this.userConfiguration.keys + userLocal: { + contents: this.localUserConfiguration.contents, + overrides: this.localUserConfiguration.overrides, + keys: this.localUserConfiguration.keys, + raw: Array.isArray(this.localUserConfiguration.raw) ? undefined : this.localUserConfiguration.raw + }, + userRemote: { + contents: this.remoteUserConfiguration.contents, + overrides: this.remoteUserConfiguration.overrides, + keys: this.remoteUserConfiguration.keys, + raw: Array.isArray(this.remoteUserConfiguration.raw) ? undefined : this.remoteUserConfiguration.raw }, workspace: { contents: this._workspaceConfiguration.contents, @@ -1091,7 +1108,8 @@ export class Configuration { const defaultConfiguration = this.parseConfigurationModel(data.defaults, logService); const policyConfiguration = this.parseConfigurationModel(data.policy, logService); const applicationConfiguration = this.parseConfigurationModel(data.application, logService); - const userConfiguration = this.parseConfigurationModel(data.user, logService); + const userLocalConfiguration = this.parseConfigurationModel(data.userLocal, logService); + const userRemoteConfiguration = this.parseConfigurationModel(data.userRemote, logService); const workspaceConfiguration = this.parseConfigurationModel(data.workspace, logService); const folders: ResourceMap = data.folders.reduce((result, value) => { result.set(URI.revive(value[0]), this.parseConfigurationModel(value[1], logService)); @@ -1101,8 +1119,8 @@ export class Configuration { defaultConfiguration, policyConfiguration, applicationConfiguration, - userConfiguration, - ConfigurationModel.createEmptyModel(logService), + userLocalConfiguration, + userRemoteConfiguration, workspaceConfiguration, folders, ConfigurationModel.createEmptyModel(logService), @@ -1112,7 +1130,7 @@ export class Configuration { } private static parseConfigurationModel(model: IConfigurationModel, logService: ILogService): ConfigurationModel { - return new ConfigurationModel(model.contents, model.keys, model.overrides, undefined, logService); + return new ConfigurationModel(model.contents, model.keys, model.overrides, model.raw, logService); } } diff --git a/code/src/vs/platform/configuration/common/configurationRegistry.ts b/code/src/vs/platform/configuration/common/configurationRegistry.ts index 6d15e6732fc..d2ba316398e 100644 --- a/code/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/code/src/vs/platform/configuration/common/configurationRegistry.ts @@ -126,13 +126,17 @@ export interface IConfigurationRegistry { export const enum ConfigurationScope { /** - * Application specific configuration, which can be configured only in local user settings. + * Application specific configuration, which can be configured only in default profile user settings. */ APPLICATION = 1, /** * Machine specific configuration, which can be configured only in local and remote user settings. */ MACHINE, + /** + * An application machine specific configuration, which can be configured only in default profile user settings and remote user settings. + */ + APPLICATION_MACHINE, /** * Window specific configuration, which can be configured in the user or workspace settings. */ @@ -269,6 +273,7 @@ export interface IConfigurationDefaultOverrideValue { export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; +export const applicationMachineSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const machineSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const machineOverridableSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const windowSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; @@ -744,6 +749,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { case ConfigurationScope.MACHINE: machineSettings.properties[key] = property; break; + case ConfigurationScope.APPLICATION_MACHINE: + applicationMachineSettings.properties[key] = property; + break; case ConfigurationScope.MACHINE_OVERRIDABLE: machineOverridableSettings.properties[key] = property; break; @@ -769,6 +777,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { case ConfigurationScope.MACHINE: delete machineSettings.properties[key]; break; + case ConfigurationScope.APPLICATION_MACHINE: + delete applicationMachineSettings.properties[key]; + break; case ConfigurationScope.MACHINE_OVERRIDABLE: delete machineOverridableSettings.properties[key]; break; @@ -795,6 +806,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.updatePropertyDefaultValue(overrideIdentifierProperty, resourceLanguagePropertiesSchema); allSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; applicationSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + applicationMachineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineOverridableSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; windowSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; @@ -811,6 +823,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { }; allSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; applicationSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; + applicationMachineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; machineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; machineOverridableSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; windowSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; diff --git a/code/src/vs/platform/configuration/test/common/configurationModels.test.ts b/code/src/vs/platform/configuration/test/common/configurationModels.test.ts index 9d5ff123bd6..27e025bde09 100644 --- a/code/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/code/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -366,7 +366,7 @@ suite('ConfigurationModel', () => { }); test('inspect when raw is not same', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], [{ + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { 'a': 1, 'b': 2, 'c': 1, @@ -375,7 +375,7 @@ suite('ConfigurationModel', () => { 'a': 2, 'b': 1 } - }], new NullLogService()); + }, new NullLogService()); assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); @@ -397,7 +397,7 @@ suite('ConfigurationModel', () => { }); test('inspect in merged configuration when raw is not same for one model', () => { - const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], [{ + const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { 'a': 1, 'b': 2, 'c': 3, @@ -405,7 +405,7 @@ suite('ConfigurationModel', () => { 'a': 2, 'b': 4, } - }], new NullLogService()); + }, new NullLogService()); const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, new NullLogService()); const testObject = target1.merge(target2); @@ -554,13 +554,14 @@ export class TestConfiguration extends Configuration { policyConfiguration: ConfigurationModel, applicationConfiguration: ConfigurationModel, localUserConfiguration: ConfigurationModel, + remoteUserConfiguration?: ConfigurationModel, ) { super( defaultConfiguration, policyConfiguration, applicationConfiguration, localUserConfiguration, - ConfigurationModel.createEmptyModel(new NullLogService()), + remoteUserConfiguration ?? ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), new ResourceMap(), ConfigurationModel.createEmptyModel(new NullLogService()), @@ -1116,6 +1117,225 @@ suite('ConfigurationChangeEvent', () => { }); +suite('Configuration.Parse', () => { + + const logService = new NullLogService(); + ensureNoDisposablesAreLeakedInTestSuite(); + + test('parsing configuration only with local user configuration and raw is same', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 1 }, keys: ['a'] }], undefined, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userLocal, { value: undefined, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).user, { value: undefined, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).user, undefined); + }); + + test('parsing configuration only with local user configuration and raw is not same', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 1, + 'd': 3, + '[x][y]': { + 'a': 2, + 'b': 1 + } + }, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 2, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('e', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('e', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).user, { value: 2, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('e', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is same for both', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], undefined, logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is not same for local user', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }, logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, { value: 2, override: undefined, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, { value: 2, override: 4, merged: 4, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, merged: 3, override: undefined, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is not same for remote user', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }, logService), + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 2, override: undefined, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 2, override: 4, merged: 4, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is not same for both', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], { + 'a': 4, + 'b': 3 + }, logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }, logService), + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 4, override: undefined, merged: 4, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 4, override: undefined, merged: 4, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 2, override: undefined, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 2, override: 4, merged: 4, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + +}); + function toConfigurationModel(obj: any): ConfigurationModel { const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify(obj)); diff --git a/code/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/code/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index 7a2892fd098..fe483a5a408 100644 --- a/code/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/code/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -6,12 +6,11 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; -import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService } from './extensionManagement.js'; +import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService, AllowedExtensionsConfigValueType } from './extensionManagement.js'; import { ExtensionType, IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; import { IProductService } from '../../product/common/productService.js'; import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; -import { IStringDictionary } from '../../../base/common/collections.js'; import { isBoolean, isObject, isUndefined } from '../../../base/common/types.js'; import { Emitter } from '../../../base/common/event.js'; @@ -26,17 +25,18 @@ function isIExtension(extension: any): extension is IExtension { const VersionRegex = /^(?\d+\.\d+\.\d+(-.*)?)(@(?.+))?$/; -type AllowedExtensionsConfigValueType = IStringDictionary; - export class AllowedExtensionsService extends Disposable implements IAllowedExtensionsService { _serviceBrand: undefined; - private allowedExtensions: AllowedExtensionsConfigValueType | undefined; private readonly publisherOrgs: string[]; + private _allowedExtensionsConfigValue: AllowedExtensionsConfigValueType | undefined; + get allowedExtensionsConfigValue(): AllowedExtensionsConfigValueType | undefined { + return this._allowedExtensionsConfigValue; + } private _onDidChangeAllowedExtensions = this._register(new Emitter()); - readonly onDidChangeAllowedExtensions = this._onDidChangeAllowedExtensions.event; + readonly onDidChangeAllowedExtensionsConfigValue = this._onDidChangeAllowedExtensions.event; constructor( @IProductService productService: IProductService, @@ -44,18 +44,17 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte ) { super(); this.publisherOrgs = productService.extensionPublisherOrgs?.map(p => p.toLowerCase()) ?? []; - this.allowedExtensions = this.getAllowedExtensionsValue(); + this._allowedExtensionsConfigValue = this.getAllowedExtensionsValue(); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AllowedExtensionsConfigKey)) { - this.allowedExtensions = this.getAllowedExtensionsValue(); + this._allowedExtensionsConfigValue = this.getAllowedExtensionsValue(); this._onDidChangeAllowedExtensions.fire(); } })); } private getAllowedExtensionsValue(): AllowedExtensionsConfigValueType | undefined { - const inspectValue = this.configurationService.inspect(AllowedExtensionsConfigKey); - const value = inspectValue.policyValue ?? inspectValue.userValue ?? inspectValue.defaultValue; + const value = this.configurationService.getValue(AllowedExtensionsConfigKey); if (!isObject(value) || Array.isArray(value)) { return undefined; } @@ -67,7 +66,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } isAllowed(extension: IGalleryExtension | IExtension | { id: string; publisherDisplayName: string | undefined; version?: string; prerelease?: boolean; targetPlatform?: TargetPlatform }): true | IMarkdownString { - if (!this.allowedExtensions) { + if (!this._allowedExtensionsConfigValue) { return true; } @@ -97,7 +96,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } const settingsCommandLink = URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify({ query: `@id:${AllowedExtensionsConfigKey}` }))}`).toString(); - const extensionValue = this.allowedExtensions[id]; + const extensionValue = this._allowedExtensionsConfigValue[id]; const extensionReason = new MarkdownString(nls.localize('specific extension not allowed', "it is not in the [allowed list]({0})", settingsCommandLink)); if (!isUndefined(extensionValue)) { if (isBoolean(extensionValue)) { @@ -126,7 +125,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } const publisherKey = publisherDisplayName && this.publisherOrgs.includes(publisherDisplayName) ? publisherDisplayName : publisher; - const publisherValue = this.allowedExtensions[publisherKey]; + const publisherValue = this._allowedExtensionsConfigValue[publisherKey]; if (!isUndefined(publisherValue)) { if (isBoolean(publisherValue)) { return publisherValue ? true : new MarkdownString(nls.localize('publisher not allowed', "the extensions from this publisher are not in the [allowed list]({1})", publisherKey, settingsCommandLink)); @@ -137,7 +136,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte return true; } - if (this.allowedExtensions['*'] === true) { + if (this._allowedExtensionsConfigValue['*'] === true) { return true; } diff --git a/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 7ed28238492..b8fee5a8c4c 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -10,7 +10,7 @@ import { CancellationError, getErrorMessage, isCancellationError } from '../../. import { IPager } from '../../../base/common/paging.js'; import { isWeb, platform } from '../../../base/common/platform.js'; import { arch } from '../../../base/common/process.js'; -import { isBoolean } from '../../../base/common/types.js'; +import { isBoolean, isString } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -297,14 +297,27 @@ type GalleryServiceAdditionalQueryEvent = { readonly count: number; }; -interface IExtensionCriteria { +type ExtensionsCriteria = { readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; readonly versions?: (IExtensionIdentifier & { version: string })[]; +}; + +const enum VersionKind { + Release, + Prerelease, + Latest } +type ExtensionVersionCriteria = { + readonly productVersion: IProductVersion; + readonly targetPlatform: TargetPlatform; + readonly compatible: boolean; + readonly version: VersionKind | string; +}; + class Query { constructor(private state = DefaultQueryState) { } @@ -742,25 +755,34 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return; } + if (!EXTENSION_IDENTIFIER_REGEX.test(extensionInfo.id)) { + return; + } + try { const rawGalleryExtension = await this.getLatestRawGalleryExtension(extensionInfo.id, token); if (!rawGalleryExtension) { - toQuery.push(extensionInfo); + if (extensionInfo.uuid) { + toQuery.push(extensionInfo); + } return; } - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, { - targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, - includePreRelease: !!extensionInfo.preRelease, - compatible: !!options.compatible, - productVersion: options.productVersion ?? { - version: this.productService.version, - date: this.productService.date - } - }, false); + const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); + const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( + rawGalleryExtension, + { + targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, + compatible: !!options.compatible, + productVersion: options.productVersion ?? { + version: this.productService.version, + date: this.productService.date + }, + version: extensionInfo.preRelease ? VersionKind.Prerelease : VersionKind.Release + }, allTargetPlatforms); - if (extension) { - result.push(extension); + if (rawGalleryExtensionVersion) { + result.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms)); } // report telemetry @@ -861,13 +883,30 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return areApiProposalsCompatible(enabledApiProposals); } - private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, publisherDisplayName: string, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { - const targetPlatformForExtension = getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion); - if (!isTargetPlatformCompatible(targetPlatformForExtension, allTargetPlatforms, targetPlatform)) { - return false; + private async isValidVersion( + extension: string, + rawGalleryExtensionVersion: IRawGalleryExtensionVersion, + { targetPlatform, compatible, productVersion, version }: ExtensionVersionCriteria, + publisherDisplayName: string, + allTargetPlatforms: TargetPlatform[] + ): Promise { + + // Specific version + if (isString(version)) { + if (rawGalleryExtensionVersion.version !== version) { + return false; + } } - if (versionType !== 'any' && isPreReleaseVersion(rawGalleryExtensionVersion) !== (versionType === 'prerelease')) { + // Prerelease or release version kind + else if (version === VersionKind.Release || version === VersionKind.Prerelease) { + if (isPreReleaseVersion(rawGalleryExtensionVersion) !== (version === VersionKind.Prerelease)) { + return false; + } + } + + const targetPlatformForExtension = getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion); + if (!isTargetPlatformCompatible(targetPlatformForExtension, allTargetPlatforms, targetPlatform)) { return false; } @@ -956,7 +995,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { firstPage: extensions, total, pageSize: query.pageSize, getPage }; } - private async queryGalleryExtensions(query: Query, criteria: IExtensionCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> { + private async queryGalleryExtensions(query: Query, criteria: ExtensionsCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> { const flags = query.flags; /** @@ -990,9 +1029,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (hasAllVersions) { const extensions: IGalleryExtension[] = []; for (const rawGalleryExtension of rawGalleryExtensions) { - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, true, context); - if (extension) { - extensions.push(extension); + const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); + const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; + const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( + rawGalleryExtension, + { + compatible: criteria.compatible, + targetPlatform: criteria.targetPlatform, + productVersion: criteria.productVersion, + version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version + ?? (criteria.includePreRelease ? VersionKind.Latest : VersionKind.Release) + }, + allTargetPlatforms + ); + if (rawGalleryExtensionVersion) { + extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, context)); } } return { extensions, total }; @@ -1004,22 +1055,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const rawGalleryExtension = rawGalleryExtensions[index]; const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; + const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); if (criteria.compatible) { - /** Skip if requested for a web-compatible extension and it is not a web extension. - * All versions are not needed in this case - */ - if (isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)) { + // Skip looking for all versions if requested for a web-compatible extension and it is not a web extension. + if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) { continue; } - /** - * Skip if the extension is not allowed. - * All versions are not needed in this case - */ + // Skip looking for all versions if the extension is not allowed. if (this.allowedExtensionsService.isAllowed({ id: extensionIdentifier.id, publisherDisplayName: rawGalleryExtension.publisher.displayName }) !== true) { continue; } } - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, false, context); + const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( + rawGalleryExtension, + { + compatible: criteria.compatible, + targetPlatform: criteria.targetPlatform, + productVersion: criteria.productVersion, + version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version + ?? (criteria.includePreRelease ? VersionKind.Latest : VersionKind.Release) + }, + allTargetPlatforms + ); + const extension = rawGalleryExtensionVersion ? toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, context) : null; if (!extension /** Need all versions if the extension is a pre-release version but * - the query is to look for a release version or @@ -1060,38 +1118,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total }; } - private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, hasAllVersions: boolean, queryContext?: IStringDictionary): Promise { - + private async getRawGalleryExtensionVersion(rawGalleryExtension: IRawGalleryExtension, criteria: ExtensionVersionCriteria, allTargetPlatforms: TargetPlatform[]): Promise { const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; - const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version; - const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; - const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform); if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) { return null; } + const version = isString(criteria.version) ? criteria.version : undefined; + for (let index = 0; index < rawGalleryExtensionVersions.length; index++) { const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index]; - if (version && rawGalleryExtensionVersion.version !== version) { - continue; - } - // Allow any version if includePreRelease flag is set otherwise only release versions are allowed if (await this.isValidVersion( extensionIdentifier.id, rawGalleryExtensionVersion, + criteria, rawGalleryExtension.publisher.displayName, - includePreRelease ? (hasAllVersions ? 'any' : 'prerelease') : 'release', - criteria.compatible, - allTargetPlatforms, - criteria.targetPlatform, - criteria.productVersion) + allTargetPlatforms) ) { if (criteria.compatible && !this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(rawGalleryExtensionVersion))) { continue; } - return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); + return rawGalleryExtensionVersion; } if (version && rawGalleryExtensionVersion.version === version) { return null; @@ -1106,7 +1155,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi * Fallback: Return the latest version * This can happen when the extension does not have a release version or does not have a version compatible with the given target platform. */ - return toExtension(rawGalleryExtension, rawGalleryExtension.versions[0], allTargetPlatforms); + return rawGalleryExtension.versions[0]; } private async queryRawGalleryExtensions(query: Query, token: CancellationToken): Promise { @@ -1190,16 +1239,12 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } } - private async getLatestRawGalleryExtension(extensionId: string, token: CancellationToken): Promise { + private async getLatestRawGalleryExtension(extensionId: string, token: CancellationToken): Promise { let errorCode: string | undefined; const stopWatch = new StopWatch(); try { const [publisher, name] = extensionId.split('.'); - if (!publisher || !name) { - errorCode = 'InvalidExtensionId'; - return undefined; - } const uri = URI.parse(format2(this.extensionUrlTemplate!, { publisher, name })); const commonHeaders = await this.commonHeadersPromise; const headers = { @@ -1216,20 +1261,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi timeout: 10000 /*10s*/ }, token); + if (context.res.statusCode === 404) { + errorCode = 'NotFound'; + return null; + } + if (context.res.statusCode && context.res.statusCode !== 200) { errorCode = `GalleryServiceError:` + context.res.statusCode; - this.logService.warn('Error getting latest version of the extension', extensionId, context.res.statusCode); - return undefined; + throw new Error('Unexpected HTTP response: ' + context.res.statusCode); } const result = await asJson(context); - if (result) { - return result; + if (!result) { + errorCode = 'NoData'; } - - errorCode = 'NoData'; - this.logService.warn('Error getting latest version of the extension', extensionId, errorCode); - + return result; } catch (error) { @@ -1243,6 +1289,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi ? ExtensionGalleryErrorCode.Timeout : ExtensionGalleryErrorCode.Failed; } + throw error; } finally { @@ -1260,8 +1307,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi }; this.telemetryService.publicLog2('galleryService:getLatest', { extension: extensionId, duration: stopWatch.elapsed(), errorCode }); } - - return undefined; } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { @@ -1412,17 +1457,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const validVersions: IRawGalleryExtensionVersion[] = []; + const productVersion = { version: this.productService.version, date: this.productService.date }; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { if ( (await this.isValidVersion( extensionIdentifier.id, version, + { + compatible: true, + productVersion, + targetPlatform, + version: includePreRelease ? VersionKind.Latest : VersionKind.Release + }, galleryExtensions[0].publisher.displayName, - includePreRelease ? 'any' : 'release', - true, - allTargetPlatforms, - targetPlatform)) + allTargetPlatforms)) && this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(version)) ) { validVersions.push(version); diff --git a/code/src/vs/platform/extensionManagement/common/extensionManagement.ts b/code/src/vs/platform/extensionManagement/common/extensionManagement.ts index dd719f089e9..c186beb8773 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -628,11 +628,14 @@ export interface IExtensionTipsService { getOtherExecutableBasedTips(): Promise; } +export type AllowedExtensionsConfigValueType = IStringDictionary; + export const IAllowedExtensionsService = createDecorator('IAllowedExtensionsService'); export interface IAllowedExtensionsService { readonly _serviceBrand: undefined; - readonly onDidChangeAllowedExtensions: Event; + readonly allowedExtensionsConfigValue: AllowedExtensionsConfigValueType | undefined; + readonly onDidChangeAllowedExtensionsConfigValue: Event; isAllowed(extension: IGalleryExtension | IExtension): true | IMarkdownString; isAllowed(extension: { id: string; publisherDisplayName: string | undefined; version?: string; prerelease?: boolean; targetPlatform?: TargetPlatform }): true | IMarkdownString; diff --git a/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 162f11b5c96..c0eefb92063 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -19,7 +19,6 @@ import { IUserDataProfilesService } from '../../userDataProfile/common/userDataP import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { Mutable, isObject, isString, isUndefined } from '../../../base/common/types.js'; import { getErrorMessage } from '../../../base/common/errors.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; interface IStoredProfileExtension { identifier: IExtensionIdentifier; @@ -110,7 +109,6 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable @IFileService private readonly fileService: IFileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -244,13 +242,13 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable } if (storedProfileExtensions) { if (!Array.isArray(storedProfileExtensions)) { - this.reportAndThrowInvalidConentError(file); + this.throwInvalidConentError(file); } // TODO @sandy081: Remove this migration after couple of releases let migrate = false; for (const e of storedProfileExtensions) { if (!isStoredProfileExtension(e)) { - this.reportAndThrowInvalidConentError(file); + this.throwInvalidConentError(file); } let location: URI; if (isString(e.relativeLocation) && e.relativeLocation) { @@ -302,15 +300,8 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable }); } - private reportAndThrowInvalidConentError(file: URI): void { - type ErrorClassification = { - owner: 'sandy081'; - comment: 'Information about the error that occurred while scanning'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; - }; - const error = new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT); - this.telemetryService.publicLogError2<{ code: string }, ErrorClassification>('extensionsProfileScanningError', { code: error.code }); - throw error; + private throwInvalidConentError(file: URI): void { + throw new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT); } private toRelativePath(extensionLocation: URI): string | undefined { diff --git a/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts b/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts index 2299ce88003..c9ae2f8a5e0 100644 --- a/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts +++ b/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts @@ -6,7 +6,6 @@ import { ILogService } from '../../log/common/log.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from '../common/extensionsProfileScannerService.js'; import { IFileService } from '../../files/common/files.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; @@ -19,10 +18,9 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, ) { - super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, logService); } } diff --git a/code/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts b/code/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts index 3f1e919161f..131d9185562 100644 --- a/code/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts +++ b/code/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts @@ -6,7 +6,6 @@ import { ILogService } from '../../log/common/log.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AbstractExtensionsProfileScannerService } from '../common/extensionsProfileScannerService.js'; import { IFileService } from '../../files/common/files.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; @@ -18,9 +17,8 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, ) { - super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, logService); } } diff --git a/code/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts b/code/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts index 72491b40110..3bcca170ffc 100644 --- a/code/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts +++ b/code/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts @@ -196,7 +196,7 @@ suite('AllowedExtensionsService', () => { test('should trigger change event when allowed list change', async () => { configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': false }); const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); - const promise = Event.toPromise(testObject.onDidChangeAllowedExtensions); + const promise = Event.toPromise(testObject.onDidChangeAllowedExtensionsConfigValue); configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true, affectedKeys: new Set([AllowedExtensionsConfigKey]), change: { keys: [], overrides: [] }, source: ConfigurationTarget.USER }); await promise; }); diff --git a/code/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/code/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index ea4108b2c17..1d349f5822a 100644 --- a/code/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/code/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -19,7 +19,6 @@ import { IInstantiationService } from '../../../instantiation/common/instantiati import { TestInstantiationService } from '../../../instantiation/test/common/instantiationServiceMock.js'; import { ILogService, NullLogService } from '../../../log/common/log.js'; import { IProductService } from '../../../product/common/productService.js'; -import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; import { IUriIdentityService } from '../../../uriIdentity/common/uriIdentity.js'; import { UriIdentityService } from '../../../uriIdentity/common/uriIdentityService.js'; import { IUserDataProfilesService, UserDataProfilesService } from '../../../userDataProfile/common/userDataProfile.js'; @@ -81,7 +80,7 @@ suite('NativeExtensionsScanerService Test', () => { instantiationService.stub(IUriIdentityService, uriIdentityService); const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); instantiationService.stub(IUserDataProfilesService, userDataProfilesService); - instantiationService.stub(IExtensionsProfileScannerService, disposables.add(new ExtensionsProfileScannerService(environmentService, fileService, userDataProfilesService, uriIdentityService, NullTelemetryService, logService))); + instantiationService.stub(IExtensionsProfileScannerService, disposables.add(new ExtensionsProfileScannerService(environmentService, fileService, userDataProfilesService, uriIdentityService, logService))); await fileService.createFolder(systemExtensionsLocation); await fileService.createFolder(userExtensionsLocation); }); diff --git a/code/src/vs/platform/extensions/common/extensions.ts b/code/src/vs/platform/extensions/common/extensions.ts index d6758a9b54d..b2d6194419a 100644 --- a/code/src/vs/platform/extensions/common/extensions.ts +++ b/code/src/vs/platform/extensions/common/extensions.ts @@ -111,9 +111,10 @@ export interface IWalkthroughStep { readonly title: string; readonly description: string | undefined; readonly media: - | { image: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; svg?: never } - | { markdown: string; image?: never; svg?: never } - | { svg: string; altText: string; markdown?: never; image?: never }; + | { image: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; svg?: never; video?: never } + | { markdown: string; image?: never; svg?: never; video?: never } + | { svg: string; altText: string; markdown?: never; image?: never; video?: never } + | { video: string | { dark: string; light: string; hc: string }; poster: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; image?: never; svg?: never }; readonly completionEvents?: string[]; /** @deprecated use `completionEvents: 'onCommand:...'` */ readonly doneOn?: { command: string }; diff --git a/code/src/vs/platform/extensions/common/extensionsApiProposals.ts b/code/src/vs/platform/extensions/common/extensionsApiProposals.ts index cced7c27af0..79fc43225b9 100644 --- a/code/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/code/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -38,6 +38,9 @@ const _allApiProposals = { chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', }, + chatReadonlyPromptReference: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts', + }, chatReferenceBinaryData: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts', }, @@ -319,6 +322,9 @@ const _allApiProposals = { speech: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', }, + statusBarItemTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts', + }, tabInputMultiDiff: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', }, @@ -349,6 +355,12 @@ const _allApiProposals = { terminalSelection: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', }, + terminalShellEnv: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts', + }, + terminalShellType: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellType.d.ts', + }, testObserver: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', }, diff --git a/code/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/code/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index d2b0da2e857..5da9d072880 100644 --- a/code/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/code/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -11,24 +11,10 @@ import { ExtUri } from '../../../base/common/resources.js'; import { isString } from '../../../base/common/types.js'; import { URI, UriDto } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; -import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from '../common/files.js'; -import { DBClosedError, IndexedDB } from '../../../base/browser/indexedDB.js'; +import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from '../common/files.js'; +import { IndexedDB } from '../../../base/browser/indexedDB.js'; import { BroadcastDataChannel } from '../../../base/browser/broadcast.js'; -export type IndexedDBFileSystemProviderErrorDataClassification = { - owner: 'sandy081'; - comment: 'Information about errors that occur in the IndexedDB file system provider'; - readonly scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'IndexedDB file system provider scheme for which this error occurred' }; - readonly operation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'operation during which this error occurred' }; - readonly code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'error code' }; -}; - -export type IndexedDBFileSystemProviderErrorData = { - readonly scheme: string; - readonly operation: string; - readonly code: string; -}; - // Standard FS Errors (expected to be thrown in production when invalid FS operations are requested) const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); @@ -179,9 +165,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst private readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChangeFile.event; - private readonly _onReportError = this._register(new Emitter()); - readonly onReportError = this._onReportError.event; - private readonly mtimes = new Map(); private cachedFiletree: Promise | undefined; @@ -255,7 +238,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst return [...entry.children.entries()].map(([name, node]) => [name, node.type]); } } catch (error) { - this.reportError('readDir', error); throw error; } } @@ -277,7 +259,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst return buffer; } catch (error) { - this.reportError('readFile', error); throw error; } } @@ -290,7 +271,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst } await this.bulkWrite([[resource, content]]); } catch (error) { - this.reportError('writeFile', error); throw error; } } @@ -452,8 +432,4 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst await this.indexedDB.runInTransaction(this.store, 'readwrite', objectStore => objectStore.clear()); } - private reportError(operation: string, error: Error): void { - this._onReportError.fire({ scheme: this.scheme, operation, code: error instanceof FileSystemProviderError || error instanceof DBClosedError ? error.code : 'unknown' }); - } - } diff --git a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 35ea5eea71f..b016dde0a9f 100644 --- a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -17,6 +17,7 @@ import { realpath } from '../../../../../base/node/extpath.js'; import { Promises } from '../../../../../base/node/pfs.js'; import { FileChangeType, IFileChange } from '../../../common/files.js'; import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from '../../../common/watcher.js'; +import { Lazy } from '../../../../../base/common/lazy.js'; export class NodeJSFileWatcherLibrary extends Disposable { @@ -55,6 +56,28 @@ export class NodeJSFileWatcherLibrary extends Disposable { private readonly cts = new CancellationTokenSource(); + private readonly realPath = new Lazy(async () => { + + // This property is intentionally `Lazy` and not using `realcase()` as the counterpart + // in the recursive watcher because of the amount of paths this watcher is dealing with. + // We try as much as possible to avoid even needing `realpath()` if we can because even + // that method does an `lstat()` per segment of the path. + + let result = this.request.path; + + try { + result = await realpath(this.request.path); + + if (this.request.path !== result) { + this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${this.request.path}, real: ${result})`); + } + } catch (error) { + // ignore + } + + return result; + }); + readonly ready = this.watch(); private _isReusingRecursiveWatcher = false; @@ -76,19 +99,13 @@ export class NodeJSFileWatcherLibrary extends Disposable { private async watch(): Promise { try { - const realPath = await this.normalizePath(this.request); + const stat = await promises.stat(this.request.path); if (this.cts.token.isCancellationRequested) { return; } - const stat = await promises.stat(realPath); - - if (this.cts.token.isCancellationRequested) { - return; - } - - this._register(await this.doWatch(realPath, stat.isDirectory())); + this._register(await this.doWatch(stat.isDirectory())); } catch (error) { if (error.code !== 'ENOENT') { this.error(error); @@ -106,46 +123,21 @@ export class NodeJSFileWatcherLibrary extends Disposable { this.onDidWatchFail?.(); } - private async normalizePath(request: INonRecursiveWatchRequest): Promise { - let realPath = request.path; - - try { - - // Check for symbolic link - realPath = await realpath(request.path); - - // Note: we used to also call `realcase()` here, but - // that operation is very expensive for large amounts - // of files and is actually not needed for single - // file/folder watching where we report on the original - // path anyway. - // (https://github.com/microsoft/vscode/issues/237351) - - if (request.path !== realPath) { - this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realPath})`); - } - } catch (error) { - // ignore - } - - return realPath; - } - - private async doWatch(realPath: string, isDirectory: boolean): Promise { + private async doWatch(isDirectory: boolean): Promise { const disposables = new DisposableStore(); - if (this.doWatchWithExistingWatcher(realPath, isDirectory, disposables)) { + if (this.doWatchWithExistingWatcher(isDirectory, disposables)) { this.trace(`reusing an existing recursive watcher for ${this.request.path}`); this._isReusingRecursiveWatcher = true; } else { this._isReusingRecursiveWatcher = false; - await this.doWatchWithNodeJS(realPath, isDirectory, disposables); + await this.doWatchWithNodeJS(isDirectory, disposables); } return disposables; } - private doWatchWithExistingWatcher(realPath: string, isDirectory: boolean, disposables: DisposableStore): boolean { + private doWatchWithExistingWatcher(isDirectory: boolean, disposables: DisposableStore): boolean { if (isDirectory) { // Recursive watcher re-use is currently not enabled for when // folders are watched. this is because the dispatching in the @@ -162,7 +154,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { } if (error) { - const watchDisposable = await this.doWatch(realPath, isDirectory); + const watchDisposable = await this.doWatch(isDirectory); if (!disposables.isDisposed) { disposables.add(watchDisposable); } else { @@ -188,7 +180,8 @@ export class NodeJSFileWatcherLibrary extends Disposable { return false; } - private async doWatchWithNodeJS(realPath: string, isDirectory: boolean, disposables: DisposableStore): Promise { + private async doWatchWithNodeJS(isDirectory: boolean, disposables: DisposableStore): Promise { + const realPath = await this.realPath.value; // macOS: watching samba shares can crash VSCode so we do // a simple check for the file path pointing to /Volumes @@ -407,7 +400,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { if (fileExists) { this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); - watcherDisposables.add(await this.doWatch(realPath, false)); + watcherDisposables.add(await this.doWatch(false)); } // File seems to be really gone, so emit a deleted and failed event diff --git a/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index f6eea7fb0e5..c778b998140 100644 --- a/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import parcelWatcher from '@parcel/watcher'; -import { statSync, unlinkSync } from 'fs'; +import { promises } from 'fs'; import { tmpdir, homedir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js'; @@ -18,7 +18,7 @@ import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree. import { normalizeNFC } from '../../../../../base/common/normalization.js'; import { normalize, join } from '../../../../../base/common/path.js'; import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js'; -import { realcaseSync, realpathSync } from '../../../../../base/node/extpath.js'; +import { realcase, realpath } from '../../../../../base/node/extpath.js'; import { FileChangeType, IFileChange } from '../../../common/files.js'; import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; @@ -201,7 +201,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { // Figure out duplicates to remove from the requests - requests = this.removeDuplicateRequests(requests); + requests = await this.removeDuplicateRequests(requests); // Figure out which watchers to start and which to stop const requestsToStart: IRecursiveWatchRequest[] = []; @@ -232,7 +232,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Start watching as instructed for (const request of requestsToStart) { if (request.pollingInterval) { - this.startPolling(request, request.pollingInterval); + await this.startPolling(request, request.pollingInterval); } else { await this.startWatching(request); } @@ -247,7 +247,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS return isLinux ? path : path.toLowerCase() /* ignore path casing */; } - private startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): void { + private async startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): Promise { const cts = new CancellationTokenSource(); const instance = new DeferredPromise(); @@ -268,13 +268,13 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS watcher.worker.dispose(); pollingWatcher.dispose(); - unlinkSync(snapshotFile); + await promises.unlink(snapshotFile); } ); this._watchers.set(this.requestToWatcherKey(request), watcher); // Path checks for symbolic links / wrong casing - const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); + const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request); this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`); @@ -343,7 +343,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS this._watchers.set(this.requestToWatcherKey(request), watcher); // Path checks for symbolic links / wrong casing - const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); + const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request); try { const parcelWatcherLib = parcelWatcher; @@ -471,7 +471,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } } - private normalizePath(request: IRecursiveWatchRequest): { realPath: string; realPathDiffers: boolean; realPathLength: number } { + private async normalizePath(request: IRecursiveWatchRequest): Promise<{ realPath: string; realPathDiffers: boolean; realPathLength: number }> { let realPath = request.path; let realPathDiffers = false; let realPathLength = request.path.length; @@ -479,12 +479,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS try { // First check for symbolic link - realPath = realpathSync(request.path); + realPath = await realpath(request.path); // Second check for casing difference // Note: this will be a no-op on Linux platforms if (request.path === realPath) { - realPath = realcaseSync(request.path) ?? request.path; + realPath = await realcase(request.path) ?? request.path; } // Correct watch path as needed @@ -615,7 +615,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Start watcher again counting the restarts if (watcher.request.pollingInterval) { - this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); + await this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); } else { await this.startWatching(watcher.request, watcher.restarts + 1); } @@ -640,7 +640,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } } - protected removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] { + protected async removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): Promise { // Sort requests by path length to have shortest first // to have a way to prevent children to be watched if @@ -686,26 +686,29 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS for (const request of requestsForCorrelation.values()) { - // Check for overlapping requests + // Check for overlapping request paths (but preserve symbolic links) if (requestTrie.findSubstr(request.path)) { - try { - const realpath = realpathSync(request.path); - if (realpath === request.path) { - this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`); + if (requestTrie.has(request.path)) { + this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`); + } else { + try { + if (!(await promises.lstat(request.path)).isSymbolicLink()) { + this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`); - continue; - } - } catch (error) { - this.trace(`ignoring a request for watching who's realpath failed to resolve: ${this.requestToString(request)} (error: ${error})`); + continue; + } + } catch (error) { + this.trace(`ignoring a request for watching who's lstat failed to resolve: ${this.requestToString(request)} (error: ${error})`); - this._onDidWatchFail.fire(request); + this._onDidWatchFail.fire(request); - continue; + continue; + } } } // Check for invalid paths - if (validatePaths && !this.isPathValid(request.path)) { + if (validatePaths && !(await this.isPathValid(request.path))) { this._onDidWatchFail.fire(request); continue; @@ -720,9 +723,9 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS return normalizedRequests; } - private isPathValid(path: string): boolean { + private async isPathValid(path: string): Promise { try { - const stat = statSync(path); + const stat = await promises.stat(path); if (!stat.isDirectory()) { this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`); diff --git a/code/src/vs/platform/files/test/node/nodejsWatcher.test.ts b/code/src/vs/platform/files/test/node/nodejsWatcher.test.ts index 9a234b80cf5..2e748a7c532 100644 --- a/code/src/vs/platform/files/test/node/nodejsWatcher.test.ts +++ b/code/src/vs/platform/files/test/node/nodejsWatcher.test.ts @@ -72,7 +72,11 @@ suite.skip('File Watcher (node.js)', function () { setup(async () => { await createWatcher(undefined); - testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath; + // Rule out strange testing conditions by using the realpath + // here. for example, on macOS the tmp dir is potentially a + // symlink in some of the root folders, which is a rather + // unrealisic case for the file watcher. + testDir = URI.file(getRandomTestPath(fs.realpathSync(tmpdir()), 'vsctests', 'filewatcher')).fsPath; const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; diff --git a/code/src/vs/platform/files/test/node/parcelWatcher.test.ts b/code/src/vs/platform/files/test/node/parcelWatcher.test.ts index 8b1824af3bd..e2038a17f50 100644 --- a/code/src/vs/platform/files/test/node/parcelWatcher.test.ts +++ b/code/src/vs/platform/files/test/node/parcelWatcher.test.ts @@ -32,14 +32,14 @@ export class TestParcelWatcher extends ParcelWatcher { readonly onWatchFail = this._onDidWatchFail.event; - testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] { + async testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): Promise { // Work with strings as paths to simplify testing const requests: IRecursiveWatchRequest[] = paths.map(path => { return { path, excludes, recursive: true }; }); - return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); + return (await this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */)).map(request => request.path); } protected override getUpdateWatchersDelay(): number { @@ -97,7 +97,11 @@ suite.skip('File Watcher (parcel)', function () { } }); - testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath; + // Rule out strange testing conditions by using the realpath + // here. for example, on macOS the tmp dir is potentially a + // symlink in some of the root folders, which is a rather + // unrealisic case for the file watcher. + testDir = URI.file(getRandomTestPath(realpathSync(tmpdir()), 'vsctests', 'filewatcher')).fsPath; const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; @@ -636,34 +640,34 @@ suite.skip('File Watcher (parcel)', function () { return basicCrudTest(join(testDir, 'newFile.txt'), correlationId); }); - test('should not exclude roots that do not overlap', () => { + test('should not exclude roots that do not overlap', async () => { if (isWindows) { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a']), ['/a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a']), ['/a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); } }); - test('should remove sub-folders of other paths', () => { + test('should remove sub-folders of other paths', async () => { if (isWindows) { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']); } }); - test('should ignore when everything excluded', () => { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []); + test('should ignore when everything excluded', async () => { + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []); }); test('watching same or overlapping paths supported when correlation is applied', async () => { @@ -786,17 +790,19 @@ suite.skip('File Watcher (parcel)', function () { const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath); - onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await Promises.rm(folderPath); - await onDidWatchFail; + if (!reuseExistingWatcher) { + onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); - onDidWatch = Event.toPromise(watcher.onDidWatch); - await promises.mkdir(folderPath); - await changeFuture; - await onDidWatch; + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; - await basicCrudTest(filePath); + await basicCrudTest(filePath); + } } (isWindows /* Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { @@ -820,17 +826,19 @@ suite.skip('File Watcher (parcel)', function () { const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath); - const onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await Promises.rm(folderPath); - await onDidWatchFail; + if (!reuseExistingWatcher) { + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; - const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); - const onDidWatch = Event.toPromise(watcher.onDidWatch); - await promises.mkdir(folderPath); - await changeFuture; - await onDidWatch; + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); + const onDidWatch = Event.toPromise(watcher.onDidWatch); + await promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; - await basicCrudTest(filePath); + await basicCrudTest(filePath); + } } test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () { diff --git a/code/src/vs/platform/list/browser/listService.ts b/code/src/vs/platform/list/browser/listService.ts index 4bedb6c7de0..94c2acead57 100644 --- a/code/src/vs/platform/list/browser/listService.ts +++ b/code/src/vs/platform/list/browser/listService.ts @@ -90,11 +90,6 @@ export class ListService implements IListService { return combinedDisposable( widget.onDidFocus(() => this.setLastFocusedList(widget)), - widget.onDidBlur(() => { - if (this._lastFocusedWidget === widget) { - this.setLastFocusedList(undefined); - } - }), toDisposable(() => this.lists.splice(this.lists.indexOf(registeredList), 1)), widget.onDidDispose(() => { this.lists = this.lists.filter(l => l !== registeredList); @@ -865,6 +860,7 @@ export interface IWorkbenchObjectTreeOptions extends IObjectTree readonly accessibilityProvider: IListAccessibilityProvider; readonly overrideStyles?: IStyleOverride; readonly selectionNavigation?: boolean; + readonly scrollToActiveElement?: boolean; } export class WorkbenchObjectTree, TFilterData = void> extends ObjectTree { diff --git a/code/src/vs/platform/log/common/log.ts b/code/src/vs/platform/log/common/log.ts index 7ce341461a4..b5780a16134 100644 --- a/code/src/vs/platform/log/common/log.ts +++ b/code/src/vs/platform/log/common/log.ts @@ -91,6 +91,11 @@ function format(args: any, verbose: boolean = false): string { return result; } +export type LoggerGroup = { + readonly id: string; + readonly name: string; +}; + export interface ILogService extends ILogger { readonly _serviceBrand: undefined; } @@ -136,6 +141,11 @@ export interface ILoggerOptions { * Id of the extension that created this logger. */ extensionId?: string; + + /** + * Group of the logger. + */ + group?: LoggerGroup; } export interface ILoggerResource { @@ -146,6 +156,7 @@ export interface ILoggerResource { readonly hidden?: boolean; readonly when?: string; readonly extensionId?: string; + readonly group?: LoggerGroup; } export type DidChangeLoggersEvent = { @@ -616,7 +627,16 @@ export abstract class AbstractLoggerService extends Disposable implements ILogge } const loggerEntry: LoggerEntry = { logger, - info: { resource, id, logLevel, name: options?.name, hidden: options?.hidden, extensionId: options?.extensionId, when: options?.when } + info: { + resource, + id, + logLevel, + name: options?.name, + hidden: options?.hidden, + group: options?.group, + extensionId: options?.extensionId, + when: options?.when + } }; this.registerLogger(loggerEntry.info); // TODO: @sandy081 Remove this once registerLogger can take ILogger diff --git a/code/src/vs/platform/native/electron-main/auth.ts b/code/src/vs/platform/native/electron-main/auth.ts index 34adcc69f69..dfac2bf96a8 100644 --- a/code/src/vs/platform/native/electron-main/auth.ts +++ b/code/src/vs/platform/native/electron-main/auth.ts @@ -139,20 +139,13 @@ export class ProxyAuthService extends Disposable implements IProxyAuthService { // For testing. if (this.environmentMainService.extensionTestsLocationURI) { - const credentials = this.configurationService.getValue('integration-test.http.proxyAuth'); - if (credentials) { - const j = credentials.indexOf(':'); - if (j !== -1) { - return { - username: credentials.substring(0, j), - password: credentials.substring(j + 1) - }; - } else { - return { - username: credentials, - password: '' - }; + try { + const decodedRealm = Buffer.from(authInfo.realm, 'base64').toString('utf-8'); + if (decodedRealm.startsWith('{')) { + return JSON.parse(decodedRealm); } + } catch { + // ignore } return undefined; } diff --git a/code/src/vs/platform/native/electron-main/nativeHostMainService.ts b/code/src/vs/platform/native/electron-main/nativeHostMainService.ts index 030b6a27506..56263e7fdad 100644 --- a/code/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/code/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -323,7 +323,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): Promise { - this.themeMainService.saveWindowSplash(windowId, splash); + const window = this.codeWindowById(windowId); + + this.themeMainService.saveWindowSplash(windowId, window?.openedWorkspace, splash); } async overrideDefaultTitlebarStyle(windowId: number | undefined, style: 'custom' | undefined): Promise { @@ -870,12 +872,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Connectivity async resolveProxy(windowId: number | undefined, url: string): Promise { - if (this.environmentMainService.extensionTestsLocationURI) { - const testProxy = this.configurationService.getValue('integration-test.http.proxy'); - if (testProxy) { - return testProxy; - } - } const window = this.codeWindowById(windowId); const session = window?.win?.webContents?.session; diff --git a/code/src/vs/platform/quickinput/browser/quickInput.ts b/code/src/vs/platform/quickinput/browser/quickInput.ts index 432416a5a11..1ea9c37ab5f 100644 --- a/code/src/vs/platform/quickinput/browser/quickInput.ts +++ b/code/src/vs/platform/quickinput/browser/quickInput.ts @@ -38,6 +38,9 @@ export const inQuickInputContextKeyValue = 'inQuickInput'; export const InQuickInputContextKey = new RawContextKey(inQuickInputContextKeyValue, false, localize('inQuickInput', "Whether keyboard focus is inside the quick input control")); export const inQuickInputContext = ContextKeyExpr.has(inQuickInputContextKeyValue); +export const quickInputAlignmentContextKeyValue = 'quickInputAlignment'; +export const QuickInputAlignmentContextKey = new RawContextKey<'top' | 'center' | undefined>(quickInputAlignmentContextKeyValue, 'top', localize('quickInputAlignment', "The alignment of the quick input")); + export const quickInputTypeContextKeyValue = 'quickInputType'; export const QuickInputTypeContextKey = new RawContextKey(quickInputTypeContextKeyValue, undefined, localize('quickInputType', "The type of the currently visible quick input")); diff --git a/code/src/vs/platform/quickinput/browser/quickInputController.ts b/code/src/vs/platform/quickinput/browser/quickInputController.ts index 783dde2dd5b..27061cd9d81 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputController.ts @@ -19,7 +19,7 @@ import { isString } from '../../../base/common/types.js'; import { localize } from '../../../nls.js'; import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput, QuickPickFocus } from '../common/quickInput.js'; import { QuickInputBox } from './quickInputBox.js'; -import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey } from './quickInput.js'; +import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey, QuickInputAlignmentContextKey } from './quickInput.js'; import { ILayoutService } from '../../layout/browser/layoutService.js'; import { mainWindow } from '../../../base/browser/window.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; @@ -29,6 +29,10 @@ import './quickInputActions.js'; import { autorun, observableValue } from '../../../base/common/observable.js'; import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { Platform, platform } from '../../../base/common/platform.js'; +import { TitleBarSetting, TitlebarStyle } from '../../window/common/window.js'; +import { getZoomFactor } from '../../../base/browser/browser.js'; const $ = dom.$; @@ -331,7 +335,21 @@ export class QuickInputController extends Disposable { // Drag and Drop support this.dndController = this._register(this.instantiationService.createInstance( - QuickInputDragAndDropController, this._container, container, [titleBar, title, headerContainer])); + QuickInputDragAndDropController, + this._container, + container, + [ + { + node: titleBar, + includeChildren: true + }, + { + node: headerContainer, + includeChildren: false + } + ], + this.viewState + )); // DnD update layout this._register(autorun(reader => { @@ -607,6 +625,10 @@ export class QuickInputController extends Disposable { return new InputBox(ui); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { + this.dndController?.setAlignment(alignment); + } + createQuickWidget(): IQuickWidget { const ui = this.getUI(true); return new QuickWidget(ui); @@ -649,6 +671,7 @@ export class QuickInputController extends Disposable { ui.container.style.display = ''; this.updateLayout(); + this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); } @@ -879,132 +902,186 @@ class QuickInputDragAndDropController extends Disposable { readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined); private readonly _snapThreshold = 20; - private readonly _snapLineHorizontalRatio = 0.15; - private readonly _snapLineHorizontal: HTMLElement; - private readonly _snapLineVertical1: HTMLElement; - private readonly _snapLineVertical2: HTMLElement; + private readonly _snapLineHorizontalRatio = 0.25; + + private readonly _controlsOnLeft: boolean; + private readonly _controlsOnRight: boolean; + + private _quickInputAlignmentContext = QuickInputAlignmentContextKey.bindTo(this._contextKeyService); constructor( private _container: HTMLElement, private readonly _quickInputContainer: HTMLElement, - private _quickInputDragAreas: HTMLElement[], - @ILayoutService private readonly _layoutService: ILayoutService + private _quickInputDragAreas: { node: HTMLElement; includeChildren: boolean }[], + initialViewState: QuickInputViewState | undefined, + @ILayoutService private readonly _layoutService: ILayoutService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); + const customTitleBar = this.configurationService.getValue(TitleBarSetting.TITLE_BAR_STYLE) === TitlebarStyle.CUSTOM; - this._snapLineHorizontal = dom.append(this._container, $('.quick-input-widget-snapline.horizontal.hidden')); - this._snapLineVertical1 = dom.append(this._container, $('.quick-input-widget-snapline.vertical.hidden')); - this._snapLineVertical2 = dom.append(this._container, $('.quick-input-widget-snapline.vertical.hidden')); - + // Do not allow the widget to overflow or underflow window controls. + // Use CSS calculations to avoid having to force layout with `.clientWidth` + this._controlsOnLeft = customTitleBar && platform === Platform.Mac; + this._controlsOnRight = customTitleBar && (platform === Platform.Windows || platform === Platform.Linux); + this._registerLayoutListener(); this.registerMouseListeners(); + this.dndViewState.set({ ...initialViewState, done: true }, undefined); } reparentUI(container: HTMLElement): void { this._container = container; - this._snapLineHorizontal.remove(); - this._snapLineVertical1.remove(); - this._snapLineVertical2.remove(); - dom.append(this._container, this._snapLineHorizontal); - dom.append(this._container, this._snapLineVertical1); - dom.append(this._container, this._snapLineVertical2); } - private registerMouseListeners(): void { - for (const dragArea of this._quickInputDragAreas) { - let top: number | undefined; - let left: number | undefined; + layoutContainer(dimension = this._layoutService.activeContainerDimension): void { + const state = this.dndViewState.get(); + const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); + if (state?.top && state?.left) { + const a = Math.round(state.left * 1e2) / 1e2; + const b = dimension.width; + const c = dragAreaRect.width; + const d = a * b - c / 2; + this._layout(state.top * dimension.height, d); + } + } - // Double click - this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { - const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void { + if (alignment === 'top') { + this.dndViewState.set({ + top: this._getTopSnapValue() / this._container.clientHeight, + left: (this._getCenterXSnapValue() + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth, + done + }, undefined); + this._quickInputAlignmentContext.set('top'); + } else if (alignment === 'center') { + this.dndViewState.set({ + top: this._getCenterYSnapValue() / this._container.clientHeight, + left: (this._getCenterXSnapValue() + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth, + done + }, undefined); + this._quickInputAlignmentContext.set('center'); + } else { + this.dndViewState.set({ top: alignment.top, left: alignment.left, done }, undefined); + this._quickInputAlignmentContext.set(undefined); + } + } - // Ignore event if the target is not the drag area - if (originEvent.target !== dragArea) { - return; - } + private _registerLayoutListener() { + this._register(Event.filter(this._layoutService.onDidLayoutContainer, e => e.container === this._container)((e) => this.layoutContainer(e.dimension))); + } - if (originEvent.detail === 2) { - top = undefined; - left = undefined; + private registerMouseListeners(): void { + const dragArea = this._quickInputContainer; - this.dndViewState.set({ top, left, done: true }, undefined); - } - })); + // Double click + this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { + const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); + if (originEvent.detail !== 2) { + return; + } - // Mouse down - this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { - const activeWindow = dom.getWindow(this._layoutService.activeContainer); - const originEvent = new StandardMouseEvent(activeWindow, e); + // Ignore event if the target is not the drag area + if (!this._quickInputDragAreas.some(({ node, includeChildren }) => includeChildren ? dom.isAncestor(originEvent.target as HTMLElement, node) : originEvent.target === node)) { + return; + } - // Ignore event if the target is not the drag area - if (originEvent.target !== dragArea) { - return; - } + this.dndViewState.set({ top: undefined, left: undefined, done: true }, undefined); + })); - // Mouse position offset relative to dragArea - const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); - const dragOffsetX = originEvent.browserEvent.clientX - dragAreaRect.left; - const dragOffsetY = originEvent.browserEvent.clientY - dragAreaRect.top; - - // Snap lines - let snapLinesVisible = false; - const snapCoordinateYTop = this._layoutService.activeContainerOffset.quickPickTop; - const snapCoordinateY = Math.round(this._container.clientHeight * this._snapLineHorizontalRatio); - const snapCoordinateX = Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); - - // Mouse move - const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { - const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); - mouseMoveEvent.preventDefault(); - - if (!snapLinesVisible) { - this._showSnapLines(snapCoordinateY, snapCoordinateX); - snapLinesVisible = true; - } + // Mouse down + this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { + const activeWindow = dom.getWindow(this._layoutService.activeContainer); + const originEvent = new StandardMouseEvent(activeWindow, e); - let topCoordinate = e.clientY - dragOffsetY; - topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); - topCoordinate = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold ? snapCoordinateYTop : topCoordinate; - topCoordinate = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold ? snapCoordinateY : topCoordinate; - top = topCoordinate / this._container.clientHeight; + // Ignore event if the target is not the drag area + if (!this._quickInputDragAreas.some(({ node, includeChildren }) => includeChildren ? dom.isAncestor(originEvent.target as HTMLElement, node) : originEvent.target === node)) { + return; + } - let leftCoordinate = e.clientX - dragOffsetX; - leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); - leftCoordinate = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold ? snapCoordinateX : leftCoordinate; - left = (leftCoordinate + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth; + // Mouse position offset relative to dragArea + const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); + const dragOffsetX = originEvent.browserEvent.clientX - dragAreaRect.left; + const dragOffsetY = originEvent.browserEvent.clientY - dragAreaRect.top; - this.dndViewState.set({ top, left, done: false }, undefined); - }); + let isMovingQuickInput = false; + const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { + const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); + mouseMoveEvent.preventDefault(); - // Mouse up - const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { - // Hide snaplines - this._hideSnapLines(); + if (!isMovingQuickInput) { + isMovingQuickInput = true; + } + this._layout(e.clientY - dragOffsetY, e.clientX - dragOffsetX); + }); + const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { + if (isMovingQuickInput) { // Save position - this.dndViewState.set({ top, left, done: true }, undefined); + const state = this.dndViewState.get(); + this.dndViewState.set({ top: state?.top, left: state?.left, done: true }, undefined); + } - // Dispose listeners - mouseMoveListener.dispose(); - mouseUpListener.dispose(); - }); - })); + // Dispose listeners + mouseMoveListener.dispose(); + mouseUpListener.dispose(); + }); + })); + } + + private _layout(topCoordinate: number, leftCoordinate: number) { + const snapCoordinateYTop = this._getTopSnapValue(); + const snapCoordinateY = this._getCenterYSnapValue(); + const snapCoordinateX = this._getCenterXSnapValue(); + // Make sure the quick input is not moved outside the container + topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); + + if (topCoordinate < this._layoutService.activeContainerOffset.top) { + if (this._controlsOnLeft) { + leftCoordinate = Math.max(leftCoordinate, 80 / getZoomFactor(dom.getActiveWindow())); + } else if (this._controlsOnRight) { + leftCoordinate = Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth - (140 / getZoomFactor(dom.getActiveWindow()))); + } } + + const snappingToTop = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold; + topCoordinate = snappingToTop ? snapCoordinateYTop : topCoordinate; + const snappingToCenter = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold; + topCoordinate = snappingToCenter ? snapCoordinateY : topCoordinate; + const top = topCoordinate / this._container.clientHeight; + + // Make sure the quick input is not moved outside the container + leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); + const snappingToCenterX = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold; + leftCoordinate = snappingToCenterX ? snapCoordinateX : leftCoordinate; + + const b = this._container.clientWidth; + const c = this._quickInputContainer.clientWidth; + const d = leftCoordinate; + const left = (d + c / 2) / b; + + this.dndViewState.set({ top, left, done: false }, undefined); + if (snappingToCenterX) { + if (snappingToTop) { + this._quickInputAlignmentContext.set('top'); + return; + } else if (snappingToCenter) { + this._quickInputAlignmentContext.set('center'); + return; + } + } + this._quickInputAlignmentContext.set(undefined); } - private _showSnapLines(horizontal: number, vertical: number) { - this._snapLineHorizontal.style.top = `${horizontal}px`; - this._snapLineVertical1.style.left = `${vertical}px`; - this._snapLineVertical2.style.left = `${vertical + this._quickInputContainer.clientWidth}px`; + private _getTopSnapValue() { + return this._layoutService.activeContainerOffset.quickPickTop; + } - this._snapLineHorizontal.classList.remove('hidden'); - this._snapLineVertical1.classList.remove('hidden'); - this._snapLineVertical2.classList.remove('hidden'); + private _getCenterYSnapValue() { + return Math.round(this._container.clientHeight * this._snapLineHorizontalRatio); } - private _hideSnapLines() { - this._snapLineHorizontal.classList.add('hidden'); - this._snapLineVertical1.classList.add('hidden'); - this._snapLineVertical2.classList.add('hidden'); + private _getCenterXSnapValue() { + return Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); } } diff --git a/code/src/vs/platform/quickinput/browser/quickInputService.ts b/code/src/vs/platform/quickinput/browser/quickInputService.ts index ccdb6965563..8adc5abd707 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputService.ts @@ -195,6 +195,10 @@ export class QuickInputService extends Themable implements IQuickInputService { return this.controller.cancel(); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { + this.controller.setAlignment(alignment); + } + override updateStyles() { if (this.hasController) { this.controller.applyStyles(this.computeStyles()); diff --git a/code/src/vs/platform/quickinput/common/quickInput.ts b/code/src/vs/platform/quickinput/common/quickInput.ts index 39490182051..a8aa5bca251 100644 --- a/code/src/vs/platform/quickinput/common/quickInput.ts +++ b/code/src/vs/platform/quickinput/common/quickInput.ts @@ -931,4 +931,10 @@ export interface IQuickInputService { * The current quick pick that is visible. Undefined if none is open. */ currentQuickInput: IQuickInput | undefined; + + /** + * Set the alignment of the quick input. + * @param alignment either a preset or a custom alignment + */ + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void; } diff --git a/code/src/vs/platform/request/common/request.ts b/code/src/vs/platform/request/common/request.ts index 2c85ab8c043..fddb00f6597 100644 --- a/code/src/vs/platform/request/common/request.ts +++ b/code/src/vs/platform/request/common/request.ts @@ -75,7 +75,7 @@ export abstract class AbstractRequestService extends Disposable implements IRequ } protected async logAndRequest(options: IRequestOptions, request: () => Promise): Promise { - const prefix = `[network] #${++this.counter}: ${options.url}`; + const prefix = `#${++this.counter}: ${options.url}`; this.logService.trace(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {})); try { const result = await request(); @@ -134,91 +134,138 @@ export async function asJson(context: IRequestContext): Promise(Extensions.Configuration); const oldProxyConfiguration = proxyConfiguration; - proxyConfiguration = { - id: 'http', - order: 15, - title: localize('httpConfigurationTitle', "HTTP"), - type: 'object', - scope, - properties: { - 'http.proxy': { - type: 'string', - pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', - markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables."), - restricted: true - }, - 'http.proxyStrictSSL': { - type: 'boolean', - default: true, - description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs."), - restricted: true - }, - 'http.proxyKerberosServicePrincipal': { - type: 'string', - markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."), - restricted: true - }, - 'http.noProxy': { - type: 'array', - items: { type: 'string' }, - markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests."), - restricted: true - }, - 'http.proxyAuthorization': { - type: ['null', 'string'], - default: null, - markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request."), - restricted: true - }, - 'http.proxySupport': { - type: 'string', - enum: ['off', 'on', 'fallback', 'override'], - enumDescriptions: [ - localize('proxySupportOff', "Disable proxy support for extensions."), - localize('proxySupportOn', "Enable proxy support for extensions."), - localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."), - localize('proxySupportOverride', "Enable proxy support for extensions, override request options."), - ], - default: 'override', - description: localize('proxySupport', "Use the proxy support for extensions."), - restricted: true - }, - 'http.systemCertificates': { - type: 'boolean', - default: true, - description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS, a reload of the window is required after turning this off.)"), - restricted: true - }, - 'http.experimental.systemCertificatesV2': { - type: 'boolean', - tags: ['experimental'], - default: false, - description: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation."), - restricted: true - }, - 'http.electronFetch': { - type: 'boolean', - default: false, - description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."), - restricted: true - }, - 'http.fetchAdditionalSupport': { - type: 'boolean', - default: true, - markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({0}) and system certificates ({1}) are added when the corresponding settings are enabled.", '`#http.proxySupport#`', '`#http.systemCertificates#`'), - restricted: true + proxyConfiguration = [ + { + id: 'http', + order: 15, + title: localize('httpConfigurationTitle', "HTTP"), + type: 'object', + scope: ConfigurationScope.MACHINE, + properties: { + 'http.useLocalProxyConfiguration': { + type: 'boolean', + default: useHostProxyDefault, + markdownDescription: localize('useLocalProxy', "Controls whether in the remote extension host the local proxy configuration should be used. This setting only applies as a remote setting during [remote development](https://aka.ms/vscode-remote)."), + restricted: true + }, + } + }, + { + id: 'http', + order: 15, + title: localize('httpConfigurationTitle', "HTTP"), + type: 'object', + scope: ConfigurationScope.APPLICATION, + properties: { + 'http.electronFetch': { + type: 'boolean', + default: false, + description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."), + restricted: true + }, + } + }, + { + id: 'http', + order: 15, + title: localize('httpConfigurationTitle', "HTTP"), + type: 'object', + scope: useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE, + properties: { + 'http.proxy': { + type: 'string', + pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', + markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxyStrictSSL': { + type: 'boolean', + default: true, + markdownDescription: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxyKerberosServicePrincipal': { + type: 'string', + markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.noProxy': { + type: 'array', + items: { type: 'string' }, + markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxyAuthorization': { + type: ['null', 'string'], + default: null, + markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxySupport': { + type: 'string', + enum: ['off', 'on', 'fallback', 'override'], + enumDescriptions: [ + localize('proxySupportOff', "Disable proxy support for extensions."), + localize('proxySupportOn', "Enable proxy support for extensions."), + localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."), + localize('proxySupportOverride', "Enable proxy support for extensions, override request options."), + ], + default: 'override', + markdownDescription: localize('proxySupport', "Use the proxy support for extensions. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.systemCertificates': { + type: 'boolean', + default: true, + markdownDescription: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. On Windows and macOS, a reload of the window is required after turning this off. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.experimental.systemCertificatesV2': { + type: 'boolean', + tags: ['experimental'], + default: false, + markdownDescription: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.fetchAdditionalSupport': { + type: 'boolean', + default: true, + markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({1}) and system certificates ({2}) are added when the corresponding settings are enabled. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`', '`#http.proxySupport#`', '`#http.systemCertificates#`'), + restricted: true + } } } - }; - configurationRegistry.updateConfigurations({ add: [proxyConfiguration], remove: oldProxyConfiguration ? [oldProxyConfiguration] : [] }); + ]; + configurationRegistry.updateConfigurations({ add: proxyConfiguration, remove: oldProxyConfiguration }); } -registerProxyConfigurations(ConfigurationScope.APPLICATION); +registerProxyConfigurations(); diff --git a/code/src/vs/platform/request/electron-utility/requestService.ts b/code/src/vs/platform/request/electron-utility/requestService.ts index f92a2450c3e..eb3097b2e94 100644 --- a/code/src/vs/platform/request/electron-utility/requestService.ts +++ b/code/src/vs/platform/request/electron-utility/requestService.ts @@ -7,6 +7,9 @@ import { net } from 'electron'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { IRequestContext, IRequestOptions } from '../../../base/parts/request/common/request.js'; import { IRawRequestFunction, RequestService as NodeRequestService } from '../node/requestService.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { INativeEnvironmentService } from '../../environment/common/environment.js'; +import { ILogService } from '../../log/common/log.js'; function getRawRequest(options: IRequestOptions): IRawRequestFunction { return net.request as any as IRawRequestFunction; @@ -14,6 +17,14 @@ function getRawRequest(options: IRequestOptions): IRawRequestFunction { export class RequestService extends NodeRequestService { + constructor( + @IConfigurationService configurationService: IConfigurationService, + @INativeEnvironmentService environmentService: INativeEnvironmentService, + @ILogService logService: ILogService, + ) { + super('local', configurationService, environmentService, logService); + } + override request(options: IRequestOptions, token: CancellationToken): Promise { return super.request({ ...(options || {}), getRawRequest, isChromiumNetwork: true }, token); } diff --git a/code/src/vs/platform/request/node/requestService.ts b/code/src/vs/platform/request/node/requestService.ts index 38a582b044e..333705f23e2 100644 --- a/code/src/vs/platform/request/node/requestService.ts +++ b/code/src/vs/platform/request/node/requestService.ts @@ -21,12 +21,6 @@ import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from ' import { Agent, getProxyAgent } from './proxy.js'; import { createGunzip } from 'zlib'; -interface IHTTPConfiguration { - proxy?: string; - proxyStrictSSL?: boolean; - proxyAuthorization?: string; -} - export interface IRawRequestFunction { (options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest; } @@ -52,6 +46,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe private shellEnvErrorLogged?: boolean; constructor( + private readonly machine: 'local' | 'remote', @IConfigurationService private readonly configurationService: IConfigurationService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @ILogService logService: ILogService, @@ -66,11 +61,9 @@ export class RequestService extends AbstractRequestService implements IRequestSe } private configure() { - const config = this.configurationService.getValue('http'); - - this.proxyUrl = config?.proxy; - this.strictSSL = !!config?.proxyStrictSSL; - this.authorization = config?.proxyAuthorization; + this.proxyUrl = this.getConfigValue('http.proxy'); + this.strictSSL = !!this.getConfigValue('http.proxyStrictSSL'); + this.authorization = this.getConfigValue('http.proxyAuthorization'); } async request(options: NodeRequestOptions, token: CancellationToken): Promise { @@ -115,7 +108,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe async lookupKerberosAuthorization(urlStr: string): Promise { try { - const spnConfig = this.configurationService.getValue('http.proxyKerberosServicePrincipal'); + const spnConfig = this.getConfigValue('http.proxyKerberosServicePrincipal'); const response = await lookupKerberosAuthorization(urlStr, spnConfig, this.logService, 'RequestService#lookupKerberosAuthorization'); return 'Negotiate ' + response; } catch (err) { @@ -128,6 +121,14 @@ export class RequestService extends AbstractRequestService implements IRequestSe const proxyAgent = await import('@vscode/proxy-agent'); return proxyAgent.loadSystemCertificates({ log: this.logService }); } + + private getConfigValue(key: string): T | undefined { + if (this.machine === 'remote') { + return this.configurationService.getValue(key); + } + const values = this.configurationService.inspect(key); + return values.userLocalValue || values.defaultValue; + } } export async function lookupKerberosAuthorization(urlStr: string, spnConfig: string | undefined, logService: ILogService, logPrefix: string) { diff --git a/code/src/vs/platform/telemetry/common/1dsAppender.ts b/code/src/vs/platform/telemetry/common/1dsAppender.ts index 7efb786c763..59a0dadcb7d 100644 --- a/code/src/vs/platform/telemetry/common/1dsAppender.ts +++ b/code/src/vs/platform/telemetry/common/1dsAppender.ts @@ -57,7 +57,7 @@ async function getClient(instrumentationKey: string, addInternalFlag?: boolean, appInsightsCore.initialize(coreConfig, []); - appInsightsCore.addTelemetryInitializer((envelope) => { + appInsightsCore.addTelemetryInitializer((envelope: any) => { // Opt the user out of 1DS data sharing envelope['ext'] = envelope['ext'] ?? {}; envelope['ext']['web'] = envelope['ext']['web'] ?? {}; diff --git a/code/src/vs/platform/telemetry/common/telemetryLogAppender.ts b/code/src/vs/platform/telemetry/common/telemetryLogAppender.ts index f4a19472604..26ba89e7a4c 100644 --- a/code/src/vs/platform/telemetry/common/telemetryLogAppender.ts +++ b/code/src/vs/platform/telemetry/common/telemetryLogAppender.ts @@ -8,7 +8,7 @@ import { localize } from '../../../nls.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { ILogService, ILogger, ILoggerService, LogLevel } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; -import { ITelemetryAppender, isLoggingOnly, supportsTelemetry, telemetryLogId, validateTelemetryData } from './telemetryUtils.js'; +import { ITelemetryAppender, TelemetryLogGroup, isLoggingOnly, supportsTelemetry, telemetryLogId, validateTelemetryData } from './telemetryUtils.js'; export class TelemetryLogAppender extends Disposable implements ITelemetryAppender { @@ -31,10 +31,13 @@ export class TelemetryLogAppender extends Disposable implements ITelemetryAppend const justLoggingAndNotSending = isLoggingOnly(productService, environmentService); const logSuffix = justLoggingAndNotSending ? ' (Not Sent)' : ''; const isVisible = () => supportsTelemetry(productService, environmentService) && logService.getLevel() === LogLevel.Trace; - this.logger = this._register(loggerService.createLogger(telemetryLogId, { name: localize('telemetryLog', "Telemetry{0}", logSuffix), hidden: !isVisible() })); + this.logger = this._register(loggerService.createLogger(telemetryLogId, + { + name: localize('telemetryLog', "Telemetry{0}", logSuffix), + hidden: !isVisible(), + group: TelemetryLogGroup + })); this._register(logService.onDidChangeLogLevel(() => loggerService.setVisibility(telemetryLogId, isVisible()))); - this.logger.info('Below are logs for every telemetry event sent from VS Code once the log level is set to trace.'); - this.logger.info('==========================================================='); } } diff --git a/code/src/vs/platform/telemetry/common/telemetryUtils.ts b/code/src/vs/platform/telemetry/common/telemetryUtils.ts index 4ec7bb1ec82..d491bcde655 100644 --- a/code/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/code/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -6,8 +6,10 @@ import { cloneAndChange, safeStringify } from '../../../base/common/objects.js'; import { isObject } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; +import { LoggerGroup } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; import { getRemoteName } from '../../remote/common/remoteHosts.js'; import { verifyMicrosoftInternalDomain } from './commonProperties.js'; @@ -56,6 +58,7 @@ export class NullEndpointTelemetryService implements ICustomEndpointTelemetrySer export const telemetryLogId = 'telemetry'; export const extensionTelemetryLogChannelId = 'extensionTelemetryLog'; +export const TelemetryLogGroup: LoggerGroup = { id: 'telemetry', name: localize('telemetryLogName', "Telemetry") }; export interface ITelemetryAppender { log(eventName: string, data: any): void; diff --git a/code/src/vs/platform/telemetry/node/1dsAppender.ts b/code/src/vs/platform/telemetry/node/1dsAppender.ts index 08be57db219..fe271ec428d 100644 --- a/code/src/vs/platform/telemetry/node/1dsAppender.ts +++ b/code/src/vs/platform/telemetry/node/1dsAppender.ts @@ -105,7 +105,7 @@ export class OneDataSystemAppender extends AbstractOneDataSystemAppender { ) { // Override the way events get sent since node doesn't have XHTMLRequest const customHttpXHROverride: IXHROverride = { - sendPOST: (payload: IPayloadData, oncomplete) => { + sendPOST: (payload: IPayloadData, oncomplete: OnCompleteFunc) => { // Fire off the async request without awaiting it sendPostAsync(requestService, payload, oncomplete); } diff --git a/code/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts b/code/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts index eb72191f928..34b82fb327c 100644 --- a/code/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts +++ b/code/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts @@ -93,7 +93,7 @@ suite('TelemetryLogAdapter', () => { const testInstantiationService = new TestInstantiationService(); const testObject = new TelemetryLogAppender(new NullLogService(), testLoggerService, testInstantiationService.stub(IEnvironmentService, {}), testInstantiationService.stub(IProductService, {})); testObject.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); - assert.strictEqual(testLoggerService.createLogger().logs.length, 2); + assert.strictEqual(testLoggerService.createLogger().logs.length, 0); testObject.dispose(); testInstantiationService.dispose(); }); @@ -103,7 +103,7 @@ suite('TelemetryLogAdapter', () => { const testInstantiationService = new TestInstantiationService(); const testObject = new TelemetryLogAppender(new NullLogService(), testLoggerService, testInstantiationService.stub(IEnvironmentService, {}), testInstantiationService.stub(IProductService, {})); testObject.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); - assert.strictEqual(testLoggerService.createLogger().logs[2], 'telemetry/testEvent' + JSON.stringify([{ + assert.strictEqual(testLoggerService.createLogger().logs[0], 'telemetry/testEvent' + JSON.stringify([{ properties: { hello: 'world', }, diff --git a/code/src/vs/platform/terminal/common/capabilities/capabilities.ts b/code/src/vs/platform/terminal/common/capabilities/capabilities.ts index 6de15be87fa..ba7597b9d92 100644 --- a/code/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/code/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -70,7 +70,13 @@ export const enum TerminalCapability { * the request (task, debug, etc) provides an ID, optional marker, hoverMessage, and hidden property. When * hidden is not provided, a generic decoration is added to the buffer and overview ruler. */ - BufferMarkDetection + BufferMarkDetection, + + /** + * The terminal can detect the latest environment of user's current shell. + */ + ShellEnvDetection, + } /** @@ -133,6 +139,7 @@ export interface ITerminalCapabilityImplMap { [TerminalCapability.NaiveCwdDetection]: INaiveCwdDetectionCapability; [TerminalCapability.PartialCommandDetection]: IPartialCommandDetectionCapability; [TerminalCapability.BufferMarkDetection]: IBufferMarkCapability; + [TerminalCapability.ShellEnvDetection]: IShellEnvDetectionCapability; } export interface ICwdDetectionCapability { @@ -143,6 +150,16 @@ export interface ICwdDetectionCapability { updateCwd(cwd: string): void; } +export interface IShellEnvDetectionCapability { + readonly type: TerminalCapability.ShellEnvDetection; + readonly onDidChangeEnv: Event>; + get env(): Map; + setEnvironment(envs: { [key: string]: string | undefined } | undefined, isTrusted: boolean): void; + startEnvironmentSingleVar(isTrusted: boolean): void; + setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void; + endEnvironmentSingleVar(isTrusted: boolean): void; +} + export const enum CommandInvalidationReason { Windows = 'windows', NoProblemsReported = 'noProblemsReported' diff --git a/code/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts b/code/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts new file mode 100644 index 00000000000..be9577acfe9 --- /dev/null +++ b/code/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.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 { Disposable } from '../../../../base/common/lifecycle.js'; +import { IShellEnvDetectionCapability, TerminalCapability } from './capabilities.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { equals } from '../../../../base/common/objects.js'; + +export class ShellEnvDetectionCapability extends Disposable implements IShellEnvDetectionCapability { + readonly type = TerminalCapability.ShellEnvDetection; + + private _pendingEnv: Map | undefined; + private _env: Map = new Map(); + get env(): Map { return this._env; } + + private readonly _onDidChangeEnv = this._register(new Emitter>()); + readonly onDidChangeEnv = this._onDidChangeEnv.event; + + setEnvironment(env: { [key: string]: string | undefined }, isTrusted: boolean): void { + if (!isTrusted) { + return; + } + + if (equals(this._env, env)) { + return; + } + + this._env.clear(); + for (const [key, value] of Object.entries(env)) { + if (value !== undefined) { + this._env.set(key, value); + } + } + + // Convert to event and fire event + this._onDidChangeEnv.fire(this._env); + } + + startEnvironmentSingleVar(isTrusted: boolean): void { + if (!isTrusted) { + return; + } + this._pendingEnv = new Map(); + } + setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void { + if (!isTrusted) { + return; + } + if (key !== undefined && value !== undefined) { + this._pendingEnv?.set(key, value); + } + } + endEnvironmentSingleVar(isTrusted: boolean): void { + if (!isTrusted) { + return; + } + if (!this._pendingEnv) { + return; + } + this._env = this._pendingEnv; + this._pendingEnv = undefined; + this._onDidChangeEnv.fire(this._env); + } +} diff --git a/code/src/vs/platform/terminal/common/terminal.ts b/code/src/vs/platform/terminal/common/terminal.ts index 2accdc4b612..13b92e6b485 100644 --- a/code/src/vs/platform/terminal/common/terminal.ts +++ b/code/src/vs/platform/terminal/common/terminal.ts @@ -150,7 +150,8 @@ export const enum GeneralShellType { PowerShell = 'pwsh', Python = 'python', Julia = 'julia', - NuShell = 'nu' + NuShell = 'nu', + Node = 'node', } export type TerminalShellType = PosixShellType | WindowsShellType | GeneralShellType; diff --git a/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 8221ad5c282..92b1851c275 100644 --- a/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -8,7 +8,7 @@ import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base import { TerminalCapabilityStore } from '../capabilities/terminalCapabilityStore.js'; import { CommandDetectionCapability } from '../capabilities/commandDetectionCapability.js'; import { CwdDetectionCapability } from '../capabilities/cwdDetectionCapability.js'; -import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, TerminalCapability } from '../capabilities/capabilities.js'; +import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, IShellEnvDetectionCapability, TerminalCapability } from '../capabilities/capabilities.js'; import { PartialCommandDetectionCapability } from '../capabilities/partialCommandDetectionCapability.js'; import { ILogService } from '../../../log/common/log.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; @@ -18,6 +18,7 @@ import type { ITerminalAddon, Terminal } from '@xterm/headless'; import { URI } from '../../../../base/common/uri.js'; import { sanitizeCwd } from '../terminalEnvironment.js'; import { removeAnsiEscapeCodesFromPrompt } from '../../../../base/common/strings.js'; +import { ShellEnvDetectionCapability } from '../capabilities/shellEnvDetectionCapability.js'; /** @@ -224,6 +225,48 @@ const enum VSCodeOscPt { * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. */ SetMark = 'SetMark', + + /** + * Sends the shell's complete environment in JSON format. + * + * Format: `OSC 633 ; EnvJson ; ; ` + * + * - `Environment` - A stringified JSON object containing the shell's complete environment. The + * variables and values use the same encoding rules as the {@link CommandLine} sequence. + * - `Nonce` - An _mandatory_ nonce to ensure the sequence is not malicious. + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvJson = 'EnvJson', + + /** + * The start of the collecting user's environment variables individually. + * Clears any environment residuals in previous sessions. + * + * Format: `OSC 633 ; EnvSingleStart ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleStart = 'EnvSingleStart', + + /** + * Sets an entry of single environment variable to transactional pending map of environment variables. + * + * Format: `OSC 633 ; EnvSingleEntry ; ; ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleEntry = 'EnvSingleEntry', + + /** + * The end of the collecting user's environment variables individually. + * Clears any pending environment variables and fires an event that contains user's environment. + * + * Format: `OSC 633 ; EnvSingleEnd ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleEnd = 'EnvSingleEnd' } /** @@ -418,6 +461,37 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetCommandDetection(this._terminal).handleContinuationEnd(); return true; } + case VSCodeOscPt.EnvJson: { + const arg0 = args[0]; + const arg1 = args[1]; + if (arg0 !== undefined) { + try { + const env = JSON.parse(deserializeMessage(arg0)); + this._createOrGetShellEnvDetection().setEnvironment(env, arg1 === this._nonce); + } catch (e) { + this._logService.warn('Failed to parse environment from shell integration sequence', arg0); + } + } + return true; + } + case VSCodeOscPt.EnvSingleStart: { + this._createOrGetShellEnvDetection().startEnvironmentSingleVar(args[0] === this._nonce); + return true; + } + case VSCodeOscPt.EnvSingleEntry: { + const arg0 = args[0]; + const arg1 = args[1]; + const arg2 = args[2]; + if (arg0 !== undefined && arg1 !== undefined) { + const env = deserializeMessage(arg1); + this._createOrGetShellEnvDetection().setEnvironmentSingleVar(arg0, env, arg2 === this._nonce); + } + return true; + } + case VSCodeOscPt.EnvSingleEnd: { + this._createOrGetShellEnvDetection().endEnvironmentSingleVar(args[0] === this._nonce); + return true; + } case VSCodeOscPt.RightPromptStart: { this._createOrGetCommandDetection(this._terminal).handleRightPromptStart(); return true; @@ -614,6 +688,15 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati } return bufferMarkDetection; } + + protected _createOrGetShellEnvDetection(): IShellEnvDetectionCapability { + let shellEnvDetection = this.capabilities.get(TerminalCapability.ShellEnvDetection); + if (!shellEnvDetection) { + shellEnvDetection = this._register(new ShellEnvDetectionCapability()); + this.capabilities.add(TerminalCapability.ShellEnvDetection, shellEnvDetection); + } + return shellEnvDetection; + } } export function deserializeMessage(message: string): string { diff --git a/code/src/vs/platform/terminal/node/terminalProcess.ts b/code/src/vs/platform/terminal/node/terminalProcess.ts index a57acc78b17..1fff426753a 100644 --- a/code/src/vs/platform/terminal/node/terminalProcess.ts +++ b/code/src/vs/platform/terminal/node/terminalProcess.ts @@ -81,6 +81,7 @@ const generalShellTypeMap = new Map([ ['python', GeneralShellType.Python], ['julia', GeneralShellType.Julia], ['nu', GeneralShellType.NuShell], + ['node', GeneralShellType.Node], ]); export class TerminalProcess extends Disposable implements ITerminalChildProcess { diff --git a/code/src/vs/platform/terminal/node/windowsShellHelper.ts b/code/src/vs/platform/terminal/node/windowsShellHelper.ts index 15edb8c02ca..ded8e456c75 100644 --- a/code/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/code/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -33,6 +33,7 @@ const SHELL_EXECUTABLES = [ 'sles-12.exe', 'julia.exe', 'nu.exe', + 'node.exe', ]; const SHELL_EXECUTABLE_REGEXES = [ @@ -157,6 +158,8 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return WindowsShellType.GitBash; case 'julia.exe': return GeneralShellType.Julia; + case 'node.exe': + return GeneralShellType.Node; case 'nu.exe': return GeneralShellType.NuShell; case 'wsl.exe': diff --git a/code/src/vs/platform/theme/common/themeService.ts b/code/src/vs/platform/theme/common/themeService.ts index bca5b3b3b3e..7911e0221da 100644 --- a/code/src/vs/platform/theme/common/themeService.ts +++ b/code/src/vs/platform/theme/common/themeService.ts @@ -230,8 +230,15 @@ export interface IPartsSplash { titleBarHeight: number; activityBarWidth: number; sideBarWidth: number; + auxiliarySideBarWidth: number; statusBarHeight: number; windowBorder: boolean; windowBorderRadius: string | undefined; } | undefined; } + +export interface IPartsSplashWorkspaceOverride { + layoutInfo: { + auxiliarySideBarWidth: [number, string[] /* workspace identifier the override applies to */]; + }; +} diff --git a/code/src/vs/platform/theme/electron-main/themeMainService.ts b/code/src/vs/platform/theme/electron-main/themeMainService.ts index 3cc4fb246e1..90d2be8dc27 100644 --- a/code/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/code/src/vs/platform/theme/electron-main/themeMainService.ts @@ -10,9 +10,11 @@ import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.j import { IConfigurationService } from '../../configuration/common/configuration.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStateService } from '../../state/node/state.js'; -import { IPartsSplash } from '../common/themeService.js'; +import { IPartsSplash, IPartsSplashWorkspaceOverride } from '../common/themeService.js'; import { IColorScheme } from '../../window/common/window.js'; import { ThemeTypeSelector } from '../common/theme.js'; +import { IBaseWorkspaceIdentifier } from '../../workspace/common/workspace.js'; +import { coalesce } from '../../../base/common/arrays.js'; // These default colors match our default themes // editor background color ("Dark Modern", etc...) @@ -23,7 +25,9 @@ const DEFAULT_BG_HC_LIGHT = '#FFFFFF'; const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; -const THEME_WINDOW_SPLASH = 'windowSplash'; + +const THEME_WINDOW_SPLASH_KEY = 'windowSplash'; +const THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY = 'windowSplashWorkspaceOverride'; namespace ThemeSettings { export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme'; @@ -41,8 +45,8 @@ export interface IThemeMainService { getBackgroundColor(): string; - saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void; - getWindowSplash(): IPartsSplash | undefined; + saveWindowSplash(windowId: number | undefined, workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): void; + getWindowSplash(workspace: IBaseWorkspaceIdentifier | undefined): IPartsSplash | undefined; getColorScheme(): IColorScheme; } @@ -163,14 +167,18 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } } - saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void { + saveWindowSplash(windowId: number | undefined, workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): void { + + // Update override as needed + const splashOverride = this.updateWindowSplashOverride(workspace, splash); // Update in storage - this.stateService.setItems([ + this.stateService.setItems(coalesce([ { key: THEME_STORAGE_KEY, data: splash.baseTheme }, { key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background }, - { key: THEME_WINDOW_SPLASH, data: splash } - ]); + { key: THEME_WINDOW_SPLASH_KEY, data: splash }, + splashOverride ? { key: THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY, data: splashOverride } : undefined + ])); // Update in opened windows if (typeof windowId === 'number') { @@ -181,6 +189,35 @@ export class ThemeMainService extends Disposable implements IThemeMainService { this.updateSystemColorTheme(); } + private updateWindowSplashOverride(workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): IPartsSplashWorkspaceOverride | undefined { + let splashOverride: IPartsSplashWorkspaceOverride | undefined = undefined; + let changed = false; + if (workspace) { + splashOverride = { ...this.getWindowSplashOverride() }; // make a copy for modifications + + const [auxiliarySideBarWidth, workspaceIds] = splashOverride.layoutInfo.auxiliarySideBarWidth; + if (splash.layoutInfo?.auxiliarySideBarWidth) { + if (auxiliarySideBarWidth !== splash.layoutInfo.auxiliarySideBarWidth) { + splashOverride.layoutInfo.auxiliarySideBarWidth[0] = splash.layoutInfo.auxiliarySideBarWidth; + changed = true; + } + + if (!workspaceIds.includes(workspace.id)) { + workspaceIds.push(workspace.id); + changed = true; + } + } else { + const index = workspaceIds.indexOf(workspace.id); + if (index > -1) { + workspaceIds.splice(index, 1); + changed = true; + } + } + } + + return changed ? splashOverride : undefined; + } + private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { for (const window of electron.BrowserWindow.getAllWindows()) { if (window.id === windowId) { @@ -190,7 +227,34 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } } - getWindowSplash(): IPartsSplash | undefined { - return this.stateService.getItem(THEME_WINDOW_SPLASH); + getWindowSplash(workspace: IBaseWorkspaceIdentifier | undefined): IPartsSplash | undefined { + const partSplash = this.stateService.getItem(THEME_WINDOW_SPLASH_KEY); + if (!partSplash?.layoutInfo) { + return partSplash; // return early: overrides currently only apply to layout info + } + + // Apply workspace specific overrides + let auxiliarySideBarWidthOverride: number | undefined; + if (workspace) { + const [auxiliarySideBarWidth, workspaceIds] = this.getWindowSplashOverride().layoutInfo.auxiliarySideBarWidth; + if (workspaceIds.includes(workspace.id)) { + auxiliarySideBarWidthOverride = auxiliarySideBarWidth; + } + } + + return { + ...partSplash, + layoutInfo: { + ...partSplash.layoutInfo, + // Only apply an auxiliary bar width when we have a workspace specific + // override. Auxiliary bar is not visible by default unless explicitly + // opened in a workspace. + auxiliarySideBarWidth: typeof auxiliarySideBarWidthOverride === 'number' ? auxiliarySideBarWidthOverride : 0 + } + }; + } + + private getWindowSplashOverride(): IPartsSplashWorkspaceOverride { + return this.stateService.getItem(THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY, { layoutInfo: { auxiliarySideBarWidth: [0, []] } }); } } diff --git a/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 11c0315468a..54408a57aba 100644 --- a/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -36,12 +36,6 @@ import { } from './userDataSync.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; -type IncompatibleSyncSourceClassification = { - owner: 'sandy081'; - comment: 'Information about the sync resource that is incompatible'; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'settings sync resource. eg., settings, keybindings...' }; -}; - export function isRemoteUserData(thing: any): thing is IRemoteUserData { if (thing && (thing.ref !== undefined && typeof thing.ref === 'string' && thing.ref !== '') @@ -325,8 +319,6 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, strategy: SyncStrategy, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { - // current version is not compatible with cloud version - this.telemetryService.publicLog2<{ source: string }, IncompatibleSyncSourceClassification>('sync/incompatible', { source: this.resource }); throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.IncompatibleLocalContent, this.resource); } diff --git a/code/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/code/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index f868cccd7e3..5ab5b3dd21d 100644 --- a/code/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/code/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -21,20 +21,6 @@ import { IUserDataSyncTask, IUserDataAutoSyncService, IUserDataManifest, IUserDa import { IUserDataSyncAccountService } from './userDataSyncAccount.js'; import { IUserDataSyncMachinesService } from './userDataSyncMachines.js'; -type AutoSyncClassification = { - owner: 'sandy081'; - comment: 'Information about the sources triggering auto sync'; - sources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Source that triggered auto sync' }; - providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Auth provider id used for sync' }; -}; - -type AutoSyncErrorClassification = { - owner: 'sandy081'; - comment: 'Information about the error that causes auto sync to fail'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; - service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings sync service for which this error has occurred' }; -}; - const disableMachineEventuallyKey = 'sync.disableMachineEventually'; const sessionIdKey = 'sync.sessionId'; const storeUrlKey = 'sync.storeUrl'; @@ -199,7 +185,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto // Reset if (everywhere) { - this.telemetryService.publicLog2<{}, { owner: 'sandy081'; comment: 'Reporting when settings sync is turned off in all devices' }>('sync/turnOffEveryWhere'); await this.userDataSyncService.reset(); } else { await this.userDataSyncService.resetLocal(); @@ -235,11 +220,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto // Error while syncing const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); - // Log to telemetry - if (userDataSyncError instanceof UserDataAutoSyncError) { - this.telemetryService.publicLog2<{ code: string; service: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); - } - // Session got expired if (userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) { await this.turnOff(false, true /* force soft turnoff on error */); @@ -361,8 +341,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this.sources.push(...sources); return this.syncTriggerDelayer.trigger(async () => { this.logService.trace('activity sources', ...this.sources); - const providerId = this.userDataSyncAccountService.account?.authenticationProviderId || ''; - this.telemetryService.publicLog2<{ sources: string[]; providerId: string }, AutoSyncClassification>('sync/triggered', { sources: this.sources, providerId }); this.sources = []; if (this.autoSync.value) { await this.autoSync.value.sync('Activity', disableCache); diff --git a/code/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts b/code/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts index 858dd8d3b47..163f805c730 100644 --- a/code/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts +++ b/code/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts @@ -8,15 +8,8 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { isWeb } from '../../../base/common/platform.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IApplicationStorageValueChangeEvent, IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { ALL_SYNC_RESOURCES, getEnablementKey, IUserDataSyncEnablementService, IUserDataSyncStoreManagementService, SyncResource } from './userDataSync.js'; -type SyncEnablementClassification = { - owner: 'sandy081'; - comment: 'Reporting when Settings Sync is turned on or off'; - enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if settings sync is enabled or not' }; -}; - const enablementKey = 'sync.enable'; export class UserDataSyncEnablementService extends Disposable implements IUserDataSyncEnablementService { @@ -31,7 +24,6 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa constructor( @IStorageService private readonly storageService: IStorageService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IEnvironmentService protected readonly environmentService: IEnvironmentService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, ) { @@ -57,7 +49,6 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa if (enabled && !this.canToggleEnablement()) { return; } - this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(enablementKey, { enabled }); this.storageService.store(enablementKey, enabled, StorageScope.APPLICATION, StorageTarget.MACHINE); } diff --git a/code/src/vs/platform/window/electron-main/window.ts b/code/src/vs/platform/window/electron-main/window.ts index 91a2eb1339a..7f2413d6370 100644 --- a/code/src/vs/platform/window/electron-main/window.ts +++ b/code/src/vs/platform/window/electron-main/window.ts @@ -139,8 +139,8 @@ export interface IWindowState { export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState { return { - width: 1024, - height: 768, + width: 1200, + height: 800, mode }; }; @@ -154,8 +154,8 @@ export const defaultAuxWindowState = function (): IWindowState { // we need to set not only width and height but also x and y to // a good location on the primary display. - const width = 800; - const height = 600; + const width = 1024; + const height = 768; const workArea = electron.screen.getPrimaryDisplay().workArea; const x = Math.max(workArea.x + (workArea.width / 2) - (width / 2), 0); const y = Math.max(workArea.y + (workArea.height / 2) - (height / 2), 0); diff --git a/code/src/vs/platform/windows/electron-main/windowImpl.ts b/code/src/vs/platform/windows/electron-main/windowImpl.ts index ab629987eae..3c8efe30dd6 100644 --- a/code/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/code/src/vs/platform/windows/electron-main/windowImpl.ts @@ -923,7 +923,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Proxy if (!e || e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.noProxy')) { - let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() + const inspect = this.configurationService.inspect('http.proxy'); + let newHttpProxy = (inspect.userLocalValue || '').trim() || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; @@ -1065,7 +1066,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } configuration.fullscreen = this.isFullScreen; configuration.maximized = this._win.isMaximized(); - configuration.partsSplash = this.themeMainService.getWindowSplash(); + configuration.partsSplash = this.themeMainService.getWindowSplash(configuration.workspace); configuration.zoomLevel = this.getZoomLevel(); configuration.isCustomZoomLevel = typeof this.customZoomLevel === 'number'; if (configuration.isCustomZoomLevel && configuration.partsSplash) { diff --git a/code/src/vs/platform/windows/electron-main/windows.ts b/code/src/vs/platform/windows/electron-main/windows.ts index 6d14080654d..b4c024a39f2 100644 --- a/code/src/vs/platform/windows/electron-main/windows.ts +++ b/code/src/vs/platform/windows/electron-main/windows.ts @@ -194,7 +194,7 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt // to use on initialization, but prefer to keep things // simple as it is temporary and not noticeable - const titleBarColor = themeMainService.getWindowSplash()?.colorInfo.titleBarBackground ?? themeMainService.getBackgroundColor(); + const titleBarColor = themeMainService.getWindowSplash(undefined)?.colorInfo.titleBarBackground ?? themeMainService.getBackgroundColor(); const symbolColor = Color.fromHex(titleBarColor).isDarker() ? '#FFFFFF' : '#000000'; options.titleBarOverlay = { diff --git a/code/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/code/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index a1ed6bd2794..68bcfe4e00f 100644 --- a/code/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts +++ b/code/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -183,8 +183,8 @@ suite('Windows State Storing', () => { "mode": 1, "x": 768, "y": 336, - "width": 1024, - "height": 768 + "width": 1200, + "height": 800 } } }`; @@ -194,7 +194,7 @@ suite('Windows State Storing', () => { openedWindows: [], lastActiveWindow: { backupPath: '/home/user/.config/code-oss-dev/Backups/1549539668998', - uiState: { mode: WindowMode.Normal, x: 768, y: 336, width: 1024, height: 768 } + uiState: { mode: WindowMode.Normal, x: 768, y: 336, width: 1200, height: 800 } } }; assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); diff --git a/code/src/vs/platform/workspaces/common/workspaces.ts b/code/src/vs/platform/workspaces/common/workspaces.ts index 6097076c43f..7dddb7f3764 100644 --- a/code/src/vs/platform/workspaces/common/workspaces.ts +++ b/code/src/vs/platform/workspaces/common/workspaces.ts @@ -364,16 +364,39 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine export function toStoreData(recents: IRecentlyOpened): RecentlyOpenedStorageData { const serialized: ISerializedRecentlyOpened = { entries: [] }; + const storeLabel = (label: string | undefined, uri: URI) => { + // Only store the label if it is provided + // and only if it differs from the path + // This gives us a chance to render the + // path better, e.g. use `~` for home. + return label && label !== uri.fsPath && label !== uri.path; + }; + for (const recent of recents.workspaces) { if (isRecentFolder(recent)) { - serialized.entries.push({ folderUri: recent.folderUri.toString(), label: recent.label, remoteAuthority: recent.remoteAuthority }); + serialized.entries.push({ + folderUri: recent.folderUri.toString(), + label: storeLabel(recent.label, recent.folderUri) ? recent.label : undefined, + remoteAuthority: recent.remoteAuthority + }); } else { - serialized.entries.push({ workspace: { id: recent.workspace.id, configPath: recent.workspace.configPath.toString() }, label: recent.label, remoteAuthority: recent.remoteAuthority }); + serialized.entries.push({ + workspace: { + id: recent.workspace.id, + configPath: recent.workspace.configPath.toString() + }, + label: storeLabel(recent.label, recent.workspace.configPath) ? recent.label : undefined, + remoteAuthority: recent.remoteAuthority + }); } } for (const recent of recents.files) { - serialized.entries.push({ fileUri: recent.fileUri.toString(), label: recent.label, remoteAuthority: recent.remoteAuthority }); + serialized.entries.push({ + fileUri: recent.fileUri.toString(), + label: storeLabel(recent.label, recent.fileUri) ? recent.label : undefined, + remoteAuthority: recent.remoteAuthority + }); } return serialized; diff --git a/code/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/code/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index 1c86fda9442..787b8784d9a 100644 --- a/code/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/code/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -145,5 +145,23 @@ suite('History Storage', () => { assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); }); + test('toStoreData drops label if it matches path', () => { + const actual = toStoreData({ + workspaces: [], + files: [{ + fileUri: URI.parse('file:///foo/bar/test.txt'), + label: '/foo/bar/test.txt', + remoteAuthority: undefined + }] + }); + assert.deepStrictEqual(actual, { + entries: [{ + fileUri: 'file:///foo/bar/test.txt', + label: undefined, + remoteAuthority: undefined + }] + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/code/src/vs/server/node/remoteExtensionHostAgentCli.ts b/code/src/vs/server/node/remoteExtensionHostAgentCli.ts index 87ceb0788c9..df33289a964 100644 --- a/code/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/code/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -129,7 +129,7 @@ class CliMain extends Disposable { userDataProfilesService.init() ]); - services.set(IRequestService, new SyncDescriptor(RequestService)); + services.set(IRequestService, new SyncDescriptor(RequestService, ['remote'])); services.set(IDownloadService, new SyncDescriptor(DownloadService)); services.set(ITelemetryService, NullTelemetryService); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); diff --git a/code/src/vs/server/node/serverServices.ts b/code/src/vs/server/node/serverServices.ts index 930626375a6..fb973bd4985 100644 --- a/code/src/vs/server/node/serverServices.ts +++ b/code/src/vs/server/node/serverServices.ts @@ -149,7 +149,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionHostStatusService, extensionHostStatusService); // Request - const requestService = new RequestService(configurationService, environmentService, logService); + const requestService = new RequestService('remote', configurationService, environmentService, logService); services.set(IRequestService, requestService); let oneDsAppender: ITelemetryAppender = NullAppender; diff --git a/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index cf451338375..eb2d7e2a9fd 100644 --- a/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -161,6 +161,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.delete(request.requestId); } }, + setRequestPaused: (requestId, isPaused) => { + this._proxy.$setRequestPaused(handle, requestId, isPaused); + }, provideFollowups: async (request, result, history, token): Promise => { if (!this._agents.get(handle)?.hasFollowups) { return []; diff --git a/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index abacb824c24..f447d2ca755 100644 --- a/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -25,15 +25,17 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper); } - $registerCodeMapperProvider(handle: number): void { + $registerCodeMapperProvider(handle: number, displayName: string): void { const impl: ICodeMapperProvider = { + displayName, mapCode: async (uiRequest: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) => { const requestId = String(MainThreadChatCodemapper._requestHandlePool++); this._responseMap.set(requestId, response); const extHostRequest: ICodeMapperRequestDto = { requestId, codeBlocks: uiRequest.codeBlocks, - conversation: uiRequest.conversation + chatRequestId: uiRequest.chatRequestId, + location: uiRequest.location }; try { return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 3d2c8205057..753d4e8a51b 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -644,8 +644,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, this._languageFeaturesService.inlineCompletionsProvider.register(selector, provider)); } - $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier): void { + $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void { const provider: languages.InlineEditProvider = { + displayName, provideInlineEdit: async (model: ITextModel, context: languages.IInlineEditContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineEdit(handle, model.uri, context, token); }, @@ -1005,13 +1006,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } return provider.resolveDocumentOnDropFileData(requestId, dataId); } - - // --- mapped edits - - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void { - const provider = new MainThreadMappedEditsProvider(displayName, handle, this._proxy, this._uriIdentService); - this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); - } } class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider { @@ -1248,17 +1242,3 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. } } -export class MainThreadMappedEditsProvider implements languages.MappedEditsProvider { - - constructor( - public readonly displayName: string, - private readonly _handle: number, - private readonly _proxy: ExtHostLanguageFeaturesShape, - private readonly _uriService: IUriIdentityService, - ) { } - - async provideMappedEdits(document: ITextModel, codeBlocks: string[], context: languages.MappedEditsContext, token: CancellationToken): Promise { - const res = await this._proxy.$provideMappedEdits(this._handle, document.uri, codeBlocks, context, token); - return res ? reviveWorkspaceEditDto(res, this._uriService) : null; - } -} diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 659f026e78d..1b1cb20d72b 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -20,6 +20,7 @@ import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticati import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { ExtHostContext, ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from '../common/extHost.protocol.js'; +import { LanguageModelError } from '../common/extHostTypes.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageModels) export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @@ -97,7 +98,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { if (data) { this._pendingProgress.delete(requestId); if (err) { - const error = transformErrorFromSerialization(err); + const error = LanguageModelError.tryDeserialize(err) ?? transformErrorFromSerialization(err); data.stream.reject(error); data.defer.error(error); } else { diff --git a/code/src/vs/workbench/api/browser/mainThreadOutputService.ts b/code/src/vs/workbench/api/browser/mainThreadOutputService.ts index 4fff96c5150..063ed1901e7 100644 --- a/code/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/code/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -59,7 +59,7 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut const id = `extension-output-${extensionId}-#${idCounter}-${label}`; const resource = URI.revive(file); - Registry.as(Extensions.OutputChannels).registerChannel({ id, label, file: resource, log: false, languageId, extensionId }); + Registry.as(Extensions.OutputChannels).registerChannel({ id, label, source: { resource }, log: false, languageId, extensionId }); this._register(toDisposable(() => this.$dispose(id))); return id; } diff --git a/code/src/vs/workbench/api/browser/mainThreadSCM.ts b/code/src/vs/workbench/api/browser/mainThreadSCM.ts index 5b22d1f069f..b643356323c 100644 --- a/code/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/code/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Barrier } from '../../../base/common/async.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; +import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; import { IObservable, observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; @@ -34,10 +34,10 @@ import { ColorIdentifier } from '../../../platform/theme/common/colorUtils.js'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { return undefined; - } else if (URI.isUri(iconDto)) { - return URI.revive(iconDto); } else if (ThemeIcon.isThemeIcon(iconDto)) { return iconDto; + } else if (isUriComponents(iconDto)) { + return URI.revive(iconDto); } else { const icon = iconDto as { light: UriComponents; dark: UriComponents }; return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) }; @@ -45,15 +45,13 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { + const authorIcon = getIconFromIconDto(historyItemDto.authorIcon); + const references = historyItemDto.references?.map(r => ({ ...r, icon: getIconFromIconDto(r.icon) })); - const newLineIndex = historyItemDto.message.indexOf('\n'); - const subject = newLineIndex === -1 ? - historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`; - - return { ...historyItemDto, subject, references }; + return { ...historyItemDto, authorIcon, references }; } function toISCMHistoryItemRef(historyItemRefDto?: SCMHistoryItemRefDto, color?: ColorIdentifier): ISCMHistoryItemRef | undefined { @@ -214,8 +212,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { return changes?.map(change => ({ uri: URI.revive(change.uri), originalUri: change.originalUri && URI.revive(change.originalUri), - modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri), - renameUri: change.renameUri && URI.revive(change.renameUri) + modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri) })); } diff --git a/code/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/code/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 4988e519f39..96059861a79 100644 --- a/code/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/code/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainThreadStatusBarShape, MainContext, ExtHostContext, StatusBarItemDto } from '../common/extHost.protocol.js'; +import { MainThreadStatusBarShape, MainContext, ExtHostContext, StatusBarItemDto, ExtHostStatusBarShape } from '../common/extHost.protocol.js'; import { ThemeColor } from '../../../base/common/themables.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; @@ -12,17 +12,20 @@ import { IAccessibilityInformation } from '../../../platform/accessibility/commo import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IExtensionStatusBarItemService, StatusBarUpdateKind } from './statusBarExtensionPoint.js'; import { IStatusbarEntry, StatusbarAlignment } from '../../services/statusbar/browser/statusbar.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { + private readonly _proxy: ExtHostStatusBarShape; private readonly _store = new DisposableStore(); constructor( extHostContext: IExtHostContext, @IExtensionStatusBarItemService private readonly statusbarService: IExtensionStatusBarItemService ) { - const proxy = extHostContext.getProxy(ExtHostContext.ExtHostStatusBar); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostStatusBar); // once, at startup read existing items and send them over const entries: StatusBarItemDto[] = []; @@ -30,11 +33,11 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { entries.push(asDto(entryId, item)); } - proxy.$acceptStaticEntries(entries); + this._proxy.$acceptStaticEntries(entries); this._store.add(statusbarService.onDidChange(e => { if (e.added) { - proxy.$acceptStaticEntries([asDto(e.added[0], e.added[1])]); + this._proxy.$acceptStaticEntries([asDto(e.added[0], e.added[1])]); } })); @@ -56,8 +59,17 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this._store.dispose(); } - $setEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void { - const kind = this.statusbarService.setOrUpdateEntry(entryId, id, extensionId, name, text, tooltip, command, color, backgroundColor, alignLeft, priority, accessibilityInformation); + $setEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, hasTooltipProvider: boolean, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void { + const tooltipOrTooltipProvider = hasTooltipProvider + ? { + markdown: (cancellation: CancellationToken) => { + return this._proxy.$provideTooltip(entryId, cancellation); + }, + markdownNotSupportedFallback: undefined + } satisfies IManagedHoverTooltipMarkdownString + : tooltip; + + const kind = this.statusbarService.setOrUpdateEntry(entryId, id, extensionId, name, text, tooltipOrTooltipProvider, command, color, backgroundColor, alignLeft, priority, accessibilityInformation); if (kind === StatusBarUpdateKind.DidDefine) { this._store.add(toDisposable(() => this.statusbarService.unsetEntry(entryId))); } diff --git a/code/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/code/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 57fe7dc25ab..90d5a2ec03f 100644 --- a/code/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/code/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -87,11 +87,15 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._store.add(_terminalService.onAnyInstanceTitleChange(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); this._store.add(_terminalService.onAnyInstanceDataInput(instance => this._proxy.$acceptTerminalInteraction(instance.instanceId))); this._store.add(_terminalService.onAnyInstanceSelectionChange(instance => this._proxy.$acceptTerminalSelection(instance.instanceId, instance.selection))); + this._store.add(_terminalService.onAnyInstanceShellTypeChanged(instance => this._onShellTypeChanged(instance.instanceId))); // Set initial ext host state for (const instance of this._terminalService.instances) { this._onTerminalOpened(instance); instance.processReady.then(() => this._onTerminalProcessIdReady(instance)); + if (instance.shellType) { + this._proxy.$acceptTerminalShellType(instance.instanceId, instance.shellType); + } } const activeInstance = this._terminalService.activeInstance; if (activeInstance) { @@ -271,7 +275,12 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._completionProviders.set(id, this._terminalCompletionService.registerTerminalCompletionProvider(extensionIdentifier, id, { id, provideCompletions: async (commandLine, cursorPosition, token) => { - return await this._proxy.$provideTerminalCompletions(id, { commandLine, cursorPosition }, token); + const completions = await this._proxy.$provideTerminalCompletions(id, { commandLine, cursorPosition }, token); + if (Array.isArray(completions)) { + return completions.map(c => ({ ...c, provider: id })); + } else { + return { items: completions?.items.map(c => ({ ...c, provider: id })), resourceRequestConfig: completions?.resourceRequestConfig }; + } } }, ...triggerCharacters)); } @@ -353,6 +362,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalTitleChange(terminalId, name); } + private _onShellTypeChanged(terminalId: number): void { + const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + if (terminalInstance) { + this._proxy.$acceptTerminalShellType(terminalId, terminalInstance.shellType); + } + } + private _onTerminalDisposed(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalClosed(terminalInstance.instanceId, terminalInstance.exitCode, terminalInstance.exitReason ?? TerminalExitReason.Unknown); } diff --git a/code/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/code/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index e2ee50cc3a4..bfe720fc087 100644 --- a/code/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/code/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -60,6 +60,14 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma this._proxy.$cwdChange(e.instance.instanceId, this._convertCwdToUri(e.data)); })); + // onDidChangeTerminalShellIntegration via env + const envChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.ShellEnvDetection, e => e.onDidChangeEnv)); + this._store.add(envChangeEvent.event(e => { + const keysArr = Array.from(e.data.keys()); + const valuesArr = Array.from(e.data.values()); + this._proxy.$shellEnvChange(e.instance.instanceId, keysArr, valuesArr); + })); + // onDidStartTerminalShellExecution const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted)); let currentCommand: ITerminalCommand | undefined; diff --git a/code/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/code/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index bae2812c5f3..fa0d7fd2307 100644 --- a/code/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/code/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -13,7 +13,7 @@ import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, import { ThemeColor } from '../../../base/common/themables.js'; import { Command } from '../../../editor/common/languages.js'; import { IAccessibilityInformation, isAccessibilityInformation } from '../../../platform/accessibility/common/accessibility.js'; -import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { IMarkdownString, isMarkdownString } from '../../../base/common/htmlContent.js'; import { getCodiconAriaLabel } from '../../../base/common/iconLabels.js'; import { hash } from '../../../base/common/hash.js'; import { Event, Emitter } from '../../../base/common/event.js'; @@ -22,6 +22,7 @@ import { Iterable } from '../../../base/common/iterator.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { asStatusBarItemIdentifier } from '../common/extHostTypes.js'; import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND } from '../../common/theme.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; // --- service @@ -49,7 +50,7 @@ export interface IExtensionStatusBarItemService { onDidChange: Event; - setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): StatusBarUpdateKind; + setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined | IManagedHoverTooltipMarkdownString, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): StatusBarUpdateKind; unsetEntry(id: string): void; @@ -75,7 +76,8 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { } setOrUpdateEntry(entryId: string, - id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, + id: string, extensionId: string | undefined, name: string, text: string, + tooltip: IMarkdownString | string | undefined | IManagedHoverTooltipMarkdownString, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined ): StatusBarUpdateKind { @@ -87,7 +89,7 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { role = accessibilityInformation.role; } else { ariaLabel = getCodiconAriaLabel(text); - if (tooltip) { + if (typeof tooltip === 'string' || isMarkdownString(tooltip)) { const tooltipString = typeof tooltip === 'string' ? tooltip : tooltip.value; ariaLabel += `, ${tooltipString}`; } @@ -101,7 +103,7 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { color = undefined; backgroundColor = undefined; } - const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role, kind }; + const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role, kind, extensionId }; if (typeof priority === 'undefined') { priority = 0; diff --git a/code/src/vs/workbench/api/common/configurationExtensionPoint.ts b/code/src/vs/workbench/api/common/configurationExtensionPoint.ts index 3fa10161b9a..8b54b9e5a1d 100644 --- a/code/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/code/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -115,6 +115,13 @@ const configurationEntrySchema: IJSONSchema = { type: 'boolean', description: nls.localize('scope.ignoreSync', 'When enabled, Settings Sync will not sync the user value of this configuration by default.') }, + tags: { + type: 'array', + items: { + type: 'string' + }, + markdownDescription: nls.localize('scope.tags', 'A list of categories under which to place the setting. The category can then be searched up in the Settings editor. For example, specifying the `experimental` tag allows one to find the setting by searching `@tag:experimental`.'), + } } } ] diff --git a/code/src/vs/workbench/api/common/extHost.api.impl.ts b/code/src/vs/workbench/api/common/extHost.api.impl.ts index c1919e71363..d058e61b0da 100644 --- a/code/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/code/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1437,9 +1437,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatVariableResolver'); return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); }, - registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { + registerMappedEditsProvider(_selector: vscode.DocumentSelector, _provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); - return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); + // no longer supported + return { dispose() { } }; }, registerMappedEditsProvider2(provider: vscode.MappedEditsProvider2) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); @@ -1666,6 +1667,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalCompletionItem: extHostTypes.TerminalCompletionItem, TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind, TerminalCompletionList: extHostTypes.TerminalCompletionList, + TerminalShellType: extHostTypes.TerminalShellType, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, diff --git a/code/src/vs/workbench/api/common/extHost.protocol.ts b/code/src/vs/workbench/api/common/extHost.protocol.ts index 7bccc00f5b9..5072aac5a20 100644 --- a/code/src/vs/workbench/api/common/extHost.protocol.ts +++ b/code/src/vs/workbench/api/common/extHost.protocol.ts @@ -43,7 +43,7 @@ import { AuthInfo, Credentials } from '../../../platform/request/common/request. import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from '../../../platform/telemetry/common/gdprTypings.js'; import { TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js'; import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../platform/terminal/common/environmentVariable.js'; -import { ICreateContributedTerminalProfileOptions, IProcessProperty, IProcessReadyWindowsPty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalExitReason, TerminalLocation } from '../../../platform/terminal/common/terminal.js'; +import { ICreateContributedTerminalProfileOptions, IProcessProperty, IProcessReadyWindowsPty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalExitReason, TerminalLocation, TerminalShellType } from '../../../platform/terminal/common/terminal.js'; import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from '../../../platform/tunnel/common/tunnel.js'; import { EditSessionIdentityMatch } from '../../../platform/workspace/common/editSessions.js'; import { WorkspaceTrustRequestOptions } from '../../../platform/workspace/common/workspaceTrust.js'; @@ -455,7 +455,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean, extensionId: string, yieldsToExtensionIds: string[]): void; - $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier): void; + $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; $emitInlayHintsEvent(eventHandle: number): void; @@ -470,7 +470,6 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void; } export interface MainThreadLanguagesShape extends IDisposable { @@ -677,7 +676,7 @@ export interface MainThreadQuickOpenShape extends IDisposable { } export interface MainThreadStatusBarShape extends IDisposable { - $setEntry(id: string, statusId: string, extensionId: string | undefined, statusName: string, text: string, tooltip: IMarkdownString | string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void; + $setEntry(id: string, statusId: string, extensionId: string | undefined, statusName: string, text: string, tooltip: IMarkdownString | string | undefined, hasTooltipProvider: boolean, command: ICommandDto | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void; $disposeEntry(id: string): void; } @@ -694,6 +693,7 @@ export type StatusBarItemDto = { export interface ExtHostStatusBarShape { $acceptStaticEntries(added?: StatusBarItemDto[]): void; + $provideTooltip(entryId: string, cancellation: CancellationToken): Promise; } export interface MainThreadStorageShape extends IDisposable { @@ -1299,7 +1299,7 @@ export interface ICodeMapperTextEdit { export type ICodeMapperProgressDto = Dto; export interface MainThreadCodeMapperShape extends IDisposable { - $registerCodeMapperProvider(handle: number): void; + $registerCodeMapperProvider(handle: number, displayName: string): void; $unregisterCodeMapperProvider(handle: number): void; $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise; } @@ -1328,6 +1328,7 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $setRequestPaused(handle: number, requestId: string, isPaused: boolean): void; $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; @@ -1603,9 +1604,11 @@ export interface SCMHistoryItemRefsChangeEventDto { export interface SCMHistoryItemDto { readonly id: string; readonly parentIds: string[]; + readonly subject: string; readonly message: string; readonly displayId?: string; readonly author?: string; + readonly authorIcon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; readonly authorEmail?: string; readonly timestamp?: number; readonly statistics?: { @@ -1620,7 +1623,6 @@ export interface SCMHistoryItemChangeDto { readonly uri: UriComponents; readonly originalUri: UriComponents | undefined; readonly modifiedUri: UriComponents | undefined; - readonly renameUri: UriComponents | undefined; } export interface MainThreadSCMShape extends IDisposable { @@ -2333,7 +2335,6 @@ export interface ExtHostLanguageFeaturesShape { $releaseTypeHierarchy(handle: number, sessionId: string): void; $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; $releaseDocumentOnDropEdits(handle: number, cacheId: number): void; - $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; } @@ -2417,6 +2418,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $acceptTerminalInteraction(id: number): void; $acceptTerminalSelection(id: number, selection: string | undefined): void; + $acceptTerminalShellType(id: number, shellType: TerminalShellType | undefined): void; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; @@ -2439,6 +2441,7 @@ export interface ExtHostTerminalShellIntegrationShape { $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void; $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void; $shellExecutionData(instanceId: number, data: string): void; + $shellEnvChange(instanceId: number, shellEnvKeys: string[], shellEnvValues: string[]): void; $cwdChange(instanceId: number, cwd: UriComponents | undefined): void; $closeTerminal(instanceId: number): void; } diff --git a/code/src/vs/workbench/api/common/extHostApiCommands.ts b/code/src/vs/workbench/api/common/extHostApiCommands.ts index 8167b0ca3fa..1d08ef21082 100644 --- a/code/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/code/src/vs/workbench/api/common/extHostApiCommands.ts @@ -534,25 +534,6 @@ const newCommands: ApiCommand[] = [ ], ApiCommandResult.Void ), - // --- mapped edits - new ApiCommand( - 'vscode.executeMappedEditsProvider', '_executeMappedEditsProvider', 'Execute Mapped Edits Provider', - [ - ApiCommandArgument.Uri, - ApiCommandArgument.StringArray, - new ApiCommandArgument( - 'MappedEditsContext', - 'Mapped Edits Context', - (v: unknown) => typeConverters.MappedEditsContext.is(v), - (v: vscode.MappedEditsContext) => typeConverters.MappedEditsContext.from(v) - ) - ], - new ApiCommandResult( - 'A promise that resolves to a workspace edit or null', - (value) => { - return value ? typeConverters.WorkspaceEdit.to(value) : null; - }) - ), // --- inline chat new ApiCommand( 'vscode.editorChat.start', 'inlineChat.start', 'Invoke a new editor chat session', diff --git a/code/src/vs/workbench/api/common/extHostChatAgents2.ts b/code/src/vs/workbench/api/common/extHostChatAgents2.ts index 13a6572cba1..4bf84b803aa 100644 --- a/code/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/code/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -18,7 +18,7 @@ import { assertType } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { Location } from '../../../editor/common/languages.js'; -import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult, IChatAgentResultTimings, IChatWelcomeMessageContent } from '../../contrib/chat/common/chatAgents.js'; import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; @@ -296,6 +296,11 @@ class ChatAgentResponseStream { } } +interface InFlightChatRequest { + requestId: string; + extRequest: vscode.ChatRequest; +} + export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsShape2 { private static _idPool = 0; @@ -312,6 +317,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _sessionDisposables: DisposableMap = this._register(new DisposableMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); + private readonly _inFlightRequests = new Set(); + constructor( mainContext: IMainContext, private readonly _logService: ILogService, @@ -387,15 +394,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { - const { request, location, history } = await this._createRequest(requestDto, context); - const detector = this._participantDetectionProviders.get(handle); if (!detector) { return undefined; } + const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); + const model = await this.getModelForRequest(request, detector.extension); - const extRequest = typeConvert.ChatAgentRequest.to(request, location, model); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference')); return detector.provider.provideParticipantDetection( extRequest, @@ -405,9 +412,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS ); } - private async _createRequest(requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }) { + private async _createRequest(requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, extension: IExtensionDescription) { const request = revive(requestDto); - const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + const convertedHistory = await this.prepareHistoryTurns(extension, request.agentId, context); // in-place converting for location-data let location: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined; @@ -443,6 +450,20 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return model; } + async $setRequestPaused(handle: number, requestId: string, isPaused: boolean) { + const agent = this._agents.get(handle); + if (!agent) { + return; + } + + const inFlight = Iterable.find(this._inFlightRequests, r => r.requestId === requestId); + if (!inFlight) { + return; + } + + agent.setChatRequestPauseState({ request: inFlight.extRequest, isPaused }); + } + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { @@ -450,9 +471,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } let stream: ChatAgentResponseStream | undefined; + let inFlightRequest: InFlightChatRequest | undefined; try { - const { request, location, history } = await this._createRequest(requestDto, context); + const { request, location, history } = await this._createRequest(requestDto, context, agent.extension); // Init session disposables let sessionDisposables = this._sessionDisposables.get(request.sessionId); @@ -464,7 +486,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); const model = await this.getModelForRequest(request, agent.extension); - const extRequest = typeConvert.ChatAgentRequest.to(request, location, model); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference')); + inFlightRequest = { requestId: requestDto.requestId, extRequest }; + this._inFlightRequests.add(inFlightRequest); const task = agent.invoke( extRequest, @@ -507,11 +531,14 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true, isQuotaExceeded } }; } finally { + if (inFlightRequest) { + this._inFlightRequests.delete(inFlightRequest); + } stream?.close(); } } - private async prepareHistoryTurns(agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { + private async prepareHistoryTurns(extension: Readonly, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = []; for (const h of context.history) { @@ -521,9 +548,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS { ...ehResult, metadata: undefined }; // REQUEST turn + const hasReadonlyProposal = isProposedApiEnabled(extension, 'chatReadonlyPromptReference'); const varsWithoutTools = h.request.variables.variables .filter(v => !v.isTool) - .map(typeConvert.ChatPromptReference.to); + .map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal)); const toolReferences = h.request.variables.variables .filter(v => v.isTool) .map(typeConvert.ChatLanguageModelToolReference.to); @@ -549,7 +577,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const request = revive(requestDto); - const convertedHistory = await this.prepareHistoryTurns(agent.id, context); + const convertedHistory = await this.prepareHistoryTurns(agent.extension, agent.id, context); const ehResult = typeConvert.ChatAgentResult.to(result); return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) @@ -642,7 +670,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return; } - const history = await this.prepareHistoryTurns(agent.id, { history: context }); + const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context }); return await agent.provideTitle({ history }, token); } @@ -687,6 +715,7 @@ class ExtHostChatAgent { private _titleProvider?: vscode.ChatTitleProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; private _supportsSlowReferences: boolean | undefined; + private _pauseStateEmitter = new Emitter(); constructor( public readonly extension: IExtensionDescription, @@ -704,6 +733,10 @@ class ExtHostChatAgent { this._onDidPerformAction.fire(event); } + setChatRequestPauseState(pauseState: vscode.ChatParticipantPauseStateEvent) { + this._pauseStateEmitter.fire(pauseState); + } + async invokeCompletionProvider(query: string, token: CancellationToken): Promise { if (!this._agentVariableProvider) { return []; @@ -909,6 +942,10 @@ class ExtHostChatAgent { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); return that._titleProvider; }, + get onDidChangePauseState() { + checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + return that._pauseStateEmitter.event; + }, onDidPerformAction: !isProposedApiEnabled(this.extension, 'chatParticipantAdditions') ? undefined! : this._onDidPerformAction.event diff --git a/code/src/vs/workbench/api/common/extHostCodeMapper.ts b/code/src/vs/workbench/api/common/extHostCodeMapper.ts index 0f6b9d12922..ffa51e9a759 100644 --- a/code/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/code/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import * as extHostProtocol from './extHost.protocol.js'; -import { ChatAgentResult, DocumentContextItem, TextEdit } from './extHostTypeConverters.js'; +import { TextEdit } from './extHostTypeConverters.js'; import { URI } from '../../../base/common/uri.js'; export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape { @@ -42,27 +42,14 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape }; const request: vscode.MappedEditsRequest = { + location: internalRequest.location, + chatRequestId: internalRequest.chatRequestId, codeBlocks: internalRequest.codeBlocks.map(block => { return { code: block.code, resource: URI.revive(block.resource), markdownBeforeBlock: block.markdownBeforeBlock }; - }), - conversation: internalRequest.conversation.map(item => { - if (item.type === 'request') { - return { - type: 'request', - message: item.message - } satisfies vscode.ConversationRequest; - } else { - return { - type: 'response', - message: item.message, - result: item.result ? ChatAgentResult.to(item.result) : undefined, - references: item.references?.map(DocumentContextItem.to) - } satisfies vscode.ConversationResponse; - } }) }; @@ -72,7 +59,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape registerMappedEditsProvider(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { const handle = ExtHostCodeMapper._providerHandlePool++; - this._proxy.$registerCodeMapperProvider(handle); + this._proxy.$registerCodeMapperProvider(handle, extension.displayName ?? extension.name); this.providers.set(handle, provider); return { dispose: () => { diff --git a/code/src/vs/workbench/api/common/extHostCommands.ts b/code/src/vs/workbench/api/common/extHostCommands.ts index b1ff9817201..0c170ec48a9 100644 --- a/code/src/vs/workbench/api/common/extHostCommands.ts +++ b/code/src/vs/workbench/api/common/extHostCommands.ts @@ -28,7 +28,7 @@ import { VSBuffer } from '../../../base/common/buffer.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; -import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { TelemetryTrustedValue } from '../../../platform/telemetry/common/telemetryUtils.js'; import { IExtHostTelemetry } from './extHostTelemetry.js'; import { generateUuid } from '../../../base/common/uuid.js'; @@ -41,7 +41,7 @@ interface CommandHandler { } export interface ArgumentProcessor { - processArgument(arg: any, extensionId: ExtensionIdentifier | undefined): any; + processArgument(arg: any, extension: IExtensionDescription | undefined): any; } export class ExtHostCommands implements ExtHostCommandsShape { @@ -310,7 +310,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { if (!cmdHandler) { return Promise.reject(new Error(`Contributed command '${id}' does not exist.`)); } else { - args = args.map(arg => this._argumentProcessors.reduce((r, p) => p.processArgument(r, cmdHandler.extension?.identifier), arg)); + args = args.map(arg => this._argumentProcessors.reduce((r, p) => p.processArgument(r, cmdHandler.extension), arg)); return this._executeContributedCommand(id, args, true); } } diff --git a/code/src/vs/workbench/api/common/extHostConfiguration.ts b/code/src/vs/workbench/api/common/extHostConfiguration.ts index 5a474e8aa92..0b03122f8ee 100644 --- a/code/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/code/src/vs/workbench/api/common/extHostConfiguration.ts @@ -32,15 +32,19 @@ function lookUp(tree: any, key: string) { } } -type ConfigurationInspect = { +export type ConfigurationInspect = { key: string; defaultValue?: T; + globalLocalValue?: T; + globalRemoteValue?: T; globalValue?: T; workspaceValue?: T; workspaceFolderValue?: T; defaultLanguageValue?: T; + globalLocalLanguageValue?: T; + globalRemoteLanguageValue?: T; globalLanguageValue?: T; workspaceLanguageValue?: T; workspaceFolderLanguageValue?: T; @@ -262,11 +266,15 @@ export class ExtHostConfigProvider { key, defaultValue: deepClone(config.policy?.value ?? config.default?.value), + globalLocalValue: deepClone(config.userLocal?.value), + globalRemoteValue: deepClone(config.userRemote?.value), globalValue: deepClone(config.user?.value ?? config.application?.value), workspaceValue: deepClone(config.workspace?.value), workspaceFolderValue: deepClone(config.workspaceFolder?.value), defaultLanguageValue: deepClone(config.default?.override), + globalLocalLanguageValue: deepClone(config.userLocal?.override), + globalRemoteLanguageValue: deepClone(config.userRemote?.override), globalLanguageValue: deepClone(config.user?.override ?? config.application?.override), workspaceLanguageValue: deepClone(config.workspace?.override), workspaceFolderLanguageValue: deepClone(config.workspaceFolder?.override), diff --git a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ac9576a3b22..70e460334a9 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -31,7 +31,7 @@ import { ExtHostDiagnostics } from './extHostDiagnostics.js'; import { ExtHostDocuments } from './extHostDocuments.js'; import { ExtHostTelemetry, IExtHostTelemetry } from './extHostTelemetry.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js'; +import { CodeAction, CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import type * as vscode from 'vscode'; import { Cache } from './cache.js'; @@ -501,7 +501,8 @@ class CodeActionAdapter { if (!candidate) { continue; } - if (CodeActionAdapter._isCommand(candidate)) { + + if (CodeActionAdapter._isCommand(candidate) && !(candidate instanceof CodeAction)) { // old school: synthetic code action this._apiDeprecation.report('CodeActionProvider.provideCodeActions - return commands', this._extension, `Return 'CodeAction' instances instead.`); @@ -512,29 +513,31 @@ class CodeActionAdapter { command: this._commands.toInternal(candidate, disposables), }); } else { + const toConvert = candidate as vscode.CodeAction; + + // new school: convert code action if (codeActionContext.only) { - if (!candidate.kind) { + if (!toConvert.kind) { this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); - } else if (!codeActionContext.only.contains(candidate.kind)) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + } else if (!codeActionContext.only.contains(toConvert.kind)) { + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${toConvert.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); } } // Ensures that this is either a Range[] or an empty array so we don't get Array - const range = candidate.ranges ?? []; + const range = toConvert.ranges ?? []; - // new school: convert code action actions.push({ cacheId: [cacheId, i], - title: candidate.title, - command: candidate.command && this._commands.toInternal(candidate.command, disposables), - diagnostics: candidate.diagnostics && candidate.diagnostics.map(typeConvert.Diagnostic.from), - edit: candidate.edit && typeConvert.WorkspaceEdit.from(candidate.edit, undefined), - kind: candidate.kind && candidate.kind.value, - isPreferred: candidate.isPreferred, - isAI: isProposedApiEnabled(this._extension, 'codeActionAI') ? candidate.isAI : false, + title: toConvert.title, + command: toConvert.command && this._commands.toInternal(toConvert.command, disposables), + diagnostics: toConvert.diagnostics && toConvert.diagnostics.map(typeConvert.Diagnostic.from), + edit: toConvert.edit && typeConvert.WorkspaceEdit.from(toConvert.edit, undefined), + kind: toConvert.kind && toConvert.kind.value, + isPreferred: toConvert.isPreferred, + isAI: isProposedApiEnabled(this._extension, 'codeActionAI') ? toConvert.isAI : false, ranges: isProposedApiEnabled(this._extension, 'codeActionRanges') ? coalesce(range.map(typeConvert.Range.from)) : undefined, - disabled: candidate.disabled?.reason + disabled: toConvert.disabled?.reason }); } } @@ -2163,57 +2166,6 @@ class DocumentDropEditAdapter { } } -class MappedEditsAdapter { - - constructor( - private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.MappedEditsProvider, - ) { } - - async provideMappedEdits( - resource: UriComponents, - codeBlocks: string[], - context: extHostProtocol.IMappedEditsContextDto, - token: CancellationToken - ): Promise { - - const uri = URI.revive(resource); - const doc = this._documents.getDocument(uri); - - const reviveContextItem = (item: extHostProtocol.IDocumentContextItemDto) => ({ - uri: URI.revive(item.uri), - version: item.version, - ranges: item.ranges.map(r => typeConvert.Range.to(r)), - } satisfies vscode.DocumentContextItem); - - - const usedContext = context.documents.map(docSubArray => docSubArray.map(reviveContextItem)); - - const ctx = { - documents: usedContext, - selections: usedContext[0]?.[0]?.ranges ?? [], // @ulugbekna: this is a hack for backward compatibility - conversation: context.conversation?.map(c => { - if (c.type === 'response') { - return { - type: 'response', - message: c.message, - references: c.references?.map(reviveContextItem) - } satisfies vscode.ConversationResponse; - } else { - return { - type: 'request', - message: c.message, - } satisfies vscode.ConversationRequest; - } - }) - }; - - const mappedEdits = await this._provider.provideMappedEdits(doc, codeBlocks, ctx, token); - - return mappedEdits ? typeConvert.WorkspaceEdit.from(mappedEdits) : null; - } -} - type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | MultiDocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter @@ -2224,7 +2176,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter - | DocumentDropEditAdapter | MappedEditsAdapter | NewSymbolNamesAdapter | InlineEditAdapter; + | DocumentDropEditAdapter | NewSymbolNamesAdapter | InlineEditAdapter; class AdapterData { constructor( @@ -2729,7 +2681,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerInlineEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineEditProvider): vscode.Disposable { const adapter = new InlineEditAdapter(extension, this._documents, provider, this._commands.converter); const handle = this._addNewAdapter(adapter, extension); - this._proxy.$registerInlineEditProvider(handle, this._transformDocumentSelector(selector, extension), extension.identifier); + this._proxy.$registerInlineEditProvider(handle, this._transformDocumentSelector(selector, extension), extension.identifier, provider.displayName || extension.name); return this._createDisposable(handle); } @@ -2938,19 +2890,6 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.releaseDropEdits(cacheId)), undefined, undefined); } - // --- mapped edits - - registerMappedEditsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable { - const handle = this._addNewAdapter(new MappedEditsAdapter(this._documents, provider), extension); - this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension), extension.displayName ?? extension.name); - return this._createDisposable(handle); - } - - $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: extHostProtocol.IMappedEditsContextDto, token: CancellationToken): Promise { - return this._withAdapter(handle, MappedEditsAdapter, adapter => - Promise.resolve(adapter.provideMappedEdits(document, codeBlocks, context, token)), null, token); - } - // --- copy/paste actions registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { diff --git a/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts index b2f25fb152f..d07093176f2 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,11 +12,9 @@ import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; - - export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { /** A map of tools that were registered in this EH */ @@ -57,8 +55,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Invalid tool invocation token`); } - const tool = this._allTools.get(toolId); - if (tool?.tags?.includes('vscode_editing') && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + if (toolId === 'vscode_editFile' && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { throw new Error(`Invalid tool: ${toolId}`); } @@ -69,6 +66,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, context: options.toolInvocationToken as IToolInvocationContext | undefined, + chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, }, token); return typeConvert.LanguageModelToolResult.to(result); } finally { @@ -87,7 +85,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return Array.from(this._allTools.values()) .map(tool => typeConvert.LanguageModelToolDescription.to(tool)) .filter(tool => { - if (tool.tags.includes('vscode_editing')) { + if (tool.name === 'vscode_editFile') { return isProposedApiEnabled(extension, 'chatParticipantPrivate'); } @@ -101,7 +99,11 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Unknown tool ${dto.toolId}`); } - const options: vscode.LanguageModelToolInvocationOptions = { input: dto.parameters, toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined }; + const options: vscode.LanguageModelToolInvocationOptions = { + input: dto.parameters, + toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined, + chatRequestId: dto.chatRequestId, + }; if (dto.tokenBudget !== undefined) { options.tokenizationOptions = { tokenBudget: dto.tokenBudget, @@ -134,16 +136,18 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return undefined; } + if (result.pastTenseMessage || result.tooltip) { + checkProposedApiEnabled(item.extension, 'chatParticipantPrivate'); + } + return { confirmationMessages: result.confirmationMessages ? { title: result.confirmationMessages.title, message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message), } : undefined, - invocationMessage: typeof result.invocationMessage === 'string' ? - result.invocationMessage : - (result.invocationMessage ? - typeConvert.MarkdownString.from(result.invocationMessage) : - undefined), + invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage), + pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage), + tooltip: result.tooltip ? typeConvert.MarkdownString.fromStrict(result.tooltip) : undefined, }; } diff --git a/code/src/vs/workbench/api/common/extHostLanguageModels.ts b/code/src/vs/workbench/api/common/extHostLanguageModels.ts index b5267b761c7..7ae740ff9fa 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -180,6 +180,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, + capabilities: metadata.capabilities, }); const responseReceivedListener = provider.onDidReceiveLanguageModelResponse2?.(({ extensionId, participant, tokenCount }) => { @@ -434,7 +435,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (error) { // we error the stream because that's the only way to signal // that the request has failed - data.res.reject(transformErrorFromSerialization(error)); + data.res.reject(extHostTypes.LanguageModelError.tryDeserialize(error) ?? transformErrorFromSerialization(error)); } else { data.res.resolve(); } diff --git a/code/src/vs/workbench/api/common/extHostMemento.ts b/code/src/vs/workbench/api/common/extHostMemento.ts index e1dc4f27947..baad57cfd67 100644 --- a/code/src/vs/workbench/api/common/extHostMemento.ts +++ b/code/src/vs/workbench/api/common/extHostMemento.ts @@ -76,7 +76,15 @@ export class ExtensionMemento implements vscode.Memento { } update(key: string, value: any): Promise { - this._value![key] = value; + if (value !== null && typeof value === 'object') { + // Prevent the value from being as-is for until we have + // received the change event from the main side by emulating + // the treatment of values via JSON parsing and stringifying. + // (https://github.com/microsoft/vscode/issues/209479) + this._value![key] = JSON.parse(JSON.stringify(value)); + } else { + this._value![key] = value; + } const record = this._deferredPromises.get(key); if (record !== undefined) { diff --git a/code/src/vs/workbench/api/common/extHostSCM.ts b/code/src/vs/workbench/api/common/extHostSCM.ts index ae2930c763f..41684493051 100644 --- a/code/src/vs/workbench/api/common/extHostSCM.ts +++ b/code/src/vs/workbench/api/common/extHostSCM.ts @@ -73,11 +73,12 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc } function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { + const authorIcon = getHistoryItemIconDto(historyItem.authorIcon); const references = historyItem.references?.map(r => ({ ...r, icon: getHistoryItemIconDto(r.icon) })); - return { ...historyItem, references }; + return { ...historyItem, authorIcon, references }; } function toSCMHistoryItemRefDto(historyItemRef?: vscode.SourceControlHistoryItemRef): SCMHistoryItemRefDto | undefined { diff --git a/code/src/vs/workbench/api/common/extHostStatusBar.ts b/code/src/vs/workbench/api/common/extHostStatusBar.ts index 327d53c5c92..c31d8ec28e0 100644 --- a/code/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/code/src/vs/workbench/api/common/extHostStatusBar.ts @@ -14,6 +14,8 @@ import { DisposableStore } from '../../../base/common/lifecycle.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { MarkdownString } from './extHostTypeConverters.js'; import { isNumber } from '../../../base/common/types.js'; +import * as htmlContent from '../../../base/common/htmlContent.js'; +import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { @@ -43,6 +45,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _text: string = ''; private _tooltip?: string | vscode.MarkdownString; + private _tooltip2?: string | vscode.MarkdownString | undefined | ((token: vscode.CancellationToken) => Promise); private _name?: string; private _color?: string | ThemeColor; private _backgroundColor?: ThemeColor; @@ -57,9 +60,9 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number); - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number); - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number, _onDispose?: () => void); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number, _onDispose?: () => void); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, private _onDispose?: () => void) { this.#proxy = proxy; this.#commands = commands; @@ -113,6 +116,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._id ?? this._extension!.identifier.value; } + public get entryId(): string { + return this._entryId; + } + public get alignment(): vscode.StatusBarAlignment { return this._alignment; } @@ -133,6 +140,14 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._tooltip; } + public get tooltip2(): vscode.MarkdownString | string | undefined | ((token: vscode.CancellationToken) => Promise) { + if (this._extension) { + checkProposedApiEnabled(this._extension, 'statusBarItemTooltip'); + } + + return this._tooltip2; + } + public get color(): string | ThemeColor | undefined { return this._color; } @@ -164,6 +179,15 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this.update(); } + public set tooltip2(tooltip: vscode.MarkdownString | string | undefined | ((token: vscode.CancellationToken) => Promise)) { + if (this._extension) { + checkProposedApiEnabled(this._extension, 'statusBarItemTooltip'); + } + + this._tooltip2 = tooltip; + this.update(); + } + public set color(color: string | ThemeColor | undefined) { this._color = color; this.update(); @@ -258,10 +282,18 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { color = ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.get(this._backgroundColor.id); } - const tooltip = MarkdownString.fromStrict(this._tooltip); + let tooltip: undefined | string | htmlContent.IMarkdownString; + let hasTooltipProvider: boolean; + if (typeof this._tooltip2 === 'function') { + tooltip = MarkdownString.fromStrict(this._tooltip); + hasTooltipProvider = true; + } else { + tooltip = MarkdownString.fromStrict(this._tooltip2 ?? this._tooltip); + hasTooltipProvider = false; + } // Set to status bar - this.#proxy.$setEntry(this._entryId, id, this._extension?.identifier.value, name, this._text, tooltip, this._command?.internal, color, + this.#proxy.$setEntry(this._entryId, id, this._extension?.identifier.value, name, this._text, tooltip, hasTooltipProvider, this._command?.internal, color, this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left, this._priority, this._accessibilityInformation); @@ -272,6 +304,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { public dispose(): void { this.hide(); + this._onDispose?.(); this._disposed = true; } } @@ -320,6 +353,7 @@ export class ExtHostStatusBar implements ExtHostStatusBarShape { private readonly _proxy: MainThreadStatusBarShape; private readonly _commands: CommandsConverter; private readonly _statusMessage: StatusBarMessage; + private readonly _entries = new Map(); private readonly _existingItems = new Map(); constructor(mainContext: IMainContext, commands: CommandsConverter) { @@ -334,10 +368,23 @@ export class ExtHostStatusBar implements ExtHostStatusBarShape { } } + async $provideTooltip(entryId: string, cancellation: vscode.CancellationToken): Promise { + const entry = this._entries.get(entryId); + if (!entry) { + return undefined; + } + + const tooltip = typeof entry.tooltip2 === 'function' ? await entry.tooltip2(cancellation) : entry.tooltip2; + return !cancellation.isCancellationRequested ? MarkdownString.fromStrict(tooltip) : undefined; + } + 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, this._existingItems, extension, id, alignment, priority); + const entry = new ExtHostStatusBarEntry(this._proxy, this._commands, this._existingItems, extension, id, alignment, priority, () => this._entries.delete(entry.entryId)); + this._entries.set(entry.entryId, entry); + + return entry; } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/code/src/vs/workbench/api/common/extHostTelemetry.ts b/code/src/vs/workbench/api/common/extHostTelemetry.ts index ffef0eeb5d8..71e920b72df 100644 --- a/code/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/code/src/vs/workbench/api/common/extHostTelemetry.ts @@ -13,7 +13,7 @@ import { IExtHostInitDataService } from './extHostInitDataService.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; import { getRemoteName } from '../../../platform/remote/common/remoteHosts.js'; -import { cleanData, cleanRemoteAuthority, extensionTelemetryLogChannelId } from '../../../platform/telemetry/common/telemetryUtils.js'; +import { cleanData, cleanRemoteAuthority, extensionTelemetryLogChannelId, TelemetryLogGroup } from '../../../platform/telemetry/common/telemetryUtils.js'; import { mixin } from '../../../base/common/objects.js'; import { URI } from '../../../base/common/uri.js'; import { Disposable } from '../../../base/common/lifecycle.js'; @@ -46,15 +46,19 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap super(); this.extHostTelemetryLogFile = URI.revive(this.initData.environment.extensionTelemetryLogResource); this._inLoggingOnlyMode = this.initData.environment.isExtensionTelemetryLoggingOnly; - this._outputLogger = loggerService.createLogger(this.extHostTelemetryLogFile, { id: extensionTelemetryLogChannelId, name: localize('extensionTelemetryLog', "Extension Telemetry{0}", this._inLoggingOnlyMode ? ' (Not Sent)' : ''), hidden: true }); + this._outputLogger = loggerService.createLogger(this.extHostTelemetryLogFile, + { + id: extensionTelemetryLogChannelId, + name: localize('extensionTelemetryLog', "Extension Telemetry{0}", this._inLoggingOnlyMode ? ' (Not Sent)' : ''), + hidden: true, + group: TelemetryLogGroup, + }); this._register(this._outputLogger); this._register(loggerService.onDidChangeLogLevel(arg => { if (isLogLevel(arg)) { this.updateLoggerVisibility(); } })); - this._outputLogger.info('Below are logs for extension telemetry events sent to the telemetry output channel API once the log level is set to trace.'); - this._outputLogger.info('==========================================================='); } private updateLoggerVisibility(): void { @@ -104,6 +108,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap commonProperties['common.extversion'] = extension.version; commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId; commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId; + commonProperties['common.vscodecommithash'] = this.initData.commit; commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId; commonProperties['common.devDeviceId'] = this.initData.telemetryInfo.devDeviceId; commonProperties['common.vscodeversion'] = this.initData.version; diff --git a/code/src/vs/workbench/api/common/extHostTerminalService.ts b/code/src/vs/workbench/api/common/extHostTerminalService.ts index 907fb7125e5..1860666282d 100644 --- a/code/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/code/src/vs/workbench/api/common/extHostTerminalService.ts @@ -10,7 +10,7 @@ import { createDecorator } from '../../../platform/instantiation/common/instanti import { URI } from '../../../base/common/uri.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IDisposable, DisposableStore, Disposable, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, TerminalExitReason, TerminalCompletionItem } from './extHostTypes.js'; +import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, TerminalExitReason, TerminalCompletionItem, TerminalShellType as VSCodeTerminalShellType } from './extHostTypes.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { localize } from '../../../nls.js'; import { NotSupportedError } from '../../../base/common/errors.js'; @@ -18,7 +18,7 @@ import { serializeEnvironmentDescriptionMap, serializeEnvironmentVariableCollect import { CancellationTokenSource } from '../../../base/common/cancellation.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IEnvironmentVariableCollectionDescription, IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from '../../../platform/terminal/common/environmentVariable.js'; -import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from '../../../platform/terminal/common/terminal.js'; +import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, ProcessPropertyType, IProcessPropertyMap, TerminalShellType, PosixShellType, WindowsShellType, GeneralShellType } from '../../../platform/terminal/common/terminal.js'; import { TerminalDataBufferer } from '../../../platform/terminal/common/terminalDataBuffering.js'; import { ThemeColor } from '../../../base/common/themables.js'; import { Promises } from '../../../base/common/async.js'; @@ -85,7 +85,7 @@ export class ExtHostTerminal extends Disposable { private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; private _rows: number | undefined; private _exitStatus: vscode.TerminalExitStatus | undefined; - private _state: vscode.TerminalState = { isInteractedWith: false }; + private _state: vscode.TerminalState = { isInteractedWith: false, shellType: undefined }; private _selection: string | undefined; shellIntegration: vscode.TerminalShellIntegration | undefined; @@ -258,7 +258,40 @@ export class ExtHostTerminal extends Disposable { public setInteractedWith(): boolean { if (!this._state.isInteractedWith) { - this._state = { isInteractedWith: true }; + this._state = { + ...this._state, + isInteractedWith: true + }; + return true; + } + return false; + } + + public setShellType(shellType: TerminalShellType | undefined): boolean { + + let extHostType: VSCodeTerminalShellType | undefined; + + switch (shellType) { + case PosixShellType.Sh: extHostType = VSCodeTerminalShellType.Sh; break; + case PosixShellType.Bash: extHostType = VSCodeTerminalShellType.Bash; break; + case PosixShellType.Fish: extHostType = VSCodeTerminalShellType.Fish; break; + case PosixShellType.Csh: extHostType = VSCodeTerminalShellType.Csh; break; + case PosixShellType.Ksh: extHostType = VSCodeTerminalShellType.Ksh; break; + case PosixShellType.Zsh: extHostType = VSCodeTerminalShellType.Zsh; break; + case WindowsShellType.CommandPrompt: extHostType = VSCodeTerminalShellType.CommandPrompt; break; + case WindowsShellType.GitBash: extHostType = VSCodeTerminalShellType.GitBash; break; + case GeneralShellType.PowerShell: extHostType = VSCodeTerminalShellType.PowerShell; break; + case GeneralShellType.Python: extHostType = VSCodeTerminalShellType.Python; break; + case GeneralShellType.Julia: extHostType = VSCodeTerminalShellType.Julia; break; + case GeneralShellType.NuShell: extHostType = VSCodeTerminalShellType.NuShell; break; + default: extHostType = undefined; break; + } + + if (this._state.shellType !== shellType) { + this._state = { + ...this._state, + shellType: extHostType + }; return true; } return false; @@ -765,6 +798,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return completions; } + public $acceptTerminalShellType(id: number, shellType: TerminalShellType | undefined): void { + const terminal = this.getTerminalById(id); + if (terminal?.setShellType(shellType)) { + this._onDidChangeTerminalState.fire(terminal.value); + } + } + public registerTerminalQuickFixProvider(id: string, extensionId: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable { if (this._quickFixProviders.has(id)) { throw new Error(`Terminal quick fix provider "${id}" is already registered`); diff --git a/code/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/code/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts index 7a1c24029d6..076f1b725cc 100644 --- a/code/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts +++ b/code/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -131,6 +131,10 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH this._activeShellIntegrations.get(instanceId)?.emitData(data); } + public $shellEnvChange(instanceId: number, shellEnvKeys: string[], shellEnvValues: string[]): void { + this._activeShellIntegrations.get(instanceId)?.setEnv(shellEnvKeys, shellEnvValues); + } + public $cwdChange(instanceId: number, cwd: UriComponents | undefined): void { this._activeShellIntegrations.get(instanceId)?.setCwd(URI.revive(cwd)); } @@ -147,6 +151,7 @@ class InternalTerminalShellIntegration extends Disposable { get currentExecution(): InternalTerminalShellExecution | undefined { return this._currentExecution; } private _ignoreNextExecution: boolean = false; + private _env: { [key: string]: string | undefined } | undefined; private _cwd: URI | undefined; readonly store: DisposableStore = this._register(new DisposableStore()); @@ -171,6 +176,9 @@ class InternalTerminalShellIntegration extends Disposable { get cwd(): URI | undefined { return that._cwd; }, + get env(): { [key: string]: string | undefined } | undefined { + return that._env; + }, // executeCommand(commandLine: string): vscode.TerminalShellExecution; // executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution; executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution { @@ -232,6 +240,15 @@ class InternalTerminalShellIntegration extends Disposable { } } + setEnv(keys: string[], values: string[]): void { + const env: { [key: string]: string | undefined } = {}; + for (let i = 0; i < keys.length; i++) { + env[keys[i]] = values[i]; + } + this._env = env; + this._fireChangeEvent(); + } + setCwd(cwd: URI | undefined): void { let wasChanged = false; if (URI.isUri(this._cwd)) { @@ -241,9 +258,13 @@ class InternalTerminalShellIntegration extends Disposable { } if (wasChanged) { this._cwd = cwd; - this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value }); + this._fireChangeEvent(); } } + + private _fireChangeEvent() { + this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value }); + } } class InternalTerminalShellExecution { diff --git a/code/src/vs/workbench/api/common/extHostTimeline.ts b/code/src/vs/workbench/api/common/extHostTimeline.ts index 4bfa096c1ce..bd631ddb10b 100644 --- a/code/src/vs/workbench/api/common/extHostTimeline.ts +++ b/code/src/vs/workbench/api/common/extHostTimeline.ts @@ -16,6 +16,7 @@ import { MarkdownString } from './extHostTypeConverters.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { isString } from '../../../base/common/types.js'; +import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; @@ -42,7 +43,7 @@ export class ExtHostTimeline implements IExtHostTimeline { commands.registerArgumentProcessor({ processArgument: (arg, extension) => { if (arg && arg.$mid === MarshalledId.TimelineActionContext) { - if (this._providers.get(arg.source) && ExtensionIdentifier.equals(extension, this._providers.get(arg.source)?.extension)) { + if (this._providers.get(arg.source) && extension && isProposedApiEnabled(extension, 'timeline')) { const uri = arg.uri === undefined ? undefined : URI.revive(arg.uri); return this._itemsBySourceAndUriMap.get(arg.source)?.get(getUriKey(uri))?.get(arg.handle); } else { diff --git a/code/src/vs/workbench/api/common/extHostTypeConverters.ts b/code/src/vs/workbench/api/common/extHostTypeConverters.ts index b7f3d7e8b5e..f535d17683b 100644 --- a/code/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/code/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1619,71 +1619,6 @@ export namespace LanguageSelector { } } -export namespace MappedEditsContext { - - export function is(v: unknown): v is vscode.MappedEditsContext { - return ( - !!v && typeof v === 'object' && - 'documents' in v && - Array.isArray(v.documents) && - v.documents.every( - subArr => Array.isArray(subArr) && - subArr.every(DocumentContextItem.is)) - ); - } - - export function from(extContext: vscode.MappedEditsContext): Dto { - return { - documents: extContext.documents.map((subArray) => - subArray.map(DocumentContextItem.from) - ), - conversation: extContext.conversation?.map(item => ( - (item.type === 'request') ? - { - type: 'request', - message: item.message, - } : - { - type: 'response', - message: item.message, - result: item.result ? ChatAgentResult.from(item.result) : undefined, - references: item.references?.map(DocumentContextItem.from) - } - )) - }; - - } -} - -export namespace DocumentContextItem { - - export function is(item: unknown): item is vscode.DocumentContextItem { - return ( - typeof item === 'object' && - item !== null && - 'uri' in item && URI.isUri(item.uri) && - 'version' in item && typeof item.version === 'number' && - 'ranges' in item && Array.isArray(item.ranges) && item.ranges.every((r: unknown) => r instanceof types.Range) - ); - } - - export function from(item: vscode.DocumentContextItem): Dto { - return { - uri: item.uri, - version: item.version, - ranges: item.ranges.map(r => Range.from(r)), - }; - } - - export function to(item: Dto): vscode.DocumentContextItem { - return { - uri: URI.revive(item.uri), - version: item.version, - ranges: item.ranges.map(r => Range.to(r)), - }; - } -} - export namespace NotebookRange { export function from(range: vscode.NotebookRange): ICellRange { @@ -2799,7 +2734,7 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.isTool); const variableReferences = request.variables.variables.filter(v => !v.isTool); return { @@ -2808,7 +2743,7 @@ export namespace ChatAgentRequest { attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, - references: variableReferences.map(ChatPromptReference.to), + references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal)), toolReferences: toolReferences.map(ChatLanguageModelToolReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, @@ -2852,7 +2787,7 @@ export namespace ChatLocation { } export namespace ChatPromptReference { - export function to(variable: IChatRequestVariableEntry): vscode.ChatPromptReference { + export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean): vscode.ChatPromptReference { const value = variable.value; if (!value) { throw new Error('Invalid value reference'); @@ -2864,8 +2799,9 @@ export namespace ChatPromptReference { range: variable.range && [variable.range.start, variable.range.endExclusive], value: isUriComponents(value) ? URI.revive(value) : value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ? - Location.to(revive(value)) : variable.isImage ? new types.ChatReferenceBinaryData(variable.mimeType ?? 'image/png', () => Promise.resolve(new Uint8Array(Object.values(value)))) : value, - modelDescription: variable.modelDescription + Location.to(revive(value)) : variable.isImage ? new types.ChatReferenceBinaryData(variable.mimeType ?? 'image/png', () => Promise.resolve(new Uint8Array(Object.values(value))), variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined) : value, + modelDescription: variable.modelDescription, + isReadonly: hasReadonlyProposal ? variable.isMarkedReadonly : undefined, }; } } diff --git a/code/src/vs/workbench/api/common/extHostTypes.ts b/code/src/vs/workbench/api/common/extHostTypes.ts index 667b018aa33..26ae6d8182e 100644 --- a/code/src/vs/workbench/api/common/extHostTypes.ts +++ b/code/src/vs/workbench/api/common/extHostTypes.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import { asArray, coalesceInPlace, equals } from '../../../base/common/arrays.js'; -import { illegalArgument } from '../../../base/common/errors.js'; +import { illegalArgument, SerializedError } from '../../../base/common/errors.js'; import { IRelativePattern } from '../../../base/common/glob.js'; import { MarkdownString as BaseMarkdownString, MarkdownStringTrustedOptions } from '../../../base/common/htmlContent.js'; import { ResourceMap } from '../../../base/common/map.js'; @@ -2072,6 +2072,22 @@ export enum TerminalShellExecutionCommandLineConfidence { High = 2 } +export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 +} + export class TerminalLink implements vscode.TerminalLink { constructor( public startIndex: number, @@ -4682,9 +4698,11 @@ export class ChatRequestNotebookData implements vscode.ChatRequestNotebookData { export class ChatReferenceBinaryData implements vscode.ChatReferenceBinaryData { mimeType: string; data: () => Thenable; - constructor(mimeType: string, data: () => Thenable) { + reference?: vscode.Uri; + constructor(mimeType: string, data: () => Thenable, reference?: vscode.Uri) { this.mimeType = mimeType; this.data = data; + this.reference = reference; } } @@ -4847,6 +4865,8 @@ export class LanguageModelChatAssistantMessage { export class LanguageModelError extends Error { + static readonly #name = 'LanguageModelError'; + static NotFound(message?: string): LanguageModelError { return new LanguageModelError(message, LanguageModelError.NotFound.name); } @@ -4859,11 +4879,18 @@ export class LanguageModelError extends Error { return new LanguageModelError(message, LanguageModelError.Blocked.name); } + static tryDeserialize(data: SerializedError): LanguageModelError | undefined { + if (data.name !== LanguageModelError.#name) { + return undefined; + } + return new LanguageModelError(data.message, data.code, data.cause); + } + readonly code: string; constructor(message?: string, code?: string, cause?: Error) { super(message, { cause }); - this.name = 'LanguageModelError'; + this.name = LanguageModelError.#name; this.code = code ?? ''; } diff --git a/code/src/vs/workbench/api/node/proxyResolver.ts b/code/src/vs/workbench/api/node/proxyResolver.ts index a4e4952e34a..9d36002ae3f 100644 --- a/code/src/vs/workbench/api/node/proxyResolver.ts +++ b/code/src/vs/workbench/api/node/proxyResolver.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IExtHostWorkspaceProvider } from '../common/extHostWorkspace.js'; -import { ExtHostConfigProvider } from '../common/extHostConfiguration.js'; +import { ConfigurationInspect, ExtHostConfigProvider } from '../common/extHostConfiguration.js'; import { MainThreadTelemetryShape } from '../common/extHost.protocol.js'; import { IExtensionHostInitData } from '../../services/extensions/common/extensionHostProtocol.js'; import { ExtHostExtensionService } from './extHostExtensionService.js'; @@ -39,17 +39,20 @@ export function connectProxyResolver( disposables: DisposableStore, ) { - const useHostProxy = initData.environment.useHostProxy; - const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; + const isRemote = initData.remote.isRemote; + const useHostProxyDefault = initData.environment.useHostProxy ?? !isRemote; + const fallbackToLocalKerberos = useHostProxyDefault; + const loadLocalCertificates = useHostProxyDefault; + const isUseHostProxyEnabled = () => !isRemote || configProvider.getConfiguration('http').get('useLocalProxyConfiguration', useHostProxyDefault); const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), - lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, doUseHostProxy), - getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), - getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', - getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], - isAdditionalFetchSupportEnabled: () => configProvider.getConfiguration('http').get('fetchAdditionalSupport', true), - addCertificatesV1: () => certSettingV1(configProvider), - addCertificatesV2: () => certSettingV2(configProvider), + lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, fallbackToLocalKerberos), + getProxyURL: () => getExtHostConfigValue(configProvider, isRemote, 'http.proxy'), + getProxySupport: () => getExtHostConfigValue(configProvider, isRemote, 'http.proxySupport') || 'off', + getNoProxyConfig: () => getExtHostConfigValue(configProvider, isRemote, 'http.noProxy') || [], + isAdditionalFetchSupportEnabled: () => getExtHostConfigValue(configProvider, isRemote, 'http.fetchAdditionalSupport', true), + addCertificatesV1: () => certSettingV1(configProvider, isRemote), + addCertificatesV2: () => certSettingV2(configProvider, isRemote), log: extHostLogService, getLogLevel: () => { const level = extHostLogService.getLevel(); @@ -68,13 +71,13 @@ export function connectProxyResolver( } }, proxyResolveTelemetry: () => { }, - useHostProxy: doUseHostProxy, + useHostProxy: isUseHostProxyEnabled(), loadAdditionalCertificates: async () => { const promises: Promise[] = []; if (initData.remote.isRemote) { promises.push(loadSystemCertificates({ log: extHostLogService })); } - if (doUseHostProxy) { + if (loadLocalCertificates) { extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading certificates from main process'); const certs = extHostWorkspace.loadCertificates(); // Loading from main process to share cache. certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length)); @@ -249,14 +252,12 @@ function createPatchedModules(params: ProxyAgentParams, resolveProxy: ResolvePro }; } -function certSettingV1(configProvider: ExtHostConfigProvider) { - const http = configProvider.getConfiguration('http'); - return !http.get('experimental.systemCertificatesV2', systemCertificatesV2Default) && !!http.get('systemCertificates'); +function certSettingV1(configProvider: ExtHostConfigProvider, isRemote: boolean) { + return !getExtHostConfigValue(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue(configProvider, isRemote, 'http.systemCertificates'); } -function certSettingV2(configProvider: ExtHostConfigProvider) { - const http = configProvider.getConfiguration('http'); - return !!http.get('experimental.systemCertificatesV2', systemCertificatesV2Default) && !!http.get('systemCertificates'); +function certSettingV2(configProvider: ExtHostConfigProvider, isRemote: boolean) { + return !!getExtHostConfigValue(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue(configProvider, isRemote, 'http.systemCertificates'); } const modulesCache = new Map(); @@ -306,7 +307,7 @@ async function lookupProxyAuthorization( proxyAuthenticateCache: Record, basicAuthCache: Record, isRemote: boolean, - useHostProxy: boolean, + fallbackToLocalKerberos: boolean, proxyURL: string, proxyAuthenticate: string | string[] | undefined, state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number } @@ -323,14 +324,14 @@ async function lookupProxyAuthorization( state.kerberosRequested = true; try { - const spnConfig = configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal'); + const spnConfig = getExtHostConfigValue(configProvider, isRemote, 'http.proxyKerberosServicePrincipal'); const response = await lookupKerberosAuthorization(proxyURL, spnConfig, extHostLogService, 'ProxyResolver#lookupProxyAuthorization'); return 'Negotiate ' + response; } catch (err) { extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); } - if (isRemote && useHostProxy) { + if (isRemote && fallbackToLocalKerberos) { extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`); const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL); if (auth) { @@ -405,3 +406,13 @@ function sendTelemetry(mainThreadTelemetry: MainThreadTelemetryShape, authentica extensionHostType: isRemote ? 'remote' : 'local', }); } + +function getExtHostConfigValue(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback: T): T; +function getExtHostConfigValue(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string): T | undefined; +function getExtHostConfigValue(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback?: T): T | undefined { + if (isRemote) { + return configProvider.getConfiguration().get(key) ?? fallback; + } + const values: ConfigurationInspect | undefined = configProvider.getConfiguration().inspect(key); + return values?.globalLocalValue ?? values?.defaultValue ?? fallback; +} diff --git a/code/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/code/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index 00c9af61938..4bc30da2455 100644 --- a/code/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { ExtHostWorkspace } from '../../common/extHostWorkspace.js'; -import { ExtHostConfigProvider } from '../../common/extHostConfiguration.js'; +import { ConfigurationInspect, ExtHostConfigProvider } from '../../common/extHostConfiguration.js'; import { MainThreadConfigurationShape, IConfigurationInitData } from '../../common/extHost.protocol.js'; import { ConfigurationModel, ConfigurationModelParser } from '../../../../platform/configuration/common/configurationModels.js'; import { TestRPCProtocol } from '../common/testRPCProtocol.js'; @@ -47,7 +47,8 @@ suite('ExtHostConfiguration', function () { defaults: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), + userLocal: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace: ConfigurationModel.createEmptyModel(new NullLogService()), folders: [], configurationScopes: [] @@ -282,16 +283,29 @@ suite('ExtHostConfiguration', function () { { defaults: new ConfigurationModel({ 'editor': { - 'wordWrap': 'off' + 'wordWrap': 'off', + 'lineNumbers': 'on', + 'fontSize': '12px' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { - 'wordWrap': 'on' + 'wordWrap': 'on', + 'lineNumbers': 'off' } - }, ['editor.wordWrap'], [], undefined, new NullLogService()), + }, ['editor.wordWrap', 'editor.lineNumbers'], [], undefined, new NullLogService()), + userRemote: new ConfigurationModel({ + 'editor': { + 'lineNumbers': 'relative' + } + }, ['editor.lineNumbers'], [], { + 'editor': { + 'lineNumbers': 'relative', + 'fontSize': '14px' + } + }, new NullLogService()), workspace: new ConfigurationModel({}, [], [], undefined, new NullLogService()), folders: [], configurationScopes: [] @@ -299,17 +313,39 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); + assert.strictEqual(actual.globalLocalValue, 'on'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.globalValue, 'on'); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); actual = testObject.getConfiguration('editor').inspect('wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); + assert.strictEqual(actual.globalLocalValue, 'on'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.globalValue, 'on'); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); + + actual = testObject.getConfiguration('editor').inspect('lineNumbers')!; + assert.strictEqual(actual.defaultValue, 'on'); + assert.strictEqual(actual.globalLocalValue, 'off'); + assert.strictEqual(actual.globalRemoteValue, 'relative'); + assert.strictEqual(actual.globalValue, 'relative'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + + assert.strictEqual(testObject.getConfiguration('editor').get('fontSize'), '12px'); + + actual = testObject.getConfiguration('editor').inspect('fontSize')!; + assert.strictEqual(actual.defaultValue, '12px'); + assert.strictEqual(actual.globalLocalValue, undefined); + assert.strictEqual(actual.globalRemoteValue, '14px'); + assert.strictEqual(actual.globalValue, undefined); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); }); test('inspect in single root context', function () { @@ -338,11 +374,12 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap'], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace, folders, configurationScopes: [] @@ -350,26 +387,34 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual1 = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual1: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.globalValue, 'on'); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); actual1 = testObject.getConfiguration('editor').inspect('wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.globalValue, 'on'); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); - let actual2 = testObject.getConfiguration(undefined, workspaceUri).inspect('editor.wordWrap')!; + let actual2: ConfigurationInspect = testObject.getConfiguration(undefined, workspaceUri).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.globalValue, 'on'); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'bounded'); actual2 = testObject.getConfiguration('editor', workspaceUri).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.globalValue, 'on'); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'bounded'); @@ -417,11 +462,12 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap'], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace, folders, configurationScopes: [] @@ -429,57 +475,75 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual1 = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual1: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); assert.strictEqual(actual1.globalValue, 'on'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); actual1 = testObject.getConfiguration('editor').inspect('wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); assert.strictEqual(actual1.globalValue, 'on'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); actual1 = testObject.getConfiguration('editor').inspect('lineNumbers')!; assert.strictEqual(actual1.defaultValue, 'on'); assert.strictEqual(actual1.globalValue, undefined); + assert.strictEqual(actual1.globalLocalValue, undefined); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.workspaceValue, undefined); assert.strictEqual(actual1.workspaceFolderValue, undefined); - let actual2 = testObject.getConfiguration(undefined, firstRoot).inspect('editor.wordWrap')!; + let actual2: ConfigurationInspect = testObject.getConfiguration(undefined, firstRoot).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'off'); actual2 = testObject.getConfiguration('editor', firstRoot).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'off'); actual2 = testObject.getConfiguration('editor', firstRoot).inspect('lineNumbers')!; assert.strictEqual(actual2.defaultValue, 'on'); assert.strictEqual(actual2.globalValue, undefined); + assert.strictEqual(actual2.globalLocalValue, undefined); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, undefined); assert.strictEqual(actual2.workspaceFolderValue, 'relative'); actual2 = testObject.getConfiguration(undefined, secondRoot).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'on'); actual2 = testObject.getConfiguration('editor', secondRoot).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'on'); actual2 = testObject.getConfiguration(undefined, thirdRoot).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.ok(Object.keys(actual2).indexOf('workspaceFolderValue') !== -1); assert.strictEqual(actual2.workspaceFolderValue, undefined); @@ -487,6 +551,8 @@ suite('ExtHostConfiguration', function () { actual2 = testObject.getConfiguration('editor', thirdRoot).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.ok(Object.keys(actual2).indexOf('workspaceFolderValue') !== -1); assert.strictEqual(actual2.workspaceFolderValue, undefined); @@ -522,12 +588,13 @@ suite('ExtHostConfiguration', function () { }), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: toConfigurationModel({ + userLocal: toConfigurationModel({ 'editor.wordWrap': 'bounded', '[typescript]': { 'editor.lineNumbers': 'off', } }), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace: toConfigurationModel({ '[typescript]': { 'editor.wordWrap': 'unbounded', @@ -540,9 +607,11 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual = testObject.getConfiguration(undefined, { uri: firstRoot, languageId: 'typescript' }).inspect('editor.wordWrap')!; + let actual: ConfigurationInspect = testObject.getConfiguration(undefined, { uri: firstRoot, languageId: 'typescript' }).inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); assert.strictEqual(actual.globalValue, 'bounded'); + assert.strictEqual(actual.globalLocalValue, 'bounded'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, 'bounded'); assert.strictEqual(actual.defaultLanguageValue, undefined); @@ -554,6 +623,8 @@ suite('ExtHostConfiguration', function () { actual = testObject.getConfiguration(undefined, { uri: secondRoot, languageId: 'typescript' }).inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); assert.strictEqual(actual.globalValue, 'bounded'); + assert.strictEqual(actual.globalLocalValue, 'bounded'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(actual.defaultLanguageValue, undefined); @@ -582,12 +653,13 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'on' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { 'wordWrap': 'auto', 'lineNumbers': 'off' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace: new ConfigurationModel({}, [], [], undefined, new NullLogService()), folders: [], configurationScopes: [] @@ -595,9 +667,11 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); assert.strictEqual(actual.globalValue, 'auto'); + assert.strictEqual(actual.globalLocalValue, 'auto'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(testObject.getConfiguration().get('editor.wordWrap'), 'auto'); @@ -605,12 +679,16 @@ suite('ExtHostConfiguration', function () { actual = testObject.getConfiguration().inspect('editor.lineNumbers')!; assert.strictEqual(actual.defaultValue, 'on'); assert.strictEqual(actual.globalValue, 'off'); + assert.strictEqual(actual.globalLocalValue, 'off'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(testObject.getConfiguration().get('editor.lineNumbers'), 'off'); actual = testObject.getConfiguration().inspect('editor.fontSize')!; assert.strictEqual(actual.defaultValue, '12px'); + assert.strictEqual(actual.globalLocalValue, undefined); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.globalValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); diff --git a/code/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/code/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 476023a4712..a3660ca12b2 100644 --- a/code/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -286,11 +286,11 @@ suite('ExtHostTelemetry', function () { const logger = createLogger(functionSpy, extensionTelemetry); // Ensure headers are logged on instantiation - assert.strictEqual(loggerService.createLogger().logs.length, 2); + assert.strictEqual(loggerService.createLogger().logs.length, 0); logger.logUsage('test-event', { 'test-data': 'test-data' }); // Initial header is logged then the event - assert.strictEqual(loggerService.createLogger().logs.length, 3); - assert.ok(loggerService.createLogger().logs[2].startsWith('test-extension/test-event')); + assert.strictEqual(loggerService.createLogger().logs.length, 1); + assert.ok(loggerService.createLogger().logs[0].startsWith('test-extension/test-event')); }); }); diff --git a/code/src/vs/workbench/browser/actions/layoutActions.ts b/code/src/vs/workbench/browser/actions/layoutActions.ts index aed2fe31f74..d8217f384ab 100644 --- a/code/src/vs/workbench/browser/actions/layoutActions.ts +++ b/code/src/vs/workbench/browser/actions/layoutActions.ts @@ -32,6 +32,7 @@ import { mainWindow } from '../../../base/browser/window.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; import { TitlebarStyle } from '../../../platform/window/common/window.js'; import { IPreferencesService } from '../../services/preferences/common/preferences.js'; +import { QuickInputAlignmentContextKey } from '../../../platform/quickinput/browser/quickInput.js'; // Register Icons const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('menuBarIcon', "Represents the menu bar")); @@ -49,6 +50,9 @@ const panelAlignmentRightIcon = registerIcon('panel-align-right', Codicon.layout const panelAlignmentCenterIcon = registerIcon('panel-align-center', Codicon.layoutPanelCenter, localize('panelBottomCenter', "Represents the bottom panel alignment set to the center")); const panelAlignmentJustifyIcon = registerIcon('panel-align-justify', Codicon.layoutPanelJustify, localize('panelBottomJustify', "Represents the bottom panel alignment set to justified")); +const quickInputAlignmentTopIcon = registerIcon('quickInputAlignmentTop', Codicon.arrowUp, localize('quickInputAlignmentTop', "Represents quick input alignment set to the top")); +const quickInputAlignmentCenterIcon = registerIcon('quickInputAlignmentCenter', Codicon.circle, localize('quickInputAlignmentCenter', "Represents quick input alignment set to the center")); + const fullscreenIcon = registerIcon('fullscreen', Codicon.screenFull, localize('fullScreenIcon', "Represents full screen")); const centerLayoutIcon = registerIcon('centerLayoutIcon', Codicon.layoutCentered, localize('centerLayoutIcon', "Represents centered layout mode")); const zenModeIcon = registerIcon('zenMode', Codicon.target, localize('zenModeIcon', "Represents zen mode")); @@ -1284,6 +1288,42 @@ registerAction2(DecreaseViewSizeAction); registerAction2(DecreaseViewWidthAction); registerAction2(DecreaseViewHeightAction); +//#region Quick Input Alignment Actions + +registerAction2(class AlignQuickInputTopAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.alignQuickInputTop', + title: localize2('alignQuickInputTop', 'Align Quick Input Top'), + f1: false + }); + } + + run(accessor: ServicesAccessor): void { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.setAlignment('top'); + } +}); + +registerAction2(class AlignQuickInputCenterAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.alignQuickInputCenter', + title: localize2('alignQuickInputCenter', 'Align Quick Input Center'), + f1: false + }); + } + + run(accessor: ServicesAccessor): void { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.setAlignment('center'); + } +}); + +//#endregion + type ContextualLayoutVisualIcon = { iconA: ThemeIcon; iconB: ThemeIcon; whenA: ContextKeyExpression }; type LayoutVisualIcon = ThemeIcon | ContextualLayoutVisualIcon; @@ -1355,6 +1395,11 @@ const AlignPanelActions: CustomizeLayoutItem[] = [ CreateOptionLayoutItem('workbench.action.alignPanelJustify', PanelAlignmentContext.isEqualTo('justify'), localize('justifyPanel', "Justify"), panelAlignmentJustifyIcon), ]; +const QuickInputActions: CustomizeLayoutItem[] = [ + CreateOptionLayoutItem('workbench.action.alignQuickInputTop', QuickInputAlignmentContextKey.isEqualTo('top'), localize('top', "Top"), quickInputAlignmentTopIcon), + CreateOptionLayoutItem('workbench.action.alignQuickInputCenter', QuickInputAlignmentContextKey.isEqualTo('center'), localize('center', "Center"), quickInputAlignmentCenterIcon), +]; + const MiscLayoutOptions: CustomizeLayoutItem[] = [ CreateOptionLayoutItem('workbench.action.toggleFullScreen', IsMainWindowFullscreenContext, localize('fullscreen', "Full Screen"), fullscreenIcon), CreateOptionLayoutItem('workbench.action.toggleZenMode', InEditorZenModeContext, localize('zenMode', "Zen Mode"), zenModeIcon), @@ -1362,7 +1407,7 @@ const MiscLayoutOptions: CustomizeLayoutItem[] = [ ]; const LayoutContextKeySet = new Set(); -for (const { active } of [...ToggleVisibilityActions, ...MoveSideBarActions, ...AlignPanelActions, ...MiscLayoutOptions]) { +for (const { active } of [...ToggleVisibilityActions, ...MoveSideBarActions, ...AlignPanelActions, ...QuickInputActions, ...MiscLayoutOptions]) { for (const key of active.keys()) { LayoutContextKeySet.add(key); } @@ -1444,6 +1489,11 @@ registerAction2(class CustomizeLayoutAction extends Action2 { label: localize('panelAlignment', "Panel Alignment") }, ...AlignPanelActions.map(toQuickPickItem), + { + type: 'separator', + label: localize('quickOpen', "Quick Input Position") + }, + ...QuickInputActions.map(toQuickPickItem), { type: 'separator', label: localize('layoutModes', "Modes"), diff --git a/code/src/vs/workbench/browser/layout.ts b/code/src/vs/workbench/browser/layout.ts index bf1f54199d1..1d362b33fb5 100644 --- a/code/src/vs/workbench/browser/layout.ts +++ b/code/src/vs/workbench/browser/layout.ts @@ -620,8 +620,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService): void { - this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, this.parent); - this.stateModel.load(); + this._mainContainerDimension = getClientArea(this.parent); + + this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService); + this.stateModel.load(this._mainContainerDimension); // Both editor and panel should not be hidden on startup if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) && this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN)) { @@ -724,9 +726,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - // Auxiliary Panel to restore + // Auxiliary View to restore if (this.isVisible(Parts.AUXILIARYBAR_PART)) { - const viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); + const viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activeViewSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); if (viewContainerToRestore) { this.state.initialization.views.containerToRestore.auxiliaryBar = viewContainerToRestore; } else { @@ -1074,7 +1076,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore Auxiliary Bar layoutReadyPromises.push((async () => { - // Restoring views could mean that panel already + // Restoring views could mean that auxbar already // restored, as such we need to test again await restoreDefaultViewsPromise; if (!this.state.initialization.views.containerToRestore.auxiliaryBar) { @@ -1083,9 +1085,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi mark('code/willRestoreAuxiliaryBar'); - const panel = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); - if (!panel) { - await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar); // fallback to default panel as needed + const viewlet = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); + if (!viewlet) { + await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar); // fallback to default viewlet as needed } mark('code/didRestoreAuxiliaryBar'); @@ -1932,7 +1934,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (panelToOpen) { const focus = !skipLayout; - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); + const panel = this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); + if (!panel) { + this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id, ViewContainerLocation.Panel, focus); + } } } @@ -2020,19 +2025,22 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // If auxiliary bar becomes visible, show last active pane composite or default pane composite else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { - let panelToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); + let viewletToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); - // verify that the panel we try to open has views before we default to it + // verify that the viewlet we try to open has views before we default to it // otherwise fall back to any view that has views still refs #111463 - if (!panelToOpen || !this.hasViews(panelToOpen)) { - panelToOpen = this.viewDescriptorService + if (!viewletToOpen || !this.hasViews(viewletToOpen)) { + viewletToOpen = this.viewDescriptorService .getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar) .find(viewContainer => this.hasViews(viewContainer.id))?.id; } - if (panelToOpen) { + if (viewletToOpen) { const focus = !skipLayout; - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.AuxiliaryBar, focus); + const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.AuxiliaryBar, focus); + if (!viewlet) { + this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar, focus); + } } } @@ -2367,7 +2375,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private createGridDescriptor(): ISerializedGrid { - const { width, height } = this.stateModel.getInitializationValue(LayoutStateKeys.GRID_SIZE); + const { width, height } = this._mainContainerDimension!; const sideBarSize = this.stateModel.getInitializationValue(LayoutStateKeys.SIDEBAR_SIZE); const auxiliaryBarPartSize = this.stateModel.getInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE); const panelSize = this.stateModel.getInitializationValue(LayoutStateKeys.PANEL_SIZE); @@ -2573,7 +2581,6 @@ const LayoutStateKeys = { }), // Part Sizing - GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }), SIDEBAR_SIZE: new InitializationStateKey('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), PANEL_SIZE: new InitializationStateKey('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), @@ -2626,8 +2633,7 @@ class LayoutStateModel extends Disposable { constructor( private readonly storageService: IStorageService, private readonly configurationService: IConfigurationService, - private readonly contextService: IWorkspaceContextService, - private readonly container: HTMLElement + private readonly contextService: IWorkspaceContextService ) { super(); @@ -2663,7 +2669,7 @@ class LayoutStateModel extends Disposable { } } - load(): void { + load(mainContainerDimension: IDimension): void { let key: keyof typeof LayoutStateKeys; // Load stored values for all keys @@ -2682,12 +2688,10 @@ class LayoutStateModel extends Disposable { this.stateCache.set(LayoutStateKeys.SIDEBAR_POSITON.name, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); // Set dynamic defaults: part sizing and side bar visibility - const workbenchDimensions = getClientArea(this.container); LayoutStateKeys.PANEL_POSITION.defaultValue = positionFromString(this.configurationService.getValue(WorkbenchLayoutSettings.PANEL_POSITION) ?? 'bottom'); - LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; - LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); + LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? mainContainerDimension.height / 3 : mainContainerDimension.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults diff --git a/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 71337cabefe..9250304af19 100644 --- a/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -39,8 +39,8 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { - static readonly activePanelSettingsKey = 'workbench.auxiliarybar.activepanelid'; - static readonly pinnedPanelsKey = 'workbench.auxiliarybar.pinnedPanels'; + static readonly activeViewSettingsKey = 'workbench.auxiliarybar.activepanelid'; + static readonly pinnedViewsKey = 'workbench.auxiliarybar.pinnedPanels'; static readonly placeholdeViewContainersKey = 'workbench.auxiliarybar.placeholderPanels'; static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState'; @@ -95,7 +95,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { hasTitle: true, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0, }, - AuxiliaryBarPart.activePanelSettingsKey, + AuxiliaryBarPart.activeViewSettingsKey, ActiveAuxiliaryContext.bindTo(contextKeyService), AuxiliaryBarFocusContext.bindTo(contextKeyService), 'auxiliarybar', @@ -156,7 +156,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { const $this = this; return { partContainerClass: 'auxiliarybar', - pinnedViewContainersKey: AuxiliaryBarPart.pinnedPanelsKey, + pinnedViewContainersKey: AuxiliaryBarPart.pinnedViewsKey, placeholderViewContainersKey: AuxiliaryBarPart.placeholdeViewContainersKey, viewContainersWorkspaceStateKey: AuxiliaryBarPart.viewContainersWorkspaceStateKey, icon: true, diff --git a/code/src/vs/workbench/browser/parts/compositeBarActions.ts b/code/src/vs/workbench/browser/parts/compositeBarActions.ts index 726b76aeffd..136b41c9800 100644 --- a/code/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/code/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -514,26 +514,11 @@ export class CompositeOverflowActivityActionViewItem extends CompositeBarActionV } } -class ManageExtensionAction extends Action { - - constructor( - @ICommandService private readonly commandService: ICommandService - ) { - super('activitybar.manage.extension', localize('manageExtension', "Manage Extension")); - } - - override run(id: string): Promise { - return this.commandService.executeCommand('_extensions.manage', id); - } -} - export class CompositeActionViewItem extends CompositeBarActionViewItem { - private static manageExtensionAction: ManageExtensionAction; - constructor( options: ICompositeBarActionViewItemOptions, - private readonly compositeActivityAction: CompositeBarAction, + compositeActivityAction: CompositeBarAction, private readonly toggleCompositePinnedAction: IAction, private readonly toggleCompositeBadgeAction: IAction, private readonly compositeContextMenuActionsProvider: (compositeId: string) => IAction[], @@ -557,10 +542,6 @@ export class CompositeActionViewItem extends CompositeBarActionViewItem { configurationService, keybindingService ); - - if (!CompositeActionViewItem.manageExtensionAction) { - CompositeActionViewItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction); - } } override render(container: HTMLElement): void { @@ -669,11 +650,6 @@ export class CompositeActionViewItem extends CompositeBarActionViewItem { actions.push(...compositeContextMenuActions); } - if ((this.compositeActivityAction.compositeBarActionItem).extensionId) { - actions.push(new Separator()); - actions.push(CompositeActionViewItem.manageExtensionAction); - } - const isPinned = this.compositeBar.isPinned(this.compositeBarActionItem.id); if (isPinned) { this.toggleCompositePinnedAction.label = localize('hide', "Hide '{0}'", this.compositeBarActionItem.name); diff --git a/code/src/vs/workbench/browser/parts/editor/editorActions.ts b/code/src/vs/workbench/browser/parts/editor/editorActions.ts index 9d9766f4eb5..fc9b8ec03e0 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -1420,9 +1420,9 @@ export class NavigateForwardAction extends Action2 { precondition: ContextKeyExpr.has('canNavigateForward'), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - win: { primary: KeyMod.Alt | KeyCode.RightArrow }, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Minus }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus } + win: { primary: KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyCode.BrowserForward] }, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Minus, secondary: [KeyCode.BrowserForward] }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus, secondary: [KeyCode.BrowserForward] } }, menu: [ { id: MenuId.MenubarGoMenu, group: '1_history_nav', order: 2 }, @@ -1455,9 +1455,9 @@ export class NavigateBackwardsAction extends Action2 { icon: Codicon.arrowLeft, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, - mac: { primary: KeyMod.WinCtrl | KeyCode.Minus }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Minus } + win: { primary: KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyCode.BrowserBack] }, + mac: { primary: KeyMod.WinCtrl | KeyCode.Minus, secondary: [KeyCode.BrowserBack] }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Minus, secondary: [KeyCode.BrowserBack] } }, menu: [ { id: MenuId.MenubarGoMenu, group: '1_history_nav', order: 1 }, diff --git a/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 2b7d3ed86eb..1dff715f703 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -650,7 +650,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { /* __GDPR__ "editorOpened" : { - "owner": "bpasero", + "owner": "isidorn", "${include}": [ "${EditorTelemetryDescriptor}" ] @@ -687,7 +687,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { /* __GDPR__ "editorClosed" : { - "owner": "bpasero", + "owner": "isidorn", "${include}": [ "${EditorTelemetryDescriptor}" ] diff --git a/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 6c100075472..15d87f9ad15 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -5,7 +5,7 @@ import './media/editorplaceholder.css'; import { localize } from '../../../../nls.js'; -import { truncate, truncateMiddle } from '../../../../base/common/strings.js'; +import { truncate } from '../../../../base/common/strings.js'; import Severity from '../../../../base/common/severity.js'; import { IEditorOpenContext, isEditorOpenError } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -48,7 +48,7 @@ export interface IErrorEditorPlaceholderOptions extends IEditorOptions { export abstract class EditorPlaceholder extends EditorPane { - protected static readonly PLACEHOLDER_LABEL_MAX_LENGTH = 1024; + private static readonly PLACEHOLDER_LABEL_MAX_LENGTH = 1024; private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; @@ -248,7 +248,7 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { } else if (isEditorOpenError(error) && error.forceMessage) { label = error.message; } else if (error) { - label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", truncateMiddle(toErrorMessage(error), EditorPlaceholder.PLACEHOLDER_LABEL_MAX_LENGTH / 2)); + label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error. Please consult the log for more details."); } else { label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error."); } diff --git a/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index a9e4fd0ebb0..2025ec3ea7d 100644 --- a/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css @@ -6,15 +6,15 @@ .monaco-workbench > .notifications-center { position: absolute; z-index: 1000; - right: 8px; - bottom: 31px; + right: 11px; /* attempt to position at same location as a toast */ + bottom: 33px; /* 22px status bar height + 11px (attempt to position at same location as a toast) */ display: none; overflow: hidden; border-radius: 4px; } .monaco-workbench.nostatusbar > .notifications-center { - bottom: 8px; + bottom: 11px; /* attempt to position at same location as a toast */ } .monaco-workbench > .notifications-center.visible { diff --git a/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index d4ef2314bc5..f12d6b925df 100644 --- a/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -7,7 +7,7 @@ position: absolute; z-index: 1000; right: 3px; - bottom: 26px; + bottom: 25px; /* 22px status bar height + 3px */ display: none; overflow: hidden; } diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/code/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts index 0e206216e58..723c97a134c 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -8,7 +8,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IAccessibilitySignalService, AccessibilitySignal } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -17,7 +17,7 @@ import { getNotificationFromContext } from './notificationsCommands.js'; import { NotificationFocusedContext } from '../../../common/contextkeys.js'; import { INotificationViewItem } from '../../../common/notifications.js'; -export class NotificationAccessibleView implements IAccessibleViewImplentation { +export class NotificationAccessibleView implements IAccessibleViewImplementation { readonly priority = 90; readonly name = 'notifications'; readonly when = NotificationFocusedContext; diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index c80c3561c46..04980171865 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -12,12 +12,10 @@ import { MenuRegistry, MenuId } from '../../../../platform/actions/common/action import { localize, localize2 } from '../../../../nls.js'; import { IListService, WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { NotificationMetrics, NotificationMetricsClassification, notificationToMetrics } from './notificationsTelemetry.js'; import { NotificationFocusedContext, NotificationsCenterVisibleContext, NotificationsToastsVisibleContext } from '../../../common/contextkeys.js'; -import { INotificationService, INotificationSourceFilter, NotificationPriority, NotificationsFilter } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, INotificationSourceFilter, NotificationsFilter } from '../../../../platform/notification/common/notification.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ActionRunner, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js'; -import { hash } from '../../../../base/common/hash.js'; import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; @@ -109,16 +107,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl weight: KeybindingWeight.WorkbenchContrib + 50, when: NotificationsCenterVisibleContext, primary: KeyCode.Escape, - handler: accessor => { - const telemetryService = accessor.get(ITelemetryService); - for (const notification of model.notifications) { - if (notification.visible) { - telemetryService.publicLog2('notification:hide', notificationToMetrics(notification.message.original, notification.sourceId, notification.priority === NotificationPriority.SILENT)); - } - } - - center.hide(); - } + handler: () => center.hide() }); // Toggle Notifications Center @@ -210,12 +199,6 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Hide Toasts CommandsRegistry.registerCommand(HIDE_NOTIFICATION_TOAST, accessor => { - const telemetryService = accessor.get(ITelemetryService); - for (const notification of model.notifications) { - if (notification.visible) { - telemetryService.publicLog2('notification:hide', notificationToMetrics(notification.message.original, notification.sourceId, notification.priority === NotificationPriority.SILENT)); - } - } toasts.hide(); }); @@ -342,22 +325,6 @@ export function registerNotificationCommands(center: INotificationsCenterControl } -interface NotificationActionMetrics { - readonly id: string; - readonly actionLabel: string; - readonly source: string; - readonly silent: boolean; -} - -type NotificationActionMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run from a notification.' }; - actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was run from a notification.' }; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification where an action was run.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification where an action was run is silent or not.' }; - owner: 'bpasero'; - comment: 'Tracks when actions are fired from notifcations and how they were fired.'; -}; - export class NotificationActionRunner extends ActionRunner { constructor( @@ -370,17 +337,6 @@ export class NotificationActionRunner extends ActionRunner { protected override async runAction(action: IAction, context: unknown): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: action.id, from: 'message' }); - if (isNotificationViewItem(context)) { - // Log some additional telemetry specifically for actions - // that are triggered from within notifications. - this.telemetryService.publicLog2('notification:actionExecuted', { - id: hash(context.message.original.toString()).toString(), - actionLabel: action.label, - source: context.sourceId || 'core', - silent: context.priority === NotificationPriority.SILENT - }); - } - // Run and make sure to notify on any error again try { await super.runAction(action, context); diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts deleted file mode 100644 index fd7b3f3da3a..00000000000 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ /dev/null @@ -1,55 +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 '../../../../base/common/lifecycle.js'; -import { INotificationService, NotificationMessage, NotificationPriority } from '../../../../platform/notification/common/notification.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { hash } from '../../../../base/common/hash.js'; - -export interface NotificationMetrics { - readonly id: string; - readonly silent: boolean; - readonly source?: string; -} - -export type NotificationMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the source of the notification.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification is silent or not.' }; - source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification.' }; - owner: 'bpasero'; - comment: 'Helps us gain insights to what notifications are being shown, how many, and if they are silent or not.'; -}; - -export function notificationToMetrics(message: NotificationMessage, source: string | undefined, silent: boolean): NotificationMetrics { - return { - id: hash(message.toString()).toString(), - silent, - source: source || 'core' - }; -} - -export class NotificationsTelemetry extends Disposable implements IWorkbenchContribution { - - constructor( - @ITelemetryService private readonly telemetryService: ITelemetryService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(); - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.notificationService.onDidAddNotification(notification => { - const source = notification.source && typeof notification.source !== 'string' ? notification.source.id : notification.source; - this.telemetryService.publicLog2('notification:show', notificationToMetrics(notification.message, source, notification.priority === NotificationPriority.SILENT)); - })); - - this._register(this.notificationService.onDidRemoveNotification(notification => { - const source = notification.source && typeof notification.source !== 'string' ? notification.source.id : notification.source; - this.telemetryService.publicLog2('notification:close', notificationToMetrics(notification.message, source, notification.priority === NotificationPriority.SILENT)); - })); - } -} diff --git a/code/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts b/code/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts index 55117c9fee2..08794f0900d 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts +++ b/code/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts @@ -16,6 +16,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { StatusbarViewModel } from './statusbarModel.js'; import { StatusBarFocused } from '../../../common/contextkeys.js'; import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; export class ToggleStatusbarEntryVisibilityAction extends Action { @@ -45,6 +46,20 @@ export class HideStatusbarEntryAction extends Action { } } +export class ManageExtensionAction extends Action { + + constructor( + private readonly extensionId: string, + @ICommandService private readonly commandService: ICommandService + ) { + super('statusbar.manage.extension', localize('manageExtension', "Manage Extension")); + } + + override run(): Promise { + return this.commandService.executeCommand('_extensions.manage', this.extensionId); + } +} + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.statusBar.focusPrevious', weight: KeybindingWeight.WorkbenchContrib, diff --git a/code/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/code/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 30b01a7a4bb..82db371b426 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/code/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -11,6 +11,7 @@ import { Emitter } from '../../../../base/common/event.js'; export interface IStatusbarViewModelEntry { readonly id: string; + readonly extensionId: string | undefined; readonly name: string; readonly hasCommand: boolean; readonly alignment: StatusbarAlignment; diff --git a/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index efc5e92e03f..6aee246ef2c 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -29,7 +29,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { hash } from '../../../../base/common/hash.js'; import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js'; -import { HideStatusbarEntryAction, ToggleStatusbarEntryVisibilityAction } from './statusbarActions.js'; +import { HideStatusbarEntryAction, ManageExtensionAction, ToggleStatusbarEntryVisibilityAction } from './statusbarActions.js'; import { IStatusbarViewModelEntry, StatusbarViewModel } from './statusbarModel.js'; import { StatusbarEntryItem } from './statusbarItem.js'; import { StatusBarFocused } from '../../../common/contextkeys.js'; @@ -259,6 +259,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { // View model entry const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry { readonly id = id; + readonly extensionId = entry.extensionId; readonly alignment = alignment; readonly priority = priority; readonly container = itemContainer; @@ -600,6 +601,9 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { if (statusEntryUnderMouse) { actions.push(new Separator()); + if (statusEntryUnderMouse.extensionId) { + actions.push(this.instantiationService.createInstance(ManageExtensionAction, statusEntryUnderMouse.extensionId)); + } actions.push(new HideStatusbarEntryAction(statusEntryUnderMouse.id, statusEntryUnderMouse.name, this.viewModel)); } diff --git a/code/src/vs/workbench/browser/parts/views/viewFilter.ts b/code/src/vs/workbench/browser/parts/views/viewFilter.ts index 9eae6e9bc5f..ee6f952a249 100644 --- a/code/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/code/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -95,7 +95,7 @@ export class FilterWidget extends Widget { @IKeybindingService private readonly keybindingService: IKeybindingService ) { super(); - this.delayedFilterUpdate = new Delayer(400); + this.delayedFilterUpdate = new Delayer(300); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); if (options.focusContextKey) { diff --git a/code/src/vs/workbench/browser/web.main.ts b/code/src/vs/workbench/browser/web.main.ts index 142cfa90cd1..5dcdad8aaac 100644 --- a/code/src/vs/workbench/browser/web.main.ts +++ b/code/src/vs/workbench/browser/web.main.ts @@ -40,7 +40,7 @@ import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/ import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from '../services/workspaces/browser/workspaces.js'; import { InMemoryFileSystemProvider } from '../../platform/files/common/inMemoryFilesystemProvider.js'; import { ICommandService } from '../../platform/commands/common/commands.js'; -import { IndexedDBFileSystemProviderErrorDataClassification, IndexedDBFileSystemProvider, IndexedDBFileSystemProviderErrorData } from '../../platform/files/browser/indexedDBFileSystemProvider.js'; +import { IndexedDBFileSystemProvider } from '../../platform/files/browser/indexedDBFileSystemProvider.js'; import { BrowserRequestService } from '../services/request/browser/requestService.js'; import { IRequestService } from '../../platform/request/common/request.js'; import { IUserDataInitializationService, IUserDataInitializer, UserDataInitializationService } from '../services/userData/browser/userDataInit.js'; @@ -64,7 +64,6 @@ import { IOpenerService } from '../../platform/opener/common/opener.js'; import { mixin, safeStringify } from '../../base/common/objects.js'; import { IndexedDB } from '../../base/browser/indexedDB.js'; import { WebFileSystemAccess } from '../../platform/files/browser/webFileSystemAccess.js'; -import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; import { IProgressService } from '../../platform/progress/common/progress.js'; import { DelayedLogChannel } from '../services/output/common/delayedLogChannel.js'; import { dirname, joinPath } from '../../base/common/resources.js'; @@ -77,7 +76,7 @@ import { UserDataProfileService } from '../services/userDataProfile/common/userD import { IUserDataProfileService } from '../services/userDataProfile/common/userDataProfile.js'; import { BrowserUserDataProfilesService } from '../../platform/userDataProfile/browser/userDataProfile.js'; import { DeferredPromise, timeout } from '../../base/common/async.js'; -import { windowLogId } from '../services/log/common/logConstants.js'; +import { windowLogGroup, windowLogId } from '../services/log/common/logConstants.js'; import { LogService } from '../../platform/log/common/logService.js'; import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from '../../platform/remote/common/remoteSocketFactoryService.js'; import { BrowserSocketFactory } from '../../platform/remote/browser/browserSocketFactory.js'; @@ -137,13 +136,6 @@ export class BrowserMain extends Disposable { // Logging services.logService.trace('workbench#open with configuration', safeStringify(this.configuration)); - instantiationService.invokeFunction(accessor => { - const telemetryService = accessor.get(ITelemetryService); - for (const indexedDbFileSystemProvider of this.indexedDBFileSystemProviders) { - this._register(indexedDbFileSystemProvider.onReportError(e => telemetryService.publicLog2('indexedDBFileSystemProviderError', e))); - } - }); - // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); @@ -298,7 +290,7 @@ export class BrowserMain extends Disposable { if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { otherLoggers.push(new ConsoleLogInAutomationLogger(loggerService.getLogLevel())); } - const logger = loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: localize('rendererLog', "Window") }); + const logger = loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: windowLogGroup.name, group: windowLogGroup }); const logService = new LogService(logger, otherLoggers); serviceCollection.set(ILogService, logService); @@ -397,7 +389,7 @@ export class BrowserMain extends Disposable { this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()))); // Request Service - const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); + const requestService = new BrowserRequestService(remoteAgentService, configurationService, loggerService); serviceCollection.set(IRequestService, requestService); // Userdata Sync Store Management Service diff --git a/code/src/vs/workbench/browser/workbench.ts b/code/src/vs/workbench/browser/workbench.ts index b40c9fa73a9..fd795d5ad61 100644 --- a/code/src/vs/workbench/browser/workbench.ts +++ b/code/src/vs/workbench/browser/workbench.ts @@ -26,7 +26,6 @@ import { NotificationService } from '../services/notification/common/notificatio import { NotificationsCenter } from './parts/notifications/notificationsCenter.js'; import { NotificationsAlerts } from './parts/notifications/notificationsAlerts.js'; import { NotificationsStatus } from './parts/notifications/notificationsStatus.js'; -import { NotificationsTelemetry } from './parts/notifications/notificationsTelemetry.js'; import { registerNotificationCommands } from './parts/notifications/notificationsCommands.js'; import { NotificationsToasts } from './parts/notifications/notificationsToasts.js'; import { setARIAContainer } from '../../base/browser/ui/aria/aria.js'; @@ -376,7 +375,6 @@ export class Workbench extends Layout { const notificationsToasts = this._register(instantiationService.createInstance(NotificationsToasts, this.mainContainer, notificationService.model)); this._register(instantiationService.createInstance(NotificationsAlerts, notificationService.model)); const notificationsStatus = instantiationService.createInstance(NotificationsStatus, notificationService.model); - this._register(instantiationService.createInstance(NotificationsTelemetry)); // Visibility this._register(notificationsCenter.onDidChangeVisibility(() => { diff --git a/code/src/vs/workbench/common/configuration.ts b/code/src/vs/workbench/common/configuration.ts index 9fb1e03a4ef..46f42503e1c 100644 --- a/code/src/vs/workbench/common/configuration.ts +++ b/code/src/vs/workbench/common/configuration.ts @@ -221,13 +221,13 @@ export class DynamicWorkbenchSecurityConfiguration extends Disposable implements }, 'default': [], 'markdownDescription': localize('security.allowedUNCHosts', 'A set of UNC host names (without leading or trailing backslash, for example `192.168.0.1` or `my-server`) to allow without user confirmation. If a UNC host is being accessed that is not allowed via this setting or has not been acknowledged via user confirmation, an error will occur and the operation stopped. A restart is required when changing this setting. Find out more about this setting at https://aka.ms/vscode-windows-unc.'), - 'scope': ConfigurationScope.MACHINE + 'scope': ConfigurationScope.APPLICATION_MACHINE }, 'security.restrictUNCAccess': { 'type': 'boolean', 'default': true, 'markdownDescription': localize('security.restrictUNCAccess', 'If enabled, only allows access to UNC host names that are allowed by the `#security.allowedUNCHosts#` setting or after user confirmation. Find out more about this setting at https://aka.ms/vscode-windows-unc.'), - 'scope': ConfigurationScope.MACHINE + 'scope': ConfigurationScope.APPLICATION_MACHINE } } }); diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index fc060c2832e..3edbf376c5a 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -63,6 +63,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditorActive = 'accessibility.verbosity.diffEditorActive', Debug = 'accessibility.verbosity.debug', Walkthrough = 'accessibility.verbosity.walkthrough', + SourceControl = 'accessibility.verbosity.scm' } const baseVerbosityProperty: IConfigurationPropertySchema = { diff --git a/code/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/code/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 237b768fbd4..ce82d4983b7 100644 --- a/code/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/code/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -255,15 +255,19 @@ class DeleteOperation implements IFileOperation { // read file contents for undo operation. when a file is too large it won't be restored let fileContent: IFileContent | undefined; - if (!edit.undoesCreate && !edit.options.folder && !(typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize)) { - try { - fileContent = await this._fileService.readFile(edit.oldUri); - } catch (err) { - this._logService.error(err); + let fileContentExceedsMaxSize = false; + if (!edit.undoesCreate && !edit.options.folder) { + fileContentExceedsMaxSize = typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize; + if (!fileContentExceedsMaxSize) { + try { + fileContent = await this._fileService.readFile(edit.oldUri); + } catch (err) { + this._logService.error(err); + } } } - if (fileContent !== undefined) { - undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent.value)); + if (!fileContentExceedsMaxSize) { + undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 09805452279..5ed43c1ba43 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -9,7 +9,7 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { AccessibleDiffViewerNext } from '../../../../../editor/browser/widget/diffEditor/commands.js'; import { localize } from '../../../../../nls.js'; import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { ActiveAuxiliaryContext } from '../../../../common/contextkeys.js'; @@ -19,7 +19,7 @@ import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatWidgetService } from '../chat.js'; -export class PanelChatAccessibilityHelp implements IAccessibleViewImplentation { +export class PanelChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 107; readonly name = 'panelChat'; readonly type = AccessibleViewType.Help; @@ -30,7 +30,7 @@ export class PanelChatAccessibilityHelp implements IAccessibleViewImplentation { } } -export class QuickChatAccessibilityHelp implements IAccessibleViewImplentation { +export class QuickChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 107; readonly name = 'quickChat'; readonly type = AccessibleViewType.Help; @@ -41,7 +41,7 @@ export class QuickChatAccessibilityHelp implements IAccessibleViewImplentation { } } -export class EditsChatAccessibilityHelp implements IAccessibleViewImplentation { +export class EditsChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 119; readonly name = 'editsView'; readonly type = AccessibleViewType.Help; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 4b1b916b751..d7532a3514b 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -92,10 +92,6 @@ class OpenChatGlobalAction extends Action2 { title: OpenChatGlobalAction.TITLE, icon: Codicon.copilot, f1: true, - precondition: ContextKeyExpr.or( - ChatContextKeys.Setup.installed, - ChatContextKeys.panelParticipantRegistered - ), category: CHAT_CATEGORY, keybinding: { weight: KeybindingWeight.WorkbenchContrib, @@ -339,7 +335,7 @@ class ChatAddAction extends Action2 { MenuRegistry.appendMenuItem(MenuId.ViewTitle, { command: { id: 'update.showCurrentReleaseNotes', - title: localize2('chat.releaseNotes.label', "Explore New Features"), + title: localize2('chat.releaseNotes.label', "Show Release Notes"), }, when: ContextKeyExpr.equals('view', ChatViewId) }); @@ -489,7 +485,7 @@ export function registerChatActions() { }); } - const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals('config.github.copilot.advanced.authProvider', 'github-enterprise')); + const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.providerSetting}`, defaultChat.enterpriseProviderId)); registerOpenLinkAction('workbench.action.chat.managePlan', localize2('managePlan', "Manage Copilot Plan"), defaultChat.managePlanUrl, 1, nonEnterpriseCopilotUsers); registerOpenLinkAction('workbench.action.chat.manageSettings', localize2('manageSettings', "Manage Copilot Settings"), defaultChat.manageSettingsUrl, 2, nonEnterpriseCopilotUsers); registerOpenLinkAction('workbench.action.chat.learnMore', localize2('learnMore', "Learn More"), defaultChat.documentationUrl, 3); @@ -527,6 +523,8 @@ const defaultChat = { documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '', + enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + providerSetting: product.defaultChatAgent?.providerSetting ?? '', }; MenuRegistry.appendMenuItem(MenuId.CommandCenter, { diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 07f9ce1cc93..87d33948281 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -19,14 +19,15 @@ import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatViewId, EditsViewId, IChatWidgetService } from '../chat.js'; +import { ctxIsGlobalEditingSession } from '../chatEditorController.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`; - export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`; +export const ChatDoneActionId = 'workbench.action.chat.done'; export function registerNewChatActions() { registerAction2(class NewChatEditorAction extends Action2 { @@ -210,7 +211,7 @@ export function registerNewChatActions() { registerAction2(class GlobalEditsDoneAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.done', + id: ChatDoneActionId, title: localize2('chat.done.label', "Done"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered), @@ -315,7 +316,6 @@ export function registerNewChatActions() { title: localize2('chat.openEdits.label', "Open {0}", 'Copilot Edits'), category: CHAT_CATEGORY, icon: Codicon.goToEditingSession, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered), f1: true, menu: [{ id: MenuId.ViewTitle, @@ -333,6 +333,7 @@ export function registerNewChatActions() { order: 2 }, { id: MenuId.ChatEditingEditorContent, + when: ctxIsGlobalEditingSession, group: 'navigate', order: 4, }], diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 248bbd7da53..2be07929199 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -7,18 +7,23 @@ import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { Disposable, markAsSingleton } from '../../../../../base/common/lifecycle.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; +import { MenuEntryActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; @@ -81,6 +86,44 @@ abstract class ChatCodeBlockAction extends Action2 { abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any; } +const APPLY_IN_EDITOR_ID = 'workbench.action.chat.applyInEditor'; + +export class CodeBlockActionRendering extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.codeBlockActionRendering'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + ) { + super(); + + const disposable = actionViewItemService.register(MenuId.ChatCodeBlock, APPLY_IN_EDITOR_ID, (action, options) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + return instantiationService.createInstance(class extends MenuEntryActionViewItem { + protected override getTooltip(): string { + const context = this._context; + if (isCodeBlockActionContext(context) && context.codemapperUri) { + const label = labelService.getUriLabel(context.codemapperUri, { relative: true }); + return localize('interactive.applyInEditorWithURL.label', "Apply in {0}", label); + } + return super.getTooltip(); + } + override setActionContext(newContext: unknown): void { + super.setActionContext(newContext); + this.updateTooltip(); + } + }, action, undefined); + }); + + // Reduces flicker a bit on reload/restart + markAsSingleton(disposable); + } +} + export function registerChatCodeBlockActions() { registerAction2(class CopyCodeBlockAction extends Action2 { constructor() { @@ -187,7 +230,7 @@ export function registerChatCodeBlockActions() { constructor() { super({ - id: 'workbench.action.chat.applyInEditor', + id: APPLY_IN_EDITOR_ID, title: localize2('interactive.applyInEditor.label', "Apply in Editor"), precondition: ChatContextKeys.enabled, f1: true, @@ -227,7 +270,7 @@ export function registerChatCodeBlockActions() { } }); - registerAction2(class SmartApplyInEditorAction extends ChatCodeBlockAction { + registerAction2(class InsertAtCursorAction extends ChatCodeBlockAction { constructor() { super({ id: 'workbench.action.chat.insertCodeBlock', @@ -558,7 +601,7 @@ export function registerChatCodeCompareBlockActions() { const inlineChatController = InlineChatController.get(editorToApply); if (inlineChatController) { editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - inlineChatController.reviewEdits(firstEdit.range, textEdits, CancellationToken.None); + inlineChatController.reviewEdits(textEdits, CancellationToken.None); response.setEditApplied(item, 1); return true; } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index c812ec0d053..e293e2b5c15 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -14,7 +14,6 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { EditorType } from '../../../../../editor/common/editorCommon.js'; import { Command } from '../../../../../editor/common/languages.js'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; import { localize, localize2 } from '../../../../../nls.js'; @@ -22,11 +21,14 @@ import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../. import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js'; +import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; @@ -35,6 +37,7 @@ import { VIEW_ID as SEARCH_VIEW_ID } from '../../../../services/search/common/se import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js'; +import { TEXT_FILE_EDITOR_ID } from '../../../files/common/files.js'; import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { AnythingQuickAccessProvider } from '../../../search/browser/anythingQuickAccess.js'; import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browser/searchTreeModel/searchTreeCommon.js'; @@ -48,10 +51,12 @@ import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariableData, IChatVariablesService } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; +import { resizeImage } from '../imageUtils.js'; import { CHAT_CATEGORY } from './chatActions.js'; export function registerChatContextActions() { @@ -65,7 +70,9 @@ export function registerChatContextActions() { /** * We fill the quickpick with these types, and enable some quick access providers */ -type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | IImageQuickPickItem | IVariableQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | IScreenShotQuickPickItem | IRelatedFilesQuickPickItem; +type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | + IImageQuickPickItem | IVariableQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | + IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem; /** * These are the types that we can get out of the quick pick @@ -119,6 +126,17 @@ function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPick ); } +/** + * Checks is a provided object is a prompt instructions quick pick item. + */ +function isPromptInstructionsQuickPickItem(obj: unknown): obj is IPromptInstructionsQuickPickItem { + if (!obj || typeof obj !== 'object') { + return false; + } + + return ('kind' in obj && obj.kind === 'prompt-instructions'); +} + interface IRelatedFilesQuickPickItem extends IQuickPickItem { kind: 'related-files'; id: string; @@ -177,9 +195,25 @@ interface IScreenShotQuickPickItem extends IQuickPickItem { icon?: ThemeIcon; } +/** + * Quick pick item for prompt instructions attachment. + */ +interface IPromptInstructionsQuickPickItem extends IQuickPickItem { + /** + * Unique kind identifier of the prompt instructions + * attachment quick pick item. + */ + kind: 'prompt-instructions'; + + /** + * The id of the qucik pick item. + */ + id: string; +} + abstract class AttachFileAction extends Action2 { getFiles(accessor: ServicesAccessor, ...args: any[]): URI[] { - const textEditorService = accessor.get(IEditorService); + const editorService = accessor.get(IEditorService); const contexts = Array.isArray(args[1]) ? args[1] : [args[0]]; const files = []; @@ -191,8 +225,8 @@ abstract class AttachFileAction extends Action2 { uri = context.resource; } else if (isSearchTreeMatch(context)) { uri = context.parent().resource; - } else if (!context && textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor) { - uri = textEditorService.activeEditor?.resource; + } else if (!context && editorService.activeTextEditorControl) { + uri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); } if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) { @@ -214,12 +248,11 @@ class AttachFileToChatAction extends AttachFileAction { title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: [{ id: MenuId.ChatCommandCenter, group: 'b_chat_context', - when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), - order: 10, + order: 15, }, { id: MenuId.SearchContext, group: 'z_chat', @@ -251,12 +284,11 @@ class AttachSelectionToChatAction extends Action2 { title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: [{ id: MenuId.ChatCommandCenter, group: 'b_chat_context', - when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), - order: 15, + order: 10, }, { id: MenuId.SearchContext, group: 'z_chat', @@ -267,7 +299,7 @@ class AttachSelectionToChatAction extends Action2 { override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const variablesService = accessor.get(IChatVariablesService); - const textEditorService = accessor.get(IEditorService); + const editorService = accessor.get(IEditorService); const [_, matches] = args; // If we have search matches, it means this is coming from the search widget if (matches && matches.length > 0) { @@ -293,13 +325,14 @@ class AttachSelectionToChatAction extends Action2 { } } } else { - const activeEditor = textEditorService.activeTextEditorControl; - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const activeEditor = editorService.activeTextEditorControl; + const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (editorService.activeTextEditorControl && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { const selection = activeEditor?.getSelection(); if (selection) { (await showChatView(accessor.get(IViewsService)))?.focusInput(); - variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection; + variablesService.attachContext('file', { uri: activeUri, range }, ChatAgentLocation.Panel); } } } @@ -316,12 +349,11 @@ class AttachFileToEditingSessionAction extends AttachFileAction { title: localize2('workbench.action.edits.attachFile.label', "Add File to {0}", 'Copilot Edits'), category: CHAT_CATEGORY, f1: false, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: [{ id: MenuId.ChatCommandCenter, group: 'c_edits_context', - when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), - order: 10, + order: 15, }, { id: MenuId.SearchContext, group: 'z_chat', @@ -353,26 +385,27 @@ class AttachSelectionToEditingSessionAction extends Action2 { title: localize2('workbench.action.edits.attachSelection.label', "Add Selection to {0}", 'Copilot Edits'), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor')), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: { id: MenuId.ChatCommandCenter, group: 'c_edits_context', - order: 15, + order: 10, } }); } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const variablesService = accessor.get(IChatVariablesService); - const textEditorService = accessor.get(IEditorService); + const editorService = accessor.get(IEditorService); - const activeEditor = textEditorService.activeTextEditorControl; - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const activeEditor = editorService.activeTextEditorControl; + const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (editorService.activeTextEditorControl && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { const selection = activeEditor?.getSelection(); if (selection) { (await showEditsView(accessor.get(IViewsService)))?.focusInput(); - variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.EditingSession); + const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection; + variablesService.attachContext('file', { uri: activeUri, range }, ChatAgentLocation.EditingSession); } } } @@ -429,7 +462,7 @@ export class AttachContextAction extends Action2 { `:${item.range.startLineNumber}`); } - private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, openerService: IOpenerService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { if (isISymbolQuickPickItem(pick) && pick.symbol) { @@ -446,14 +479,19 @@ export class AttachContextAction extends Action2 { } else if (isIQuickPickItemWithResource(pick) && pick.resource) { if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) { // checks if the file is an image - toAttach.push({ - id: pick.resource.toString(), - name: pick.label, - fullName: pick.label, - value: pick.resource, - isDynamic: true, - isImage: true - }); + if (URI.isUri(pick.resource)) { + // read the image and attach a new file context. + const readFile = await fileService.readFile(pick.resource); + const resizedImage = await resizeImage(readFile.value.buffer); + toAttach.push({ + id: pick.resource.toString(), + name: pick.label, + fullName: pick.label, + value: resizedImage, + isDynamic: true, + isImage: true + }); + } } else { // file attachment if (chatEditingService) { @@ -544,6 +582,13 @@ export class AttachContextAction extends Action2 { if (blob) { toAttach.push(convertBufferToScreenshotVariable(blob)); } + } else if (isPromptInstructionsQuickPickItem(pick)) { + await selectPromptAttachment({ + widget, + quickInputService, + labelService, + openerService, + }); } else { // Anything else is an attachment const attachmentPick = pick as IAttachmentQuickPickItem; @@ -618,6 +663,8 @@ export class AttachContextAction extends Action2 { const viewsService = accessor.get(IViewsService); const hostService = accessor.get(IHostService); const extensionService = accessor.get(IExtensionService); + const fileService = accessor.get(IFileService); + const openerService = accessor.get(IOpenerService); const context: { widget?: IChatWidget; showFilesOnly?: boolean; placeholder?: string } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; @@ -757,6 +804,17 @@ export class AttachContextAction extends Action2 { } } + // if the `prompt instructions` feature is enabled, add + // the `Instructions` attachment type to the list + if (widget.attachmentModel.promptInstructions.featureEnabled) { + quickPickItems.push({ + kind: 'prompt-instructions', + id: 'prompt-instructions', + label: localize('promptWithEllipsis', 'Prompt...'), + iconClass: ThemeIcon.asClassName(Codicon.lightbulbSparkle), + }); + } + function extractTextFromIconLabel(label: string | undefined): string { if (!label) { return ''; @@ -771,19 +829,19 @@ export class AttachContextAction extends Action2 { const second = extractTextFromIconLabel(b.label).toUpperCase(); return compare(first, second); - }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, '', context?.placeholder); + }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, openerService, '', context?.placeholder); } - private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, query: string = '', placeholder?: string) { + private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, openerService: IOpenerService, query: string = '', placeholder?: string) { const providerOptions: AnythingQuickAccessProviderRunOptions = { handleAccept: (item: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { if ('prefix' in item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, item.prefix, placeholder); + this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, openerService, item.prefix, placeholder); } else { if (!clipboardService) { return; } - this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, isBackgroundAccept, item); + this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, openerService, isBackgroundAccept, item); if (isQuickChat(widget)) { quickChatService.open(); } @@ -865,3 +923,80 @@ registerAction2(class AttachFilesAction extends AttachContextAction { return super.run(accessor, attachFilesContext); } }); + +/** + * Options for the {@link selectPromptAttachment} function. + */ +interface ISelectPromptOptions { + widget: IChatWidget; + quickInputService: IQuickInputService; + labelService: ILabelService; + openerService: IOpenerService; +} + +/** + * Open the prompt files selection dialog and adds + * selected prompts to the chat attachments model. + */ +const selectPromptAttachment = async (options: ISelectPromptOptions): Promise => { + const { widget, quickInputService, labelService, openerService } = options; + const { promptInstructions } = widget.attachmentModel; + + // find all prompt instruction files in the user project + // and present them to the user so they can select one + const files = await promptInstructions.listNonAttachedFiles() + .then((files) => { + return files.map((file) => { + const result: IQuickPickItem & { value: URI } = { + type: 'item', + label: labelService.getUriBasenameLabel(file), + description: labelService.getUriLabel(dirname(file), { relative: true }), + tooltip: file.fsPath, + value: file, + }; + + return result; + }); + }); + + // if not prompt files found, render the "how to add" message + // to the user with a link to the documentation + if (files.length === 0) { + const docsQuickPick: IQuickPickItem & { value: URI } = { + type: 'item', + label: localize('noPromptFilesFoundTooltipLabel', 'Learn how create reusable prompts'), + description: PromptFilesConfig.DOCUMENTATION_URL, + tooltip: PromptFilesConfig.DOCUMENTATION_URL, + value: URI.parse(PromptFilesConfig.DOCUMENTATION_URL), + }; + + const result = await quickInputService.pick( + [docsQuickPick], + { + placeHolder: localize('noPromptFilesFoundLabel', 'No prompt files found.'), + canPickMany: false, + }); + + if (!result) { + return; + } + + await openerService.open(result.value); + return; + } + + // otherwise show the prompt file selection dialog + const selectedFile = await quickInputService.pick( + files, + { + placeHolder: localize('selectPromptFile', 'Select a prompt file'), + canPickMany: false, + }); + + // if a file was selected, add it to the chat attachments model + if (selectedFile) { + promptInstructions.add(selectedFile.value); + } + + // if the file selection dialog was dismissed, nothing to do +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index caf69f26262..cc5339b263c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -8,6 +8,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -15,12 +16,14 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsContextKey, IChatEditingService } from '../../common/chatEditingService.js'; +import { applyingChatEditsContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { chatAgentLeader, extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatService } from '../../common/chatService.js'; import { EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; +import { discardAllEditsWithConfirmation } from '../chatEditing/chatEditingActions.js'; import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY } from './chatActions.js'; +import { ChatDoneActionId } from './chatClearActions.js'; export interface IVoiceChatExecuteActionContext { readonly disableTimeout?: boolean; @@ -46,13 +49,21 @@ export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.requestInProgress.negate(), + ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), + ); + super({ id: ChatSubmitAction.ID, title: localize2('interactive.submit.label', "Send and Dispatch"), f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession)), + precondition, keybinding: { when: ChatContextKeys.inChatInput, primary: KeyCode.Enter, @@ -67,7 +78,10 @@ export class ChatSubmitAction extends SubmitAction { { id: MenuId.ChatExecute, order: 4, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession)), + when: ContextKeyExpr.and( + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), + ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), + ), group: 'navigation', }, ] @@ -76,23 +90,26 @@ export class ChatSubmitAction extends SubmitAction { } export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode'; + +export interface IToggleAgentModeArgs { + agentMode: boolean; +} + export class ToggleAgentModeAction extends Action2 { static readonly ID = ToggleAgentModeActionId; constructor() { super({ id: ToggleAgentModeAction.ID, - title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode"), + title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode (Experimental)"), f1: true, category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), - ChatContextKeys.Editing.hasToolsAgent), - icon: Codicon.edit, + ChatContextKeys.Editing.hasToolsAgent, + ChatContextKeys.requestInProgress.negate()), toggled: { condition: ChatContextKeys.Editing.agentMode, - icon: Codicon.tools, - tooltip: localize('agentEnabled', "Agent Mode Enabled"), + tooltip: localize('agentEnabled', "Agent Mode Enabled (Experimental)"), }, tooltip: localize('agentDisabled', "Agent Mode Disabled"), keybinding: { @@ -115,9 +132,81 @@ export class ToggleAgentModeAction extends Action2 { }); } - override run(accessor: ServicesAccessor, ...args: any[]): void { + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const agentService = accessor.get(IChatAgentService); - agentService.toggleToolsAgentMode(); + const chatEditingService = accessor.get(IChatEditingService); + const chatService = accessor.get(IChatService); + const commandService = accessor.get(ICommandService); + const dialogService = accessor.get(IDialogService); + const currentEditingSession = chatEditingService.currentEditingSession; + if (!currentEditingSession) { + return; + } + + const entries = currentEditingSession.entries.get(); + if (entries.length > 0 && entries.some(entry => entry.state.get() === WorkingSetEntryState.Modified)) { + if (!await discardAllEditsWithConfirmation(accessor)) { + // User cancelled + return; + } + } else { + const chatSession = chatService.getSession(currentEditingSession.chatSessionId); + if (chatSession?.getRequests().length) { + const confirmation = await dialogService.confirm({ + title: localize('agent.newSession', "Start new session?"), + message: localize('agent.newSessionMessage', "Toggling agent mode will start a new session. Would you like to continue?"), + primaryButton: localize('agent.newSession.confirm', "Yes"), + type: 'info' + }); + if (!confirmation.confirmed) { + return; + } + } + } + + const arg = args[0] as IToggleAgentModeArgs | undefined; + agentService.toggleToolsAgentMode(typeof arg?.agentMode === 'boolean' ? arg.agentMode : undefined); + + await commandService.executeCommand(ChatDoneActionId); + } +} + +export const ToggleRequestPausedActionId = 'workbench.action.chat.toggleRequestPaused'; +export class ToggleRequestPausedAction extends Action2 { + static readonly ID = ToggleRequestPausedActionId; + + constructor() { + super({ + id: ToggleRequestPausedAction.ID, + title: localize2('interactive.toggleRequestPausd.label', "Toggle Request Paused"), + category: CHAT_CATEGORY, + icon: Codicon.debugPause, + toggled: { + condition: ChatContextKeys.isRequestPaused, + icon: Codicon.play, + tooltip: localize('requestIsPaused', "Resume Request"), + }, + tooltip: localize('requestNotPaused', "Pause Request"), + menu: [ + { + id: MenuId.ChatExecute, + order: 3.5, + when: ContextKeyExpr.and( + ChatContextKeys.canRequestBePaused, + ChatContextKeys.Editing.agentMode, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ), + group: 'navigation', + }, + ] + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const context: IChatExecuteActionContext | undefined = args[0]; + const widgetService = accessor.get(IChatWidgetService); + const widget = context?.widget ?? widgetService.lastFocusedWidget; + widget?.togglePaused(); } } @@ -125,13 +214,21 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + applyingChatEditsContextKey.toNegated(), + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ); + super({ id: ChatEditingSessionSubmitAction.ID, title: localize2('edits.submit.label', "Send"), f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + precondition, keybinding: { when: ChatContextKeys.inChatInput, primary: KeyCode.Enter, @@ -141,13 +238,13 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { { id: MenuId.ChatExecuteSecondary, group: 'group_1', - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), order: 1 }, { id: MenuId.ChatExecute, order: 4, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), group: 'navigation', }, ] @@ -159,18 +256,23 @@ class SubmitWithoutDispatchingAction extends Action2 { static readonly ID = 'workbench.action.chat.submitWithoutDispatching'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), + ContextKeyExpr.and(ContextKeyExpr.or( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Editor), + )), + ); + super({ id: SubmitWithoutDispatchingAction.ID, title: localize2('interactive.submitWithoutDispatch.label', "Send"), f1: false, category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and( - ChatContextKeys.inputHasText, - ChatContextKeys.requestInProgress.negate(), - ContextKeyExpr.and(ContextKeyExpr.or( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Editor), - ))), + precondition, keybinding: { when: ChatContextKeys.inChatInput, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter, @@ -219,10 +321,18 @@ export class ChatSubmitSecondaryAgentAction extends Action2 { static readonly ID = 'workbench.action.chat.submitSecondaryAgent'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.inputHasAgent.negate(), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), + ); + super({ id: ChatSubmitSecondaryAgentAction.ID, title: localize2({ key: 'actions.chat.submitSecondaryAgent', comment: ['Send input from the chat input box to the secondary agent'] }, "Submit to Secondary Agent"), - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.inputHasAgent.negate(), ChatContextKeys.requestInProgress.negate()), + precondition, menu: { id: MenuId.ChatExecuteSecondary, group: 'group_1', @@ -262,10 +372,18 @@ export class ChatSubmitSecondaryAgentAction extends Action2 { class SendToChatEditingAction extends Action2 { constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.inputHasAgent.negate(), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), + ); + super({ id: 'workbench.action.chat.sendToChatEditing', title: localize2('chat.sendToChatEditing.label', "Send to Copilot Edits"), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.inputHasAgent.negate(), ChatContextKeys.inputHasText), + precondition, category: CHAT_CATEGORY, f1: false, menu: { @@ -347,10 +465,17 @@ class SendToChatEditingAction extends Action2 { class SendToNewChatAction extends Action2 { constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), + ); + super({ id: 'workbench.action.chat.sendToNewChat', title: localize2('chat.newChat.label', "Send to New Chat"), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.inputHasText), + precondition, category: CHAT_CATEGORY, f1: false, menu: { @@ -392,7 +517,10 @@ export class CancelAction extends Action2 { icon: Codicon.stopCircle, menu: { id: MenuId.ChatExecute, - when: ContextKeyExpr.or(ChatContextKeys.requestInProgress, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey)), + when: ContextKeyExpr.or( + ContextKeyExpr.and(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.requestInProgress), + ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey) + ), order: 4, group: 'navigation', }, @@ -435,4 +563,5 @@ export function registerChatExecuteActions() { registerAction2(ChatSubmitSecondaryAgentAction); registerAction2(SendToChatEditingAction); registerAction2(ToggleAgentModeAction); + registerAction2(ToggleRequestPausedAction); } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index ea35e44e016..208a6498d14 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -6,17 +6,16 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { CHAT_OPEN_ACTION_ID } from './chatActions.js'; import { IExtensionManagementService, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IDefaultChatAgent } from '../../../../../base/common/product.js'; import { IViewDescriptorService } from '../../../../common/views.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { ensureSideBarChatViewSize } from '../chat.js'; +import { ensureSideBarChatViewSize, showCopilotView } from '../chat.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IViewsService } from '../../../../services/views/common/viewsService.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { @@ -28,7 +27,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb constructor( @IProductService private readonly productService: IProductService, @IExtensionService private readonly extensionService: IExtensionService, - @ICommandService private readonly commandService: ICommandService, + @IViewsService private readonly viewsService: IViewsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @@ -75,9 +74,9 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb // Enable chat command center if previously disabled this.configurationService.updateValue('chat.commandCenter.enabled', true); - // Open and configure chat view - await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); - ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); + // Open Copilot view + showCopilotView(this.viewsService, this.layoutService); + ensureSideBarChatViewSize(this.viewDescriptorService, this.layoutService); // Only do this once this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 5462565f4ab..f206b6e1065 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -2,22 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../../base/common/arrays.js'; import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { CharCode } from '../../../../../base/common/charCode.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import { isEqual } from '../../../../../base/common/resources.js'; import * as strings from '../../../../../base/common/strings.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { ConversationRequest, ConversationResponse, DocumentContextItem, IWorkspaceFileEdit, IWorkspaceTextEdit } from '../../../../../editor/common/languages.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -29,9 +28,9 @@ import { InlineChatController } from '../../../inlineChat/browser/inlineChatCont import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; -import { getReferencesAsDocumentContext } from '../../common/chatCodeMapperService.js'; +import { ICodeMapperCodeBlock, ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatUserAction, IChatService } from '../../common/chatService.js'; -import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; export class InsertCodeBlockOperation { @@ -98,7 +97,7 @@ export class InsertCodeBlockOperation { } } -type IComputeEditsResult = { readonly edits?: Array; readonly codeMapper?: string }; +type IComputeEditsResult = { readonly editsProposed: boolean; readonly codeMapper?: string }; export class ApplyCodeBlockOperation { @@ -107,15 +106,14 @@ export class ApplyCodeBlockOperation { constructor( @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, - @IBulkEditService private readonly bulkEditService: IBulkEditService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IChatService private readonly chatService: IChatService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IProgressService private readonly progressService: IProgressService, @ILanguageService private readonly languageService: ILanguageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, + @ICodeMapperService private readonly codeMapperService: ICodeMapperService, + @IProgressService private readonly progressService: IProgressService ) { } @@ -166,7 +164,7 @@ export class ApplyCodeBlockOperation { codeBlockIndex: context.codeBlockIndex, totalCharacters: context.code.length, codeMapper: result?.codeMapper, - editsProposed: !!result?.edits, + editsProposed: !!result?.editsProposed }); } @@ -182,64 +180,37 @@ export class ApplyCodeBlockOperation { } private async handleTextEditor(codeEditor: IActiveCodeEditor, codeBlockContext: ICodeBlockActionContext): Promise { - if (isReadOnly(codeEditor.getModel(), this.textFileService)) { + const activeModel = codeEditor.getModel(); + if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); return undefined; } - const result = await this.computeEdits(codeEditor, codeBlockContext); - if (result.edits) { - const showWithPreview = await this.applyWithInlinePreview(result.edits, codeEditor); - if (!showWithPreview) { - await this.bulkEditService.apply(result.edits, { showPreview: true }); - const activeModel = codeEditor.getModel(); - this.codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); - } - } - return result; - } - - private async computeEdits(codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - const activeModel = codeEditor.getModel(); + const resource = codeBlockContext.codemapperUri ?? activeModel.uri; + const codeBlock = { code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }; - const mappedEditsProviders = this.languageFeaturesService.mappedEditsProvider.ordered(activeModel); - if (mappedEditsProviders.length > 0) { + const codeMapper = this.codeMapperService.providers[0]?.displayName; + if (!codeMapper) { + this.notify(localize('applyCodeBlock.noCodeMapper', "No code mapper available.")); + return undefined; + } - // 0th sub-array - editor selections array if there are any selections - // 1st sub-array - array with documents used to get the chat reply - const docRefs: DocumentContextItem[][] = []; - collectDocumentContextFromSelections(codeEditor, docRefs); - collectDocumentContextFromContext(codeBlockActionContext, docRefs); + const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); + let result = false; + if (editorToApply && editorToApply.hasModel()) { const cancellationTokenSource = new CancellationTokenSource(); - let codeMapper; // the last used code mapper try { - const result = await this.progressService.withProgress( + const iterable = await this.progressService.withProgress>( { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, async progress => { - for (const provider of mappedEditsProviders) { - codeMapper = provider.displayName; - progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); - const mappedEdits = await provider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { - documents: docRefs, - conversation: getChatConversation(codeBlockActionContext), - }, - cancellationTokenSource.token - ); - if (mappedEdits) { - return { edits: mappedEdits.edits, codeMapper }; - } - } - return undefined; + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); + const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); + return await this.waitForFirstElement(editsIterable); }, () => cancellationTokenSource.cancel() ); - if (result) { - return result; - } + result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); } catch (e) { if (!isCancellationError(e)) { this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); @@ -247,41 +218,66 @@ export class ApplyCodeBlockOperation { } finally { cancellationTokenSource.dispose(); } - return { edits: [], codeMapper }; } - return { edits: [], codeMapper: undefined }; + return { + editsProposed: result, + codeMapper + }; } - private async applyWithInlinePreview(edits: Array, codeEditor: IActiveCodeEditor): Promise { - const firstEdit = edits[0]; - if (!ResourceTextEdit.is(firstEdit)) { - return false; - } - const resource = firstEdit.resource; - const textEdits = coalesce(edits.map(edit => ResourceTextEdit.is(edit) && isEqual(resource, edit.resource) ? edit.textEdit : undefined)); - if (textEdits.length !== edits.length) { // more than one file has changed, fall back to bulk edit preview - return false; + private getEdits(codeBlock: ICodeMapperCodeBlock, token: CancellationToken): AsyncIterable { + return new AsyncIterableObject(async executor => { + const request: ICodeMapperRequest = { + codeBlocks: [codeBlock] + }; + const response: ICodeMapperResponse = { + textEdit: (target: URI, edit: TextEdit[]) => { + executor.emitOne(edit); + } + }; + const result = await this.codeMapperService.mapCode(request, response, token); + if (result?.errorMessage) { + executor.reject(new Error(result.errorMessage)); + } + }); + } + + private async waitForFirstElement(iterable: AsyncIterable): Promise> { + const iterator = iterable[Symbol.asyncIterator](); + const firstResult = await iterator.next(); + + if (firstResult.done) { + return { + async *[Symbol.asyncIterator]() { + return; + } + }; } - const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - if (editorToApply) { - const inlineChatController = InlineChatController.get(editorToApply); - if (inlineChatController) { - const tokenSource = new CancellationTokenSource(); - let isOpen = true; - const firstEdit = textEdits[0]; - editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - const promise = inlineChatController.reviewEdits(textEdits[0].range, AsyncIterableObject.fromArray([textEdits]), tokenSource.token); - promise.finally(() => { - isOpen = false; - tokenSource.dispose(); - }); - this.inlineChatPreview = { - promise, - isOpen: () => isOpen, - cancel: () => tokenSource.cancel(), - }; - return true; + + return { + async *[Symbol.asyncIterator]() { + yield firstResult.value; + yield* iterable; } + }; + } + + private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { + const inlineChatController = InlineChatController.get(codeEditor); + if (inlineChatController) { + let isOpen = true; + const promise = inlineChatController.reviewEdits(edits, tokenSource.token); + promise.finally(() => { + isOpen = false; + tokenSource.dispose(); + }); + this.inlineChatPreview = { + promise, + isOpen: () => isOpen, + cancel: () => tokenSource.cancel(), + }; + return true; + } return false; } @@ -360,49 +356,6 @@ function isReadOnly(model: ITextModel, textFileService: ITextFileService): boole return !!activeTextModel?.isReadonly(); } -function collectDocumentContextFromSelections(codeEditor: IActiveCodeEditor, result: DocumentContextItem[][]): void { - const activeModel = codeEditor.getModel(); - const currentDocUri = activeModel.uri; - const currentDocVersion = activeModel.getVersionId(); - const selections = codeEditor.getSelections(); - if (selections.length > 0) { - result.push([ - { - uri: currentDocUri, - version: currentDocVersion, - ranges: selections, - } - ]); - } -} - - -function collectDocumentContextFromContext(context: ICodeBlockActionContext, result: DocumentContextItem[][]): void { - if (isResponseVM(context.element) && context.element.usedContext?.documents) { - result.push(context.element.usedContext.documents); - } -} - -function getChatConversation(context: ICodeBlockActionContext): (ConversationRequest | ConversationResponse)[] { - // TODO@aeschli for now create a conversation with just the current element - // this will be expanded in the future to include the request and any other responses - - if (isResponseVM(context.element)) { - return [{ - type: 'response', - message: context.element.response.getMarkdown(), - references: getReferencesAsDocumentContext(context.element.contentReferences) - }]; - } else if (isRequestVM(context.element)) { - return [{ - type: 'request', - message: context.element.messageText, - }]; - } else { - return []; - } -} - function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number): string { const newContent = strings.splitLines(codeBlockContent); if (newContent.length === 0) { diff --git a/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index dfff8cee32b..73ab2960823 100644 --- a/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -79,7 +79,6 @@ export class ImplicitContextAttachmentWidget extends Disposable { this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current file context") : localize('enable', "Enable current file context"); - this.domNode.ariaLabel = buttonMsg; const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg })); toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed; this.renderDisposables.add(toggleButton.onDidClick((e) => { diff --git a/code/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts b/code/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts new file mode 100644 index 00000000000..58e60e5fbcd --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../base/common/uri.js'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { ResourceLabels } from '../../../../../browser/labels.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { InstructionsAttachmentWidget } from './instructionsAttachment.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ChatInstructionAttachmentsModel } from '../../chatAttachmentModel/chatInstructionAttachmentsModel.js'; + +/** + * Widget fot a collection of prompt instructions attachments. + * See {@linkcode InstructionsAttachmentWidget}. + */ +export class InstructionAttachmentsWidget extends Disposable { + /** + * List of child instruction attachment widgets. + */ + private children: InstructionsAttachmentWidget[] = []; + + /** + * Event that fires when number of attachments change + * + * See {@linkcode onAttachmentsCountChange}. + */ + private _onAttachmentsCountChange = this._register(new Emitter()); + /** + * Subscribe to the `onAttachmentsCountChange` event. + * @param callback Function to invoke when number of attachments change. + */ + public onAttachmentsCountChange(callback: () => unknown): this { + this._register(this._onAttachmentsCountChange.event(callback)); + + return this; + } + + /** + * The root DOM node of the widget. + */ + public readonly domNode: HTMLElement; + + /** + * Get all `URI`s of all valid references, including all + * the possible references nested inside the children. + */ + public get references(): readonly URI[] { + return this.model.references; + } + + /** + * Get the list of all prompt instruction attachment variables, including all + * nested child references of each attachment explicitly attached by user. + */ + public get chatAttachments() { + return this.model.chatAttachments; + } + + /** + * Check if child widget list is empty (no attachments present). + */ + public get empty(): boolean { + return this.children.length === 0; + } + + constructor( + private readonly model: ChatInstructionAttachmentsModel, + private readonly resourceLabels: ResourceLabels, + @IInstantiationService private readonly initService: IInstantiationService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this.render = this.render.bind(this); + this.domNode = dom.$('.chat-prompt-instructions-attachments'); + + this._register(this.model.onUpdate(this.render)); + + // when a new attachment model is added, create a new child widget for it + this.model.onAdd((attachment) => { + const widget = this.initService.createInstance( + InstructionsAttachmentWidget, + attachment, + this.resourceLabels, + ); + + // handle the child widget disposal event, removing it from the list + widget.onDispose(this.handleAttachmentDispose.bind(this, widget)); + + // register the new child widget + this.children.push(widget); + this.domNode.appendChild(widget.domNode); + this.render(); + + // fire the event to notify about the change in the number of attachments + this._onAttachmentsCountChange.fire(); + }); + } + + /** + * Handle child widget disposal. + * @param widget The child widget that was disposed. + */ + public handleAttachmentDispose(widget: InstructionsAttachmentWidget): this { + // common prefix for all log messages + const logPrefix = `[onChildDispose] Widget for instructions attachment '${widget.uri.path}'`; + + // flag to check if the widget was found in the children list + let widgetExists = false; + + // filter out disposed child widget from the list + this.children = this.children.filter((child) => { + if (child === widget) { + // because we filter out all objects here it might be ok to have multiple of them, but + // it also highlights a potential issue in our logic somewhere else, so trace a warning here + if (widgetExists) { + this.logService.warn( + `${logPrefix} is present in the children references list multiple times.`, + ); + } + + widgetExists = true; + return false; + } + + return true; + }); + + // no widget was found in the children list, while it might be ok it also + // highlights a potential issue in our logic, so trace a warning here + if (!widgetExists) { + this.logService.warn( + `${logPrefix} was disposed, but was not found in the child references.`, + ); + } + + // remove the child widget root node from the DOM + this.domNode.removeChild(widget.domNode); + + // re-render the whole widget + this.render(); + + // fire the event to notify about the change in the number of attachments + this._onAttachmentsCountChange.fire(); + + return this; + } + + /** + * Render this widget. + */ + private render(): this { + // set visibility of the root node based on the presence of attachments + dom.setVisibility(!this.empty, this.domNode); + + return this; + } + + /** + * Dispose of the widget, including all the child + * widget instances. + */ + public override dispose(): void { + for (const child of this.children) { + child.dispose(); + } + + super.dispose(); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/code/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts new file mode 100644 index 00000000000..c5ee5d143e5 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../nls.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { ResourceLabels } from '../../../../../browser/labels.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { ResourceContextKey } from '../../../../../common/contextkeys.js'; +import { Button } from '../../../../../../base/browser/ui/button/button.js'; +import { basename, dirname } from '../../../../../../base/common/resources.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; +import { FileKind, IFileService } from '../../../../../../platform/files/common/files.js'; +import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; +import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js'; +import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; + +/** + * Widget for a single prompt instructions attachment. + */ +export class InstructionsAttachmentWidget extends Disposable { + /** + * The root DOM node of the widget. + */ + public readonly domNode: HTMLElement; + + /** + * Get the `URI` associated with the model reference. + */ + public get uri(): URI { + return this.model.reference.uri; + } + + /** + * Event that fires when the object is disposed. + * + * See {@linkcode onDispose}. + */ + protected _onDispose = this._register(new Emitter()); + /** + * Subscribe to the `onDispose` event. + * @param callback Function to invoke on dispose. + */ + public onDispose(callback: () => unknown): this { + this._register(this._onDispose.event(callback)); + + return this; + } + + /** + * Temporary disposables used for rendering purposes. + */ + private readonly renderDisposables = this._register(new DisposableStore()); + + constructor( + private readonly model: ChatInstructionsAttachmentModel, + private readonly resourceLabels: ResourceLabels, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IHoverService private readonly hoverService: IHoverService, + @ILabelService private readonly labelService: ILabelService, + @IMenuService private readonly menuService: IMenuService, + @IFileService private readonly fileService: IFileService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, + ) { + super(); + + this.domNode = dom.$('.chat-prompt-instructions-attachment.chat-attached-context-attachment.show-file-icons.implicit'); + + this.render = this.render.bind(this); + this.dispose = this.dispose.bind(this); + + this.model.onUpdate(this.render); + this.model.onDispose(this.dispose); + + this.render(); + } + + /** + * Render this widget. + */ + private render() { + dom.clearNode(this.domNode); + this.renderDisposables.clear(); + this.domNode.classList.remove('warning', 'error', 'disabled'); + + const { topError } = this.model; + + const label = this.resourceLabels.create(this.domNode, { supportIcons: true }); + const file = this.model.reference.uri; + + const fileBasename = basename(file); + const fileDirname = dirname(file); + const friendlyName = `${fileBasename} ${fileDirname}`; + const ariaLabel = localize('chat.promptAttachment', "Prompt attachment, {0}", friendlyName); + + const uriLabel = this.labelService.getUriLabel(file, { relative: true }); + const promptLabel = localize('prompt', "Prompt"); + + let title = `${promptLabel} ${uriLabel}`; + + // if there are some errors/warning during the process of resolving + // attachment references (including all the nested child references), + // add the issue details in the hover title for the attachment, one + // error/warning at a time because there is a limited space available + if (topError) { + const { isRootError, message: details } = topError; + const isWarning = !isRootError; + + this.domNode.classList.add( + (isWarning) ? 'warning' : 'error', + ); + + const errorCaption = (isWarning) + ? localize('warning', "Warning") + : localize('error', "Error"); + + title += `\n-\n[${errorCaption}]: ${details}`; + } + + const fileWithoutExtension = fileBasename.replace(PROMPT_SNIPPET_FILE_EXTENSION, ''); + label.setFile(URI.file(fileWithoutExtension), { + fileKind: FileKind.FILE, + hidePath: true, + range: undefined, + title, + icon: ThemeIcon.fromId(Codicon.lightbulbSparkle.id), + extraClasses: [], + }); + this.domNode.ariaLabel = ariaLabel; + this.domNode.tabIndex = 0; + + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, promptLabel)); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); + + // create the `remove` button + const removeButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: localize('remove', "Remove") })); + removeButton.icon = Codicon.x; + this.renderDisposables.add(removeButton.onDidClick((e) => { + e.stopPropagation(); + this.model.dispose(); + })); + + // context menu + const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode)); + + const resourceContextKey = this.renderDisposables.add( + new ResourceContextKey(scopedContextKeyService, this.fileService, this.languageService, this.modelService), + ); + resourceContextKey.set(file); + + this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, async domEvent => { + const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); + dom.EventHelper.stop(domEvent, true); + + this.contextMenuService.showContextMenu({ + contextKeyService: scopedContextKeyService, + getAnchor: () => event, + getActions: () => { + const menu = this.menuService.getMenuActions(MenuId.ChatInputResourceAttachmentContext, scopedContextKeyService, { arg: file }); + return getFlatContextMenuActions(menu); + }, + }); + })); + } + + public override dispose(): void { + this._onDispose.fire(); + + super.dispose(); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index a43f6b50cac..88a458ba5cd 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -40,7 +40,7 @@ import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService. import { EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js'; import { ChatCommandCenterRendering, registerChatActions } from './actions/chatActions.js'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from './actions/chatClearActions.js'; -import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; +import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; import { registerChatCopyActions } from './actions/chatCopyActions.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; @@ -60,7 +60,6 @@ import { registerChatEditorActions } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; -import { ChatEditorAutoSaveDisabler, ChatEditorSaving } from './chatEditorSaving.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; import { ChatPasteProvidersFeature } from './chatPasteProviders.js'; @@ -78,11 +77,13 @@ import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler. import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } from '../common/ignoredFiles.js'; import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; -import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; import { BuiltinToolsContribution } from './tools/tools.js'; import { ChatSetupContribution } from './chatSetup.js'; +import { ChatEditorOverlayController } from './chatEditorOverlay.js'; +import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; +import { PromptFilesConfig } from '../common/promptSyntax/config.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -123,11 +124,12 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control Copilot (requires {0}).", '`#window.commandCenter#`'), default: true }, - 'chat.editing.alwaysSaveWithGeneratedChanges': { - type: 'boolean', - scope: ConfigurationScope.APPLICATION, - markdownDescription: nls.localize('chat.editing.alwaysSaveWithGeneratedChanges', "Whether files that have changes made by chat can be saved without confirmation."), - default: false, + 'chat.editing.autoAcceptDelay': { + type: 'number', + markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."), + default: 0, + minimum: 0, + maximum: 100 }, 'chat.editing.confirmEditRequestRemoval': { type: 'boolean', @@ -152,6 +154,18 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.detectParticipant.enabled', "Enables chat participant autodetection for panel chat."), default: true }, + [PromptFilesConfig.CONFIG_KEY]: { + type: ['string', 'array', 'boolean', 'null'], + title: nls.localize('chat.promptFiles.setting.title', "Prompt Files"), + markdownDescription: nls.localize( + 'chat.promptFiles.setting.markdownDescription', + "Enable support for attaching reusable prompt files (`*{0}`) for Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", + '.prompt.md', + PromptFilesConfig.DOCUMENTATION_URL, + ), + default: null, + tags: ['experimental'], + }, } }); Registry.as(EditorExtensions.EditorPane).registerEditorPane( @@ -313,10 +327,9 @@ registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointH registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatCommandCenterRendering.ID, ChatCommandCenterRendering, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(CodeBlockActionRendering.ID, CodeBlockActionRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(ChatEditorAutoSaveDisabler.ID, ChatEditorAutoSaveDisabler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); @@ -339,8 +352,8 @@ registerChatDeveloperActions(); registerChatEditorActions(); registerEditorFeature(ChatPasteProvidersFeature); +registerEditorContribution(ChatEditorOverlayController.ID, ChatEditorOverlayController, EditorContributionInstantiation.Lazy); registerEditorContribution(ChatEditorController.ID, ChatEditorController, EditorContributionInstantiation.Eventually); -registerEditorContribution(ChatEditorOverlayController.ID, ChatEditorOverlayController, EditorContributionInstantiation.Eventually); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.ts b/code/src/vs/workbench/contrib/chat/browser/chat.ts index 43d623df320..ed82ef63428 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.ts @@ -52,7 +52,30 @@ export async function showEditsView(viewsService: IViewsService): Promise(EditsViewId))?.widget; } -export function ensureSideBarChatViewSize(width: number, viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { +export function preferCopilotEditsView(viewsService: IViewsService): boolean { + if (viewsService.getFocusedView()?.id === ChatViewId || !!viewsService.getActiveViewWithId(ChatViewId)) { + return false; + } + + return !!viewsService.getActiveViewWithId(EditsViewId); +} + +export function showCopilotView(viewsService: IViewsService, layoutService: IWorkbenchLayoutService): Promise { + + // Ensure main window is in front + if (layoutService.activeContainer !== layoutService.mainContainer) { + layoutService.mainContainer.focus(); + } + + // Bring up the correct view + if (preferCopilotEditsView(viewsService)) { + return showEditsView(viewsService); + } else { + return showChatView(viewsService); + } +} + +export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { const location = viewDescriptorService.getViewLocationById(ChatViewId); if (location === ViewContainerLocation.Panel) { return; // panel is typically very wide @@ -60,8 +83,16 @@ export function ensureSideBarChatViewSize(width: number, viewDescriptorService: const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; const partSize = layoutService.getSize(viewPart); - if (partSize.width < width) { - layoutService.setSize(viewPart, { width: width, height: partSize.height }); + + let adjustedChatWidth: number | undefined; + if (partSize.width < 400 && layoutService.mainContainerDimension.width > 1200) { + adjustedChatWidth = 400; // up to 400px if window bounds permit + } else if (partSize.width < 300) { + adjustedChatWidth = 300; // at minimum 300px + } + + if (typeof adjustedChatWidth === 'number') { + layoutService.setSize(viewPart, { width: adjustedChatWidth, height: partSize.height }); } } @@ -156,6 +187,7 @@ export interface IChatWidgetViewOptions { defaultElementHeight?: number; editorOverflowWidgetsDomNode?: HTMLElement; enableImplicitContext?: boolean; + enableWorkingSet?: 'explicit' | 'implicit'; } export interface IChatViewViewContext { @@ -214,6 +246,7 @@ export interface IChatWidget { getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; clear(): void; getViewState(): IChatViewState; + togglePaused(): void; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/code/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 0f083a319ab..53485a25897 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -17,8 +17,8 @@ import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { showExtensionsWithIdsCommandId } from '../../extensions/browser/extensionsActions.js'; -import { verifiedPublisherIcon } from '../../extensions/browser/extensionsIcons.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; export class ChatAgentHover extends Disposable { public readonly domNode: HTMLElement; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 5dd6ddf986b..739be7f185d 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -10,8 +10,27 @@ import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IChatEditingService } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ChatInstructionAttachmentsModel } from './chatAttachmentModel/chatInstructionAttachmentsModel.js'; export class ChatAttachmentModel extends Disposable { + /** + * Collection on prompt instruction attachments. + */ + public readonly promptInstructions: ChatInstructionAttachmentsModel; + + constructor( + @IInstantiationService private readonly initService: IInstantiationService, + ) { + super(); + + this.promptInstructions = this._register( + this.initService.createInstance(ChatInstructionAttachmentsModel), + ).onUpdate(() => { + this._onDidChangeContext.fire(); + }); + } + private _attachments = new Map(); get attachments(): ReadonlyArray { return Array.from(this._attachments.values()); @@ -44,13 +63,14 @@ export class ChatAttachmentModel extends Disposable { this.addContext(this.asVariableEntry(uri, range)); } - asVariableEntry(uri: URI, range?: IRange): IChatRequestVariableEntry { + asVariableEntry(uri: URI, range?: IRange, isMarkedReadonly?: boolean): IChatRequestVariableEntry { return { value: range ? { uri, range } : uri, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri), isFile: true, - isDynamic: true + isDynamic: true, + isMarkedReadonly, }; } @@ -91,8 +111,9 @@ export class EditsAttachmentModel extends ChatAttachmentModel { constructor( @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IInstantiationService _initService: IInstantiationService, ) { - super(); + super(_initService); } private isExcludeFileAttachment(fileAttachmentId: string) { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts new file mode 100644 index 00000000000..0bfa66a79f0 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../base/common/uri.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { IChatRequestVariableEntry } from '../../common/chatModel.js'; +import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; +import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js'; +import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; + +/** + * Utility to convert a {@link reference} to a chat variable entry. + * The `id` of the chat variable can be one of the following: + * + * - `vscode.prompt.instructions__`: for all non-root prompt file references + * - `vscode.prompt.instructions.root__`: for *root* prompt file references + * - ``: for the rest of references(the ones that do not point to a prompt file) + * + * @param reference A reference object to convert to a chat variable entry. + * @param isRoot If the reference is the root reference in the references tree. + * This object most likely was explicitly attached by the user. + */ +const toChatVariable = ( + reference: Pick, + isRoot: boolean, +): IChatRequestVariableEntry => { + const { uri } = reference; + + // default `id` is the stringified `URI` + let id = `${uri}`; + + // for prompt files, we add a prefix to the `id` + if (reference.isPromptSnippet) { + // the default prefix that is used for all prompt files + let prefix = 'vscode.prompt.instructions'; + // if the reference is the root object, add the `.root` suffix + if (isRoot) { + prefix += '.root'; + } + + // final `id` for all `prompt files` starts with the well-defined + // part that the copilot extension(or other chatbot) can rely on + id = `${prefix}__${id}`; + } + + return { + id, + name: uri.fsPath, + value: uri, + isSelection: false, + enabled: true, + isFile: true, + isDynamic: true, + isMarkedReadonly: true, + }; +}; + +/** + * Model for a collection of prompt instruction attachments. + * See {@linkcode ChatInstructionsAttachmentModel} for individual attachment. + */ +export class ChatInstructionAttachmentsModel extends Disposable { + /** + * Helper to locate prompt instruction files on the disk. + */ + private readonly instructionsFileReader: ChatInstructionsFileLocator; + + /** + * List of all prompt instruction attachments. + */ + private attachments: DisposableMap = + this._register(new DisposableMap()); + + /** + * Get all `URI`s of all valid references, including all + * the possible references nested inside the children. + */ + public get references(): readonly URI[] { + const result = []; + + for (const child of this.attachments.values()) { + result.push(...child.references); + } + + return result; + } + + /** + * Get the list of all prompt instruction attachment variables, including all + * nested child references of each attachment explicitly attached by user. + */ + public get chatAttachments(): readonly IChatRequestVariableEntry[] { + const result = []; + const attachments = [...this.attachments.values()]; + + for (const attachment of attachments) { + const { reference } = attachment; + + // the usual URIs list of prompt instructions is `bottom-up`, therefore + // we do the same herfe - first add all child references of the model + result.push( + ...reference.allValidReferences.map((link) => { + return toChatVariable(link, false); + }), + ); + + // then add the root reference of the model itself + result.push( + toChatVariable(reference, true), + ); + } + + return result; + } + + /** + * Promise that resolves when parsing of all attached prompt instruction + * files completes, including parsing of all its possible child references. + */ + public async allSettled(): Promise { + const attachments = [...this.attachments.values()]; + + await Promise.allSettled( + attachments.map((attachment) => { + return attachment.allSettled; + }), + ); + } + + /** + * Event that fires then this model is updated. + * + * See {@linkcode onUpdate}. + */ + protected _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the `onUpdate` event. + * @param callback Function to invoke on update. + */ + public onUpdate(callback: () => unknown): this { + this._register(this._onUpdate.event(callback)); + + return this; + } + + /** + * Event that fires when a new prompt instruction attachment is added. + * See {@linkcode onAdd}. + */ + protected _onAdd = this._register(new Emitter()); + /** + * The `onAdd` event fires when a new prompt instruction attachment is added. + * + * @param callback Function to invoke on add. + */ + public onAdd(callback: (attachment: ChatInstructionsAttachmentModel) => unknown): this { + this._register(this._onAdd.event(callback)); + + return this; + } + + constructor( + @IInstantiationService private readonly initService: IInstantiationService, + @IConfigurationService private readonly configService: IConfigurationService, + ) { + super(); + + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + this.instructionsFileReader = initService.createInstance(ChatInstructionsFileLocator); + } + + /** + * Add a prompt instruction attachment instance with the provided `URI`. + * @param uri URI of the prompt instruction attachment to add. + */ + public add(uri: URI): this { + // if already exists, nothing to do + if (this.attachments.has(uri.path)) { + return this; + } + + const instruction = this.initService.createInstance(ChatInstructionsAttachmentModel, uri) + .onUpdate(this._onUpdate.fire) + .onDispose(() => { + // note! we have to use `deleteAndLeak` here, because the `*AndDispose` + // alternative results in an infinite loop of calling this callback + this.attachments.deleteAndLeak(uri.path); + this._onUpdate.fire(); + }); + + this.attachments.set(uri.path, instruction); + instruction.resolve(); + + this._onAdd.fire(instruction); + this._onUpdate.fire(); + + return this; + } + + /** + * Remove a prompt instruction attachment instance by provided `URI`. + * @param uri URI of the prompt instruction attachment to remove. + */ + public remove(uri: URI): this { + // if does not exist, nothing to do + if (!this.attachments.has(uri.path)) { + return this; + } + + this.attachments.deleteAndDispose(uri.path); + + return this; + } + + /** + * List prompt instruction files available and not attached yet. + */ + public async listNonAttachedFiles(): Promise { + return await this.instructionsFileReader.listFiles(this.references); + } + + /** + * Checks if the prompt instructions feature is enabled in the user settings. + */ + public get featureEnabled(): boolean { + return PromptFilesConfig.enabled(this.configService); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts new file mode 100644 index 00000000000..a9a6a6ffbe5 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../base/common/uri.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { FilePromptParser } from '../../common/promptSyntax/parsers/filePromptParser.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Model for a single chat prompt instructions attachment. + */ +export class ChatInstructionsAttachmentModel extends Disposable { + /** + * Private reference of the underlying prompt instructions + * reference instance. + */ + private readonly _reference: FilePromptParser; + /** + * Get the prompt instructions reference instance. + */ + public get reference(): FilePromptParser { + return this._reference; + } + + /** + * Get `URI` for the main reference and `URI`s of all valid child + * references it may contain, including reference of this model itself. + */ + public get references(): readonly URI[] { + const { reference } = this; + const { errorCondition } = this.reference; + + // return no references if the attachment is disabled + // or if this object itself has an error + if (errorCondition) { + return []; + } + + // otherwise return `URI` for the main reference and + // all valid child `URI` references it may contain + return [ + ...reference.allValidReferencesUris, + reference.uri, + ]; + } + + /** + * Promise that resolves when the prompt is fully parsed, + * including all its possible nested child references. + */ + public get allSettled(): Promise { + return this.reference.allSettled(); + } + + /** + * Get the top-level error of the prompt instructions + * reference, if any. + */ + public get topError() { + return this.reference.topError; + } + + /** + * Event that fires when the error condition of the prompt + * reference changes. + * + * See {@linkcode onUpdate}. + */ + protected _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the `onUpdate` event. + * @param callback Function to invoke on update. + */ + public onUpdate(callback: () => unknown): this { + this._register(this._onUpdate.event(callback)); + + return this; + } + + /** + * Event that fires when the object is disposed. + * + * See {@linkcode onDispose}. + */ + protected _onDispose = this._register(new Emitter()); + /** + * Subscribe to the `onDispose` event. + * @param callback Function to invoke on dispose. + */ + public onDispose(callback: () => unknown): this { + this._register(this._onDispose.event(callback)); + + return this; + } + + constructor( + uri: URI, + @IInstantiationService private readonly initService: IInstantiationService, + ) { + super(); + + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + this._reference = this._register(this.initService.createInstance(FilePromptParser, uri, [])) + .onUpdate(this._onUpdate.fire); + } + + /** + * Start resolving the prompt instructions reference and child references + * that it may contain. + */ + public resolve(): this { + this._reference.start(); + + return this; + } + + public override dispose(): void { + this._onDispose.fire(); + + super.dispose(); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts new file mode 100644 index 00000000000..577e38c7c81 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../base/common/uri.js'; +import { dirname, extUri } from '../../../../../base/common/resources.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; + +/** + * Class to locate prompt instructions files. + */ +export class ChatInstructionsFileLocator { + constructor( + @IFileService private readonly fileService: IFileService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IConfigurationService private readonly configService: IConfigurationService, + ) { } + + /** + * List all prompt instructions files from the filesystem. + * + * @param exclude List of `URIs` to exclude from the result. + * @returns List of prompt instructions files found in the workspace. + */ + public async listFiles(exclude: ReadonlyArray): Promise { + // create a set from the list of URIs for convenience + const excludeSet: Set = new Set(); + for (const excludeUri of exclude) { + excludeSet.add(excludeUri.path); + } + + // filter out the excluded paths from the locations list + const locations = this.getSourceLocations() + .filter((location) => { + return !excludeSet.has(location.path); + }); + + return await this.findInstructionFiles(locations, excludeSet); + } + + /** + * Get all possible prompt instructions file locations based on the current + * workspace folder structure. + * + * @returns List of possible prompt instructions file locations. + */ + private getSourceLocations(): readonly URI[] { + const state = this.workspaceService.getWorkbenchState(); + + // nothing to do if the workspace is empty + if (state === WorkbenchState.EMPTY) { + return []; + } + + const sourceLocations = PromptFilesConfig.sourceLocations(this.configService); + const result = []; + + // otherwise for each folder provided in the configuration, create + // a URI per each folder in the current workspace + const { folders } = this.workspaceService.getWorkspace(); + for (const folder of folders) { + for (const sourceFolderName of sourceLocations) { + const folderUri = extUri.resolvePath(folder.uri, sourceFolderName); + result.push(folderUri); + } + } + + // if inside a workspace, add the specified source locations inside the workspace + // root too, to allow users to use `.copilot/prompts` folder (or whatever they + // specify in the setting) in the workspace root + if (folders.length > 1) { + const workspaceRootUri = dirname(folders[0].uri); + for (const sourceFolderName of sourceLocations) { + const folderUri = extUri.resolvePath(workspaceRootUri, sourceFolderName); + result.push(folderUri); + } + } + + return result; + } + + /** + * Finds all existent prompt instruction files in the provided locations. + * + * @param locations List of locations to search for prompt instruction files in. + * @param exclude Map of `path -> boolean` to exclude from the result. + * @returns List of prompt instruction files found in the provided locations. + */ + private async findInstructionFiles( + locations: readonly URI[], + exclude: ReadonlySet, + ): Promise { + const results = await this.fileService.resolveAll( + locations.map((location) => { + return { resource: location }; + }), + ); + + const files = []; + for (const result of results) { + const { stat, success } = result; + + if (!success) { + continue; + } + + if (!stat || !stat.children) { + continue; + } + + for (const child of stat.children) { + const { name, resource, isDirectory } = child; + + if (isDirectory) { + continue; + } + + if (!name.endsWith(PROMPT_SNIPPET_FILE_EXTENSION)) { + continue; + } + + if (exclude.has(resource.path)) { + continue; + } + + files.push(resource); + } + } + + return files; + + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 6bd5ad40023..599955eefcf 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../../base/browser/ui/hover/hover.js'; import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { Promises } from '../../../../../base/common/async.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; @@ -55,7 +56,7 @@ export class ChatAttachmentsContentPart extends Disposable { private readonly variables: IChatRequestVariableEntry[], private readonly contentReferences: ReadonlyArray = [], private readonly workingSet: ReadonlyArray = [], - public readonly domNode: HTMLElement = dom.$('.chat-attached-context'), + public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IOpenerService private readonly openerService: IOpenerService, @@ -68,14 +69,17 @@ export class ChatAttachmentsContentPart extends Disposable { super(); this.initAttachedContext(domNode); + if (!domNode.childElementCount) { + this.domNode = undefined; + } } private initAttachedContext(container: HTMLElement) { dom.clearNode(container); this.attachedContextDisposables.clear(); - dom.setVisibility(Boolean(this.variables.length), this.domNode); const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); + const attachmentInitPromises: Promise[] = []; this.variables.forEach(async (attachment) => { let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; @@ -127,34 +131,51 @@ export class ChatAttachmentsContentPart extends Disposable { }); } else if (attachment.isImage) { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); - const hoverElement = dom.$('div.chat-attached-context-hover'); hoverElement.setAttribute('aria-label', ariaLabel); // Custom label - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(isAttachmentOmitted ? 'span.codicon.codicon-warning' : 'span.codicon.codicon-file-media')); const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.name); widget.appendChild(pillIcon); widget.appendChild(textLabel); - let buffer: Uint8Array; - try { - if (attachment.value instanceof URI) { - const readFile = await this.fileService.readFile(attachment.value); - buffer = readFile.value.buffer; - - } else { - buffer = attachment.value as Uint8Array; - } - await this.createImageElements(buffer, widget, hoverElement); - } catch (error) { - console.error('Error processing attachment:', error); + if (attachment.references) { + widget.style.cursor = 'pointer'; + const clickHandler = () => { + if (attachment.references && URI.isUri(attachment.references[0].reference)) { + this.openResource(attachment.references[0].reference, false, undefined); + } + }; + this.attachedContextDisposables.add(dom.addDisposableListener(widget, 'click', clickHandler)); } - widget.style.position = 'relative'; - if (!this.attachedContextDisposables.isDisposed) { - this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement)); + if (isAttachmentPartialOrOmitted) { + hoverElement.textContent = localize('chat.imageAttachmentHover', "Image was not sent to the model."); + textLabel.style.textDecoration = 'line-through'; + this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); + } else { + attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { + let buffer: Uint8Array; + try { + if (attachment.value instanceof URI) { + const readFile = await this.fileService.readFile(attachment.value); + if (this.attachedContextDisposables.isDisposed) { + return; + } + buffer = readFile.value.buffer; + } else { + buffer = attachment.value as Uint8Array; + } + this.createImageElements(buffer, widget, hoverElement); + } catch (error) { + console.error('Error processing attachment:', error); + } + this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); + resolve(); + })); } + widget.style.position = 'relative'; } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); @@ -217,6 +238,11 @@ export class ChatAttachmentsContentPart extends Disposable { } } + await Promise.all(attachmentInitPromises); + if (this.attachedContextDisposables.isDisposed) { + return; + } + if (resource) { widget.style.cursor = 'pointer'; if (!this.attachedContextDisposables.isDisposed) { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts index b38aa13424b..68a8d913c37 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -42,6 +42,8 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons)); confirmationWidget.setShowButtons(!confirmation.isUsed); + this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this._register(confirmationWidget.onDidClick(async e => { if (isResponseVM(element)) { const prompt = `${e.label}: "${confirmation.title}"`; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts index df2b54b53e1..b6f42ef60cc 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts @@ -23,6 +23,9 @@ export class ChatConfirmationWidget extends Disposable { private _onDidClick = this._register(new Emitter()); get onDidClick(): Event { return this._onDidClick.event; } + private _onDidChangeHeight = this._register(new Emitter()); + get onDidChangeHeight(): Event { return this._onDidChangeHeight.event; } + private _domNode: HTMLElement; get domNode(): HTMLElement { return this._domNode; @@ -48,10 +51,15 @@ export class ChatConfirmationWidget extends Disposable { this._domNode = elements.root; const renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); - const renderedTitle = this._register(renderer.render(new MarkdownString(title))); + const renderedTitle = this._register(renderer.render(new MarkdownString(title), { + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + })); elements.title.appendChild(renderedTitle.element); - const renderedMessage = this._register(renderer.render(typeof message === 'string' ? new MarkdownString(message) : message)); + const renderedMessage = this._register(renderer.render( + typeof message === 'string' ? new MarkdownString(message) : message, + { asyncRenderCallback: () => this._onDidChangeHeight.fire() } + )); elements.message.appendChild(renderedMessage.element); buttons.forEach(buttonData => { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 44c0c0dbb6b..8d7654540df 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -84,7 +84,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const result = this._register(renderer.render(markdown.content, { fillInIncompleteTokens, codeBlockRendererSync: (languageId, text, raw) => { - const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || raw?.trim().endsWith('```'); + const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw); if ((!text || (text.startsWith('') && !text.includes('\n'))) && !isCodeBlockComplete && rendererOptions.renderCodeBlockPills) { const hideEmptyCodeblock = $('div'); hideEmptyCodeblock.style.display = 'none'; @@ -278,6 +278,11 @@ export class EditorPool extends Disposable { } } +function codeblockHasClosingBackticks(str: string): boolean { + str = str.trim(); + return !!str.match(/\n```+$/); +} + class CollapsedCodeBlock extends Disposable { public readonly element: HTMLElement; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index a06c21d966b..04ec43faa61 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -37,7 +37,7 @@ import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { SETTINGS_AUTHORITY } from '../../../../services/preferences/common/preferences.js'; import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js'; import { ExplorerFolderContext } from '../../../files/common/files.js'; -import { chatEditingWidgetFileStateContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { chatEditingWidgetFileReadonlyContextKey, chatEditingWidgetFileStateContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference, IChatWarningMessage } from '../../common/chatService.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { IChatRendererContent, IChatResponseViewModel } from '../../common/chatViewModel.js'; @@ -52,6 +52,7 @@ export interface IChatReferenceListItem extends IChatContentReference { description?: string; state?: WorkingSetEntryState; excluded?: boolean; + isMarkedReadonly?: boolean; } export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; @@ -435,8 +436,11 @@ class CollapsibleListRenderer implements IListRenderer this._onDidChangeHeight.fire())); partStore.add(subPart.onNeedsRerender(() => { render(); this._onDidChangeHeight.fire(); @@ -66,11 +68,15 @@ class ChatToolInvocationSubPart extends Disposable { private _onNeedsRerender = this._register(new Emitter()); public readonly onNeedsRerender = this._onNeedsRerender.event; + private _onDidChangeHeight = this._register(new Relay()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + constructor( toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, context: IChatContentPartRenderContext, renderer: MarkdownRenderer, @IInstantiationService instantiationService: IInstantiationService, + @IHoverService hoverService: IHoverService, ) { super(); @@ -86,11 +92,22 @@ class ChatToolInvocationSubPart extends Disposable { this._register(confirmWidget.onDidClick(button => { toolInvocation.confirmed.complete(button.data); })); - toolInvocation.confirmed.p.then(() => this._onNeedsRerender.fire()); + this._onDidChangeHeight.input = confirmWidget.onDidChangeHeight; + toolInvocation.confirmed.p.then(() => { + this._onNeedsRerender.fire(); + }); } else { - const content = typeof toolInvocation.invocationMessage === 'string' ? - new MarkdownString().appendText(toolInvocation.invocationMessage + '…') : - new MarkdownString(toolInvocation.invocationMessage.value + '…'); + let content: IMarkdownString; + if (toolInvocation.isComplete && toolInvocation.isConfirmed !== false && toolInvocation.pastTenseMessage) { + content = typeof toolInvocation.pastTenseMessage === 'string' ? + new MarkdownString().appendText(toolInvocation.pastTenseMessage) : + toolInvocation.pastTenseMessage; + } else { + content = typeof toolInvocation.invocationMessage === 'string' ? + new MarkdownString().appendText(toolInvocation.invocationMessage + '…') : + new MarkdownString(toolInvocation.invocationMessage.value + '…'); + } + const progressMessage: IChatProgressMessage = { kind: 'progressMessage', content @@ -100,6 +117,10 @@ class ChatToolInvocationSubPart extends Disposable { toolInvocation.isComplete ? Codicon.check : undefined; const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, progressMessage, renderer, context, undefined, true, iconOverride)); + if (toolInvocation.tooltip) { + this._register(hoverService.setupDelayedHover(progressPart.domNode, { content: toolInvocation.tooltip, additionalClasses: ['chat-tool-hover'] })); + } + this.domNode = progressPart.domNode; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index c9194487fe0..2550c96d86c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -26,6 +26,7 @@ import { UntitledTextEditorInput } from '../../../services/untitled/common/untit import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../common/chatModel.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { IChatInputStyles } from './chatInputPart.js'; +import { resizeImage } from './imageUtils.js'; enum ChatDragAndDropType { FILE_INTERNAL, @@ -232,7 +233,7 @@ export class ChatDragAndDrop extends Themable { private async resolveAttachContext(editorInput: IDraggedResourceEditorInput): Promise { // Image - const imageContext = getImageAttachContext(editorInput); + const imageContext = await getImageAttachContext(editorInput, this.fileService); if (imageContext) { return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? imageContext : undefined; } @@ -425,22 +426,25 @@ function getResourceAttachContext(resource: URI, isDirectory: boolean): IChatReq }; } -function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput): IChatRequestVariableEntry | undefined { +async function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput, fileService: IFileService): Promise { if (!editor.resource) { return undefined; } - if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(editor.resource.path)) { + if (/\.(png|jpg|jpeg|gif|webp)$/i.test(editor.resource.path)) { const fileName = basename(editor.resource); + const readFile = await fileService.readFile(editor.resource); + const resizedImage = await resizeImage(readFile.value.buffer); return { id: editor.resource.toString(), name: fileName, fullName: editor.resource.path, - value: editor.resource, + value: resizedImage, icon: Codicon.fileMedia, isDynamic: true, isImage: true, - isFile: false + isFile: false, + references: [{ reference: editor.resource, kind: 'reference' }] }; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts new file mode 100644 index 00000000000..7258043aa36 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { isEqual } from '../../../../../base/common/resources.js'; +import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { findDiffEditorContainingCodeEditor } from '../../../../../editor/browser/widget/diffEditor/commands.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IModifiedFileEntry } from '../../common/chatEditingService.js'; + +export function isDiffEditorForEntry(accessor: ServicesAccessor, entry: IModifiedFileEntry, editor: ICodeEditor) { + const diffEditor = findDiffEditorContainingCodeEditor(accessor, editor); + if (!diffEditor) { + return false; + } + const originalModel = diffEditor.getOriginalEditor().getModel(); + const modifiedModel = diffEditor.getModifiedEditor().getModel(); + return isEqual(originalModel?.uri, entry.originalURI) && isEqual(modifiedModel?.uri, entry.modifiedURI); +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 8a2ac519e78..76f056f72b0 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -28,7 +28,7 @@ import { GroupsOrder, IEditorGroupsService } from '../../../../services/editor/c import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, chatEditingWidgetFileReadonlyContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatService } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; @@ -64,6 +64,35 @@ abstract class WorkingSetAction extends Action2 { abstract runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any; } +registerAction2(class MarkFileAsReadonly extends WorkingSetAction { + constructor() { + super({ + id: 'chatEditing.markFileAsReadonly', + title: localize2('markFileAsReadonly', 'Mark as read-only'), + icon: Codicon.lock, + toggled: chatEditingWidgetFileReadonlyContextKey, + menu: [{ + id: MenuId.ChatEditingWidgetModifiedFilesToolbar, + when: ContextKeyExpr.and( + chatEditingAgentSupportsReadonlyReferencesContextKey, + ContextKeyExpr.or( + ContextKeyExpr.equals(chatEditingWidgetFileReadonlyContextKey.key, true), + ContextKeyExpr.equals(chatEditingWidgetFileReadonlyContextKey.key, false), + ) + ), + order: 10, + group: 'navigation' + }], + }); + } + + async runWorkingSetAction(_accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise { + for (const uri of uris) { + currentEditingSession.markIsReadonly(uri); + } + } +}); + registerAction2(class AddFileToWorkingSet extends WorkingSetAction { constructor() { super({ @@ -102,7 +131,27 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { } async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise { + const dialogService = accessor.get(IDialogService); + + const pendingEntries = currentEditingSession.entries.get().filter((entry) => uris.includes(entry.modifiedURI) && entry.state.get() === WorkingSetEntryState.Modified); + if (pendingEntries.length > 0) { + // Ask for confirmation if there are any pending edits + const file = pendingEntries.length > 1 + ? localize('chat.editing.removeFile.confirmationmanyFiles', "{0} files", pendingEntries.length) + : basename(pendingEntries[0].modifiedURI); + const confirmation = await dialogService.confirm({ + title: localize('chat.editing.removeFile.confirmation.title', "Remove {0} from working set?", file), + message: localize('chat.editing.removeFile.confirmation.message', "This will remove {0} from your working set and undo the edits made to it. Do you want to proceed?", file), + primaryButton: localize('chat.editing.removeFile.confirmation.primaryButton', "Yes"), + type: 'info' + }); + if (!confirmation.confirmed) { + return; + } + } + // Remove from working set + await currentEditingSession.reject(...uris); currentEditingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); // Remove from chat input part @@ -274,33 +323,38 @@ export class ChatEditingDiscardAllAction extends Action2 { } async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const dialogService = accessor.get(IDialogService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } + await discardAllEditsWithConfirmation(accessor); + } +} +registerAction2(ChatEditingDiscardAllAction); - // Ask for confirmation if there are any edits - const entries = currentEditingSession.entries.get(); - if (entries.length > 0) { - const confirmation = await dialogService.confirm({ - title: localize('chat.editing.discardAll.confirmation.title', "Discard all edits?"), - message: entries.length === 1 - ? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made by {0} in {1}. Do you want to proceed?", 'Copilot Edits', basename(entries[0].modifiedURI)) - : localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made by {0} in {1} files. Do you want to proceed?", 'Copilot Edits', entries.length), - primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"), - type: 'info' - }); - if (!confirmation.confirmed) { - return; - } - } +export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const dialogService = accessor.get(IDialogService); + const currentEditingSession = chatEditingService.currentEditingSession; + if (!currentEditingSession) { + return false; + } - await currentEditingSession.reject(); + // Ask for confirmation if there are any edits + const entries = currentEditingSession.entries.get(); + if (entries.length > 0) { + const confirmation = await dialogService.confirm({ + title: localize('chat.editing.discardAll.confirmation.title', "Discard all edits?"), + message: entries.length === 1 + ? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made by {0} in {1}. Do you want to proceed?", 'Copilot Edits', basename(entries[0].modifiedURI)) + : localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made by {0} in {1} files. Do you want to proceed?", 'Copilot Edits', entries.length), + primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"), + type: 'info' + }); + if (!confirmation.confirmed) { + return false; + } } + + await currentEditingSession.reject(); + return true; } -registerAction2(ChatEditingDiscardAllAction); export class ChatEditingRemoveAllFilesAction extends Action2 { static readonly ID = 'chatEditing.clearWorkingSet'; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 37a6b9a2b68..149317c127d 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -7,7 +7,8 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { clamp } from '../../../../../base/common/numbers.js'; +import { autorun, derived, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; @@ -26,7 +27,9 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { SaveReason } from '../../../../common/editor.js'; @@ -36,6 +39,14 @@ import { ChatEditKind, IModifiedFileEntry, WorkingSetEntryState } from '../../co import { IChatService } from '../../common/chatService.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; +class AutoAcceptControl { + constructor( + readonly total: number, + readonly remaining: number, + readonly cancel: () => void + ) { } +} + export class ChatEditingModifiedFileEntry extends Disposable implements IModifiedFileEntry { public static readonly scheme = 'modified-file-entry'; @@ -89,6 +100,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this._maxLineNumberObs; } + private readonly _reviewModeTempObs = observableValue(this, undefined); + readonly reviewMode: IObservable; + + private readonly _autoAcceptCtrl = observableValue(this, undefined); + readonly autoAcceptController: IObservable = this._autoAcceptCtrl; + private _isFirstEditAfterStartOrSnapshot: boolean = true; private _edit: OffsetEdit = OffsetEdit.empty; private _isEditFromUs: boolean = false; @@ -130,6 +147,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this._telemetryInfo.requestId; } + private readonly _diffTrimWhitespace: IObservable; + + private _refCounter: number = 1; + + private readonly _autoAcceptTimeout: IObservable; + constructor( resourceRef: IReference, private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, @@ -139,6 +162,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie @IModelService modelService: IModelService, @ITextModelService textModelService: ITextModelService, @ILanguageService languageService: ILanguageService, + @IConfigurationService configService: IConfigurationService, @IChatService private readonly _chatService: IChatService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @@ -174,7 +198,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._register(this.doc.onDidChangeContent(e => this._mirrorEdits(e))); - if (this.modifiedURI.scheme !== Schemas.untitled) { + if (this.modifiedURI.scheme !== Schemas.untitled && this.modifiedURI.scheme !== Schemas.vscodeNotebookCell) { this._register(this._fileService.watch(this.modifiedURI)); this._register(this._fileService.onDidFilesChange(e => { if (e.affects(this.modifiedURI) && kind === ChatEditKind.Created && e.gotDeleted()) { @@ -186,6 +210,51 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._register(toDisposable(() => { this._clearCurrentEditLineDecoration(); })); + + this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); + this._register(autorun(r => { + this._diffTrimWhitespace.read(r); + this._updateDiffInfoSeq(); + })); + + // review mode depends on setting and temporary override + const autoAcceptRaw = observableConfigValue('chat.editing.autoAcceptDelay', 0, configService); + this._autoAcceptTimeout = derived(r => { + const value = autoAcceptRaw.read(r); + return clamp(value, 0, 100); + }); + this.reviewMode = derived(r => { + const configuredValue = this._autoAcceptTimeout.read(r); + const tempValue = this._reviewModeTempObs.read(r); + return tempValue ?? configuredValue === 0; + }); + } + + override dispose(): void { + if (--this._refCounter === 0) { + super.dispose(); + } + } + + acquire() { + this._refCounter++; + return this; + } + + enableReviewModeUntilSettled(): void { + + this._reviewModeTempObs.set(true, undefined); + + const cleanup = autorun(r => { + // reset config when settled + const resetConfig = this.state.read(r) !== WorkingSetEntryState.Modified; + if (resetConfig) { + this._store.delete(cleanup); + this._reviewModeTempObs.set(undefined, undefined); + } + }); + + this._store.add(cleanup); } private _clearCurrentEditLineDecoration() { @@ -234,6 +303,34 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._isCurrentlyBeingModifiedObs.set(false, tx); this._rewriteRatioObs.set(0, tx); this._clearCurrentEditLineDecoration(); + + // AUTO accept mode + if (!this.reviewMode.get() && !this._autoAcceptCtrl.get()) { + + const acceptTimeout = this._autoAcceptTimeout.get() * 1000; + const future = Date.now() + acceptTimeout; + const update = () => { + + const reviewMode = this.reviewMode.get(); + if (reviewMode) { + // switched back to review mode + this._autoAcceptCtrl.set(undefined, undefined); + return; + } + + const remain = Math.round(future - Date.now()); + if (remain <= 0) { + this.accept(undefined); + } else { + const handle = setTimeout(update, 100); + this._autoAcceptCtrl.set(new AutoAcceptControl(acceptTimeout, remain, () => { + clearTimeout(handle); + this._autoAcceptCtrl.set(undefined, undefined); + }), undefined); + } + }; + update(); + } } private _mirrorEdits(event: IModelContentChangedEvent) { @@ -413,10 +510,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie const docVersionNow = this.doc.getVersionId(); const snapshotVersionNow = this.docSnapshot.getVersionId(); + const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); + const diff = await this._editorWorkerService.computeDiff( this.docSnapshot.uri, this.doc.uri, - { computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: 3000 }, + { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, 'advanced' ); @@ -442,6 +541,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._diffInfo.set(nullDocumentDiff, transaction); this._edit = OffsetEdit.empty; this._stateObs.set(WorkingSetEntryState.Accepted, transaction); + this._autoAcceptCtrl.set(undefined, transaction); await this.collapse(transaction); this._notifyAction('accepted'); } @@ -466,6 +566,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie await this.collapse(transaction); } this._stateObs.set(WorkingSetEntryState.Rejected, transaction); + this._autoAcceptCtrl.set(undefined, transaction); this._notifyAction('rejected'); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index d6cc236478a..4e2854a7b37 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -9,6 +9,7 @@ import { ILanguageService } from '../../../../../editor/common/languages/languag import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { SaveReason } from '../../../../common/editor.js'; @@ -33,8 +34,9 @@ export class ChatEditingModifiedNotebookEntry extends ChatEditingModifiedFileEnt @IEditorWorkerService _editorWorkerService: IEditorWorkerService, @IUndoRedoService _undoRedoService: IUndoRedoService, @IFileService _fileService: IFileService, + @IConfigurationService configService: IConfigurationService ) { - super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, _chatService, _editorWorkerService, _undoRedoService, _fileService); + super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, configService, _chatService, _editorWorkerService, _undoRedoService, _fileService); this.resolveTextFileEditorModel = resourceRef.object as IResolvedTextFileEditorModel; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index f93b829cf14..1aba7d7c8cd 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -7,35 +7,41 @@ import { coalesce, compareBy, delta } from '../../../../../base/common/arrays.js import { AsyncIterableSource } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { BugIndicatingError } from '../../../../../base/common/errors.js'; +import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { LinkedList } from '../../../../../base/common/linkedList.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { derived, IObservable, observableValue, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { derived, IObservable, observableValue, observableValueOpts, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isString } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { localize, localize2 } from '../../../../../nls.js'; +import { localize } from '../../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; +import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -49,6 +55,17 @@ export class ChatEditingService extends Disposable implements IChatEditingServic private readonly _currentSessionObs = observableValue(this, null); private readonly _currentSessionDisposables = this._register(new DisposableStore()); + private readonly _adhocSessionsObs = observableValueOpts>({ equalsFn: (a, b) => false }, new LinkedList()); + + readonly editingSessionsObs: IObservable = derived(r => { + const result = Array.from(this._adhocSessionsObs.read(r)); + const globalSession = this._currentSessionObs.read(r); + if (globalSession) { + result.push(globalSession); + } + return result; + }); + private readonly _currentAutoApplyOperationObs = observableValue(this, null); get currentAutoApplyOperation(): CancellationTokenSource | null { return this._currentAutoApplyOperationObs.get(); @@ -62,12 +79,13 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._currentSessionObs; } - private readonly _onDidChangeEditingSession = this._register(new Emitter()); - public readonly onDidChangeEditingSession = this._onDidChangeEditingSession.event; - private _editingSessionFileLimitPromise: Promise; private _editingSessionFileLimit: number | undefined; get editingSessionFileLimit() { + if (this._chatAgentService.toolsAgentModeEnabled) { + return Number.MAX_SAFE_INTEGER; + } + return this._editingSessionFileLimit ?? defaultChatEditingMaxFileLimit; } @@ -83,14 +101,16 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @ITextModelService textModelService: ITextModelService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, - @IProgressService private readonly _progressService: IProgressService, @IEditorService private readonly _editorService: IEditorService, @IDecorationsService decorationsService: IDecorationsService, @IFileService private readonly _fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchAssignmentService private readonly _workbenchAssignmentService: IWorkbenchAssignmentService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IStorageService storageService: IStorageService, @ILogService logService: ILogService, + @IExtensionService extensionService: IExtensionService, + @IProductService productService: IProductService, ) { super(); this._applyingChatEditsFailedContextKey = applyingChatEditsFailedContextKey.bindTo(contextKeyService); @@ -109,13 +129,14 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return decidedEntries.map(entry => entry.entryId); })); this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { - const currentSession = this._currentSessionObs.read(reader); - if (!currentSession) { - return; + + for (const session of this.editingSessionsObs.read(reader)) { + const entries = session.entries.read(reader); + const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); + return decidedEntries.length > 0; } - const entries = currentSession.entries.read(reader); - const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); - return decidedEntries.length > 0; + + return false; })); this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { const currentSession = this._currentSessionObs.read(reader); @@ -144,6 +165,16 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } })); + // todo@connor4312: temporary until chatReadonlyPromptReference proposal is finalized + const readonlyEnabledContextKey = chatEditingAgentSupportsReadonlyReferencesContextKey.bindTo(contextKeyService); + const setReadonlyFilesEnabled = () => { + const enabled = productService.quality !== 'stable' && extensionService.extensions.some(e => e.enabledApiProposals?.includes('chatReadonlyPromptReference')); + readonlyEnabledContextKey.set(enabled); + }; + setReadonlyFilesEnabled(); + this._register(extensionService.onDidRegisterExtensions(setReadonlyFilesEnabled)); + this._register(extensionService.onDidChangeExtensions(setReadonlyFilesEnabled)); + this._register(this.lifecycleService.onWillShutdown((e) => { const session = this._currentSessionObs.get(); if (session) { @@ -199,6 +230,18 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } + private _lookupEntry(uri: URI): ChatEditingModifiedFileEntry | undefined { + + for (const item of Iterable.concat(this.editingSessionsObs.get())) { + const candidate = item.getEntry(uri); + if (candidate instanceof ChatEditingModifiedFileEntry) { + // make sure to ref-count this object + return candidate.acquire(); + } + } + return undefined; + } + private async _createEditingSession(chatSessionId: string): Promise { if (this._currentSessionObs.get()) { throw new BugIndicatingError('Cannot have more than one active editing session'); @@ -206,7 +249,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.clear(); - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, true, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); // listen for completed responses, run the code mapper and apply the edits to this edit session @@ -215,14 +258,33 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.add(session.onDidDispose(() => { this._currentSessionDisposables.clear(); this._currentSessionObs.set(null, undefined); - this._onDidChangeEditingSession.fire(); - })); - this._currentSessionDisposables.add(session.onDidChange(() => { - this._onDidChangeEditingSession.fire(); })); this._currentSessionObs.set(session, undefined); - this._onDidChangeEditingSession.fire(); + return session; + } + + async createAdhocEditingSession(chatSessionId: string): Promise { + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, false, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); + await session.init(); + + const list = this._adhocSessionsObs.get(); + const removeSession = list.unshift(session); + + const store = new DisposableStore(); + this._store.add(store); + + store.add(this.installAutoApplyObserver(session)); + + store.add(session.onDidDispose(e => { + removeSession(); + this._adhocSessionsObs.set(list, undefined); + this._store.deleteAndLeak(store); + store.dispose(); + })); + + this._adhocSessionsObs.set(list, undefined); + return session; } @@ -230,7 +292,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const chatModel = this._chatService.getOrRestoreSession(session.chatSessionId); if (!chatModel) { - throw new Error(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); + throw new ErrorNoTelemetry(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); } const observerDisposables = new DisposableStore(); @@ -259,9 +321,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic if (part.kind === 'codeblockUri' || part.kind === 'textEditGroup') { // ensure editor is open asap if (!editedFilesExist.get(part.uri)) { - editedFilesExist.set(part.uri, this._fileService.exists(part.uri).then((e) => { + const uri = part.uri.scheme === Schemas.vscodeNotebookCell ? CellUri.parse(part.uri)?.notebook ?? part.uri : part.uri; + editedFilesExist.set(part.uri, this._fileService.exists(uri).then((e) => { if (e) { - this._editorService.openEditor({ resource: part.uri, options: { inactive: true, preserveFocus: true, pinned: true } }); + this._editorService.openEditor({ resource: uri, options: { inactive: true, preserveFocus: true, pinned: true } }); } return e; })); @@ -291,6 +354,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editsPromise = this._continueEditingSession(session, async (builder, token) => { for await (const item of editsSource!.asyncIterable) { + if (responseModel.isCanceled) { + break; + } if (token.isCancellationRequested) { break; } @@ -352,15 +418,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const cancellationTokenSource = new CancellationTokenSource(); this._currentAutoApplyOperationObs.set(cancellationTokenSource, undefined); try { - await this._progressService.withProgress({ - location: ProgressLocation.Window, - title: localize2('chatEditing.startingSession', 'Generating edits...').value, - }, async () => { - await builder(stream, cancellationTokenSource.token); - }, - () => cancellationTokenSource.cancel() - ); - + await builder(stream, cancellationTokenSource.token); } finally { cancellationTokenSource.dispose(); this._currentAutoApplyOperationObs.set(null, undefined); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 1b5516e070e..a389cb51270 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -4,14 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { ITask, Sequencer, timeout } from '../../../../../base/common/async.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; +import { StringSHA1 } from '../../../../../base/common/hash.js'; +import { Disposable, DisposableMap, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js'; +import { isEqual, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; +import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; @@ -19,29 +25,27 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IEditorCloseEvent } from '../../../../common/editor.js'; +import { IEditorCloseEvent, SaveReason } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; +import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; -import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; -import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; -import { isEqual, joinPath } from '../../../../../base/common/resources.js'; -import { StringSHA1 } from '../../../../../base/common/hash.js'; -import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; import { IChatService } from '../../common/chatService.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; -import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; +import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; @@ -155,14 +159,16 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return this._onDidDispose.event; } - get isVisible(): boolean { - this._assertNotDisposed(); - return Boolean(this._editorPane && this._editorPane.isVisible()); + private _isToolsAgentSession = false; + get isToolsAgentSession(): boolean { + return this._isToolsAgentSession; } constructor( - public readonly chatSessionId: string, + readonly chatSessionId: string, + readonly isGlobalEditingSession: boolean, private editingSessionFileLimitPromise: Promise, + private _lookupExternalEntry: (uri: URI) => ChatEditingModifiedFileEntry | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @@ -172,6 +178,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IEditorService private readonly _editorService: IEditorService, @IChatService private readonly _chatService: IChatService, @INotebookService private readonly _notebookService: INotebookService, + @ITextFileService private readonly _textFileService: ITextFileService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { super(); } @@ -191,6 +199,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // Add the currently active editors to the working set this._trackCurrentEditorsInWorkingSet(); + this._triggerSaveParticipantsOnAccept(); this._register(this._editorService.onDidVisibleEditorsChange(() => { this._trackCurrentEditorsInWorkingSet(); })); @@ -223,6 +232,39 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return storage.storeState(state); } + private _triggerSaveParticipantsOnAccept() { + const im = this._register(new DisposableMap()); + const attachToEntry = (entry: ChatEditingModifiedFileEntry) => { + return autorunDelta(entry.state, ({ lastValue, newValue }) => { + if (newValue === WorkingSetEntryState.Accepted && lastValue === WorkingSetEntryState.Modified) { + // Don't save a file if there's still pending changes. If there's not (e.g. + // the agentic flow with autosave) then save again to trigger participants. + if (!this._textFileService.isDirty(entry.modifiedURI)) { + this._textFileService.save(entry.modifiedURI, { + reason: SaveReason.EXPLICIT, + force: true, + ignoreErrorHandler: true, + }).catch(() => { + // ignored + }); + } + } + }); + }; + + this._register(autorunIterableDelta( + reader => this._entriesObs.read(reader), + ({ addedValues, removedValues }) => { + for (const entry of addedValues) { + im.set(entry, attachToEntry(entry)); + } + for (const entry of removedValues) { + im.deleteAndDispose(entry); + } + } + )); + } + private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) { const existingTransientEntries = new ResourceSet(); for (const file of this._workingSet.keys()) { @@ -284,7 +326,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio if (requestId) { for (const [uri, data] of this._workingSet) { if (data.state !== WorkingSetEntryState.Suggested) { - this._workingSet.set(uri, { state: WorkingSetEntryState.Sent }); + this._workingSet.set(uri, { state: WorkingSetEntryState.Sent, isMarkedReadonly: data.isMarkedReadonly }); } } const linearHistory = this._linearHistory.get(); @@ -420,6 +462,22 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } + markIsReadonly(resource: URI, isReadonly?: boolean): void { + const entry = this._workingSet.get(resource); + if (entry) { + if (entry.state === WorkingSetEntryState.Transient || entry.state === WorkingSetEntryState.Suggested) { + entry.state = WorkingSetEntryState.Attached; + } + entry.isMarkedReadonly = isReadonly ?? !entry.isMarkedReadonly; + } else { + this._workingSet.set(resource, { + state: WorkingSetEntryState.Attached, + isMarkedReadonly: isReadonly ?? true + }); + } + this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); + } + private _assertNotDisposed(): void { if (this._state.get() === ChatEditingSessionState.Disposed) { throw new BugIndicatingError(`Cannot access a disposed editing session`); @@ -543,6 +601,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return; } + this._isToolsAgentSession = !!responseModel.agent?.isToolsAgent; + // ensure that the edits are processed sequentially this._sequencer.queue(() => this._acceptTextEdits(resource, textEdits, isLastEdits, responseModel)); } @@ -557,6 +617,45 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._sequencer.queue(() => this._resolve()); } + private _trackUntitledWorkingSetEntry(resource: URI) { + if (resource.scheme !== Schemas.untitled) { + return; + } + const untitled = this._textFileService.untitled.get(resource); + if (!untitled) { // Shouldn't happen + return; + } + + // Track this file until + // 1. it is removed from the working set + // 2. it is closed + // 3. we are disposed + const store = new DisposableStore(); + store.add(this.onDidChange(e => { + if (e === ChatEditingSessionChangeType.WorkingSet && !this._workingSet.get(resource)) { + // The user has removed the file from the working set + store.dispose(); + } + })); + store.add(this._textFileService.untitled.onDidSave(e => { + const existing = this._workingSet.get(resource); + if (isEqual(e.source, resource) && existing) { + this._workingSet.delete(resource); + this._workingSet.set(e.target, existing); + store.dispose(); + this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); + } + })); + store.add(this._editorService.onDidCloseEditor((e) => { + if (isEqual(e.editor.resource, resource)) { + this._workingSet.delete(resource); + store.dispose(); + this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); + } + })); + this._store.add(store); + } + addFileToWorkingSet(resource: URI, description?: string, proposedState?: WorkingSetEntryState.Suggested): void { const state = this._workingSet.get(resource); if (proposedState === WorkingSetEntryState.Suggested) { @@ -564,9 +663,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return; } this._workingSet.set(resource, { description, state: WorkingSetEntryState.Suggested }); + this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } else if (state === undefined || state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested) { this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached }); + this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } } @@ -614,8 +715,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _acceptTextEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { - if (!this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)) && this._entriesObs.get().length >= (await this.editingSessionFileLimitPromise)) { - // Do not create files in a single editing session that would be in excess of our limit + if (!this._chatAgentService.toolsAgentModeEnabled && !this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)) && this._entriesObs.get().length >= (await this.editingSessionFileLimitPromise)) { + // Do not create files in a single editing session that would be in excess of our limit. + // TODO- The agent mode check is done weirdly here because we don't know whether agent mode is enabled or not at the moment the chat editing session is created when the window is loading. + // Expecting that the limit will be removed soon anyway... return; } @@ -641,6 +744,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.Other); } + /** + * Retrieves or creates a modified file entry. + * + * @returns The modified file entry. + */ private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise { const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)); if (existingEntry) { @@ -649,23 +757,39 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } return existingEntry; } - const initialContent = this._initialFileContents.get(resource); - // This gets manually disposed in .dispose() or in .restoreSnapshot() - const entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); - if (!initialContent) { - this._initialFileContents.set(resource, entry.initialContent); + + let entry: ChatEditingModifiedFileEntry; + const existingExternalEntry = this._lookupExternalEntry(resource); + if (existingExternalEntry) { + entry = existingExternalEntry; + } else { + const initialContent = this._initialFileContents.get(resource); + // This gets manually disposed in .dispose() or in .restoreSnapshot() + entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); + if (!initialContent) { + this._initialFileContents.set(resource, entry.initialContent); + } } + // If an entry is deleted e.g. reverting a created file, // remove it from the entries and don't show it in the working set anymore // so that it can be recreated e.g. through retry - this._register(entry.onDidDelete(() => { + const listener = entry.onDidDelete(() => { const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI)); this._entriesObs.set(newEntries, undefined); this._workingSet.delete(entry.modifiedURI); this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI)); - entry.dispose(); + + if (!existingExternalEntry) { + // don't dispose entries that are not yours! + entry.dispose(); + } + + this._store.delete(listener); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - })); + }); + this._store.add(listener); + const entriesArr = [...this._entriesObs.get(), entry]; this._entriesObs.set(entriesArr, undefined); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 731a921c0ca..94e831bd1ad 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -2,15 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { localize2 } from '../../../../nls.js'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { localize, localize2 } from '../../../../nls.js'; import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { ChatEditorController, ctxHasEditorModification, ctxHasRequestInProgress } from './chatEditorController.js'; +import { ChatEditorController, ctxHasEditorModification, ctxReviewModeEnabled } from './chatEditorController.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; @@ -37,14 +37,18 @@ abstract class NavigateAction extends Action2 { primary: next ? KeyMod.Alt | KeyCode.F5 : KeyMod.Alt | KeyMod.Shift | KeyCode.F5, - weight: KeybindingWeight.EditorContrib, - when: ContextKeyExpr.and(ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification), EditorContextKeys.focus), + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and( + ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification), + EditorContextKeys.focus + ), }, f1: true, menu: { id: MenuId.ChatEditingEditorContent, group: 'navigate', order: !next ? 2 : 3, + when: ctxReviewModeEnabled } }); } @@ -54,18 +58,22 @@ abstract class NavigateAction extends Action2 { const chatEditingService = accessor.get(IChatEditingService); const editorService = accessor.get(IEditorService); - const editor = editorService.activeTextEditorControl; + let editor = editorService.activeTextEditorControl; + if (isDiffEditor(editor)) { + editor = editor.getModifiedEditor(); + } if (!isCodeEditor(editor) || !editor.hasModel()) { return; } - - const session = chatEditingService.currentEditingSession; - if (!session) { + const ctrl = ChatEditorController.get(editor); + if (!ctrl) { return; } - const ctrl = ChatEditorController.get(editor); - if (!ctrl) { + const session = chatEditingService.editingSessionsObs.get() + .find(candidate => candidate.getEntry(editor.getModel().uri)); + + if (!session) { return; } @@ -126,7 +134,7 @@ abstract class AcceptDiscardAction extends Action2 { ? localize2('accept2', 'Accept') : localize2('discard2', 'Discard'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ctxHasRequestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, hasUndecidedChatEditingResourceContextKey), icon: accept ? Codicon.check : Codicon.discard, @@ -142,6 +150,7 @@ abstract class AcceptDiscardAction extends Action2 { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: accept ? 0 : 1, + when: !accept ? ctxReviewModeEnabled : undefined } }); } @@ -152,14 +161,21 @@ abstract class AcceptDiscardAction extends Action2 { let uri = getNotebookEditorFromEditorPane(editorService.activeEditorPane)?.textModel?.uri; if (!uri) { - const editor = editorService.activeTextEditorControl; - uri = isCodeEditor(editor) && editor.hasModel() ? editor.getModel().uri : undefined; + let editor = editorService.activeTextEditorControl; + if (isDiffEditor(editor)) { + editor = editor.getModifiedEditor(); + } + uri = isCodeEditor(editor) && editor.hasModel() + ? editor.getModel().uri + : undefined; } if (!uri) { return; } - const session = chatEditingService.currentEditingSession; + const session = chatEditingService.editingSessionsObs.get() + .find(candidate => candidate.getEntry(uri)); + if (!session) { return; } @@ -190,14 +206,13 @@ export class RejectAction extends AcceptDiscardAction { } } -class UndoHunkAction extends EditorAction2 { +class RejectHunkAction extends EditorAction2 { constructor() { super({ id: 'chatEditor.action.undoHunk', - title: localize2('undo', 'Undo this Change'), - shortTitle: localize2('undo2', 'Undo'), + title: localize2('undo', 'Discard this Change'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.discard, f1: true, keybinding: { @@ -213,7 +228,7 @@ class UndoHunkAction extends EditorAction2 { } override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { - ChatEditorController.get(editor)?.undoNearestChange(args[0]); + ChatEditorController.get(editor)?.rejectNearestChange(args[0]); } } @@ -222,9 +237,8 @@ class AcceptHunkAction extends EditorAction2 { super({ id: 'chatEditor.action.acceptHunk', title: localize2('acceptHunk', 'Accept this Change'), - shortTitle: localize2('acceptHunk2', 'Accept'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.check, f1: true, keybinding: { @@ -244,32 +258,88 @@ class AcceptHunkAction extends EditorAction2 { } } -class OpenDiffFromHunkAction extends EditorAction2 { +class OpenDiffAction extends EditorAction2 { constructor() { super({ id: 'chatEditor.action.diffHunk', - title: localize2('diff', 'Open Diff'), + title: localize2('diff', 'Toggle Diff Editor'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + toggled: { + condition: EditorContextKeys.inDiffEditor, + icon: Codicon.goToFile, + }, + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.diffSingle, - menu: { + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F7, + }, + menu: [{ id: MenuId.ChatEditingEditorHunk, order: 10 - } + }, { + id: MenuId.ChatEditingEditorContent, + group: 'a_resolve', + order: 2, + when: ctxReviewModeEnabled + }] }); } override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { - ChatEditorController.get(editor)?.openDiff(args[0]); + ChatEditorController.get(editor)?.toggleDiff(args[0]); + } +} + +export class ReviewChangesAction extends EditorAction2 { + + constructor() { + super({ + id: 'chatEditor.action.reviewChanges', + title: localize2('review', "Review"), + menu: [{ + id: MenuId.ChatEditingEditorContent, + group: 'a_resolve', + order: 3, + when: ctxReviewModeEnabled.negate(), + }] + }); + } + + override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) { + const chatEditingService = accessor.get(IChatEditingService); + + if (!editor.hasModel()) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(session => session.getEntry(editor.getModel().uri)); + const entry = session?.getEntry(editor.getModel().uri); + entry?.enableReviewModeUntilSettled(); } } export function registerChatEditorActions() { registerAction2(class NextAction extends NavigateAction { constructor() { super(true); } }); registerAction2(class PrevAction extends NavigateAction { constructor() { super(false); } }); + registerAction2(ReviewChangesAction); registerAction2(AcceptAction); - registerAction2(RejectAction); - registerAction2(UndoHunkAction); registerAction2(AcceptHunkAction); - registerAction2(OpenDiffFromHunkAction); + registerAction2(RejectAction); + registerAction2(RejectHunkAction); + registerAction2(OpenDiffAction); + + MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { + command: { + id: navigationBearingFakeActionId, + title: localize('label', "Navigation Status"), + precondition: ContextKeyExpr.false(), + }, + group: 'navigate', + order: -1, + when: ctxReviewModeEnabled, + }); } + +export const navigationBearingFakeActionId = 'chatEditor.navigation.bearings'; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index bf0c199267f..77d55bbbf1c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -6,12 +6,12 @@ import './media/chatEditorController.css'; import { addStandardDisposableListener, getTotalWidth } from '../../../../base/browser/dom.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; -import { EditorOption, IEditorStickyScrollOptions } from '../../../../editor/common/config/editorOptions.js'; +import { EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js'; @@ -31,9 +31,17 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/a import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; - +import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; +import { basename, isEqual } from '../../../../base/common/resources.js'; +import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; +import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; +import { ChatEditorOverlayController } from './chatEditorOverlay.js'; +import { IChatService } from '../common/chatService.js'; + +export const ctxIsGlobalEditingSession = new RawContextKey('chat.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); +export const ctxReviewModeEnabled = new RawContextKey('chat.ctxReviewModeEnabled', true, localize('chat.ctxReviewModeEnabled', "Review mode for chat changes is enabled")); export class ChatEditorController extends Disposable implements IEditorContribution { @@ -47,8 +55,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut private readonly _diffHunkWidgets: DiffHunkWidget[] = []; private _viewZones: string[] = []; + + private readonly _overlayCtrl: ChatEditorOverlayController; + + private readonly _ctxIsGlobalEditsSession: IContextKey; private readonly _ctxHasEditorModification: IContextKey; private readonly _ctxRequestInProgress: IContextKey; + private readonly _ctxReviewModelEnabled: IContextKey; static get(editor: ICodeEditor): ChatEditorController | null { const controller = editor.getContribution(ChatEditorController.ID); @@ -65,38 +78,54 @@ export class ChatEditorController extends Disposable implements IEditorContribut constructor( private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IContextKeyService contextKeyService: IContextKeyService, + @IChatService chatService: IChatService, ) { super(); + this._overlayCtrl = ChatEditorOverlayController.get(_editor)!; + this._ctxIsGlobalEditsSession = ctxIsGlobalEditingSession.bindTo(contextKeyService); this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService); this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService); + this._ctxReviewModelEnabled = ctxReviewModeEnabled.bindTo(contextKeyService); - const fontInfoObs = observableCodeEditor(this._editor).getOption(EditorOption.fontInfo); - const lineHeightObs = observableCodeEditor(this._editor).getOption(EditorOption.lineHeight); - const modelObs = observableCodeEditor(this._editor).model; - + const editorObs = observableCodeEditor(this._editor); + const fontInfoObs = editorObs.getOption(EditorOption.fontInfo); + const lineHeightObs = editorObs.getOption(EditorOption.lineHeight); + const modelObs = editorObs.model; this._store.add(autorun(r => { - const session = this._chatEditingService.currentEditingSessionObs.read(r); - this._ctxRequestInProgress.set(session?.state.read(r) === ChatEditingSessionState.StreamingEdits); + let isStreamingEdits = false; + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + isStreamingEdits ||= session.state.read(r) === ChatEditingSessionState.StreamingEdits; + } + this._ctxRequestInProgress.set(isStreamingEdits); })); - const entryForEditor = derived(r => { const model = modelObs.read(r); - const session = this._chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return undefined; + if (!model) { + return; } - const entry = model?.uri ? session.readEntry(model.uri, r) : undefined; - if (!entry || entry.state.read(r) !== WorkingSetEntryState.Modified) { - return undefined; + + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + const entries = session.entries.read(r); + const idx = model?.uri + ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) + : -1; + + const chatModel = chatService.getSession(session.chatSessionId); + + if (idx >= 0 && chatModel) { + return { session, chatModel, entry: entries[idx], entries, idx }; + } } - return { session, entry }; + + return undefined; }); @@ -104,24 +133,49 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._register(autorunWithStore((r, store) => { - if (this._editor.getOption(EditorOption.inDiffEditor)) { - this._clearRendering(); - return; - } - const currentEditorEntry = entryForEditor.read(r); if (!currentEditorEntry) { - this._clearRendering(); + this._ctxIsGlobalEditsSession.reset(); + this._clear(); didReval = false; return; } - const { session, entry } = currentEditorEntry; + if (this._editor.getOption(EditorOption.inDiffEditor) && !_instantiationService.invokeFunction(isDiffEditorForEntry, currentEditorEntry.entry, this._editor)) { + this._clear(); + return; + } + + const { session, chatModel, entries, idx, entry } = currentEditorEntry; + + store.add(chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + didReval = false; + } + })); + + this._ctxIsGlobalEditsSession.set(session.isGlobalEditingSession); + this._ctxReviewModelEnabled.set(entry.reviewMode.read(r)); - const entryIndex = session.entries.read(r).indexOf(entry); - this._currentEntryIndex.set(entryIndex, undefined); + // context + this._currentEntryIndex.set(idx, undefined); + // overlay widget + if (entry.state.read(r) !== WorkingSetEntryState.Modified) { + this._overlayCtrl.hide(); + } else { + this._overlayCtrl.showEntry( + session, + entry, entries[(idx + 1) % entries.length], + { + entryIndex: this._currentEntryIndex, + changeIndex: this._currentChangeIndex + } + ); + } + + // scrolling logic if (entry.isCurrentlyBeingModified.read(r)) { // while modified: scroll along unless locked if (!this._scrollLock) { @@ -140,11 +194,22 @@ export class ChatEditorController extends Disposable implements IEditorContribut lineHeightObs.read(r); const diff = entry?.diffInfo.read(r); - this._updateWithDiff(entry, diff); + + // Add line decorations (just markers, no UI) for diff navigation + this._updateDiffLineDecorations(diff); + + const reviewMode = entry.reviewMode.read(r); + + // Add diff decoration to the UI (unless in diff editor) + if (!this._editor.getOption(EditorOption.inDiffEditor)) { + this._updateDiffRendering(entry, diff, reviewMode); + } else { + this._clearDiffRendering(); + } if (!didReval && !diff.identical) { didReval = true; - this.revealNext(); + this._reveal(true, false, ScrollType.Immediate); } } })); @@ -152,26 +217,31 @@ export class ChatEditorController extends Disposable implements IEditorContribut // ---- readonly while streaming const shouldBeReadOnly = derived(this, r => { - const value = this._chatEditingService.currentEditingSessionObs.read(r); - if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) { - return false; - } const model = modelObs.read(r); - return model ? value.readEntry(model.uri, r) : undefined; + if (!model) { + return undefined; + } + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + if (session.readEntry(model.uri, r) && session.state.read(r) === ChatEditingSessionState.StreamingEdits) { + return true; + } + } + return false; }); - let actualReadonly: boolean | undefined; - let actualDeco: 'off' | 'editable' | 'on' | undefined; - let actualStickyScroll: IEditorStickyScrollOptions | undefined; + let actualOptions: IEditorOptions | undefined; this._register(autorun(r => { const value = shouldBeReadOnly.read(r); if (value) { - actualReadonly ??= this._editor.getOption(EditorOption.readOnly); - actualDeco ??= this._editor.getOption(EditorOption.renderValidationDecorations); - actualStickyScroll ??= this._editor.getOption(EditorOption.stickyScroll); + + actualOptions ??= { + readOnly: this._editor.getOption(EditorOption.readOnly), + renderValidationDecorations: this._editor.getOption(EditorOption.renderValidationDecorations), + stickyScroll: this._editor.getOption(EditorOption.stickyScroll) + }; this._editor.updateOptions({ readOnly: true, @@ -179,26 +249,30 @@ export class ChatEditorController extends Disposable implements IEditorContribut stickyScroll: { enabled: false } }); } else { - if (actualReadonly !== undefined && actualDeco !== undefined && actualStickyScroll !== undefined) { - this._editor.updateOptions({ - readOnly: actualReadonly, - renderValidationDecorations: actualDeco, - stickyScroll: actualStickyScroll - }); - actualReadonly = undefined; - actualDeco = undefined; - actualStickyScroll = undefined; + if (actualOptions !== undefined) { + this._editor.updateOptions(actualOptions); + actualOptions = undefined; } } })); } override dispose(): void { - this._clearRendering(); + this._clear(); super.dispose(); } - private _clearRendering() { + private _clear() { + this._clearDiffRendering(); + this._overlayCtrl.hide(); + this._diffLineDecorations.clear(); + this._currentChangeIndex.set(undefined, undefined); + this._currentEntryIndex.set(undefined, undefined); + this._ctxHasEditorModification.reset(); + this._ctxReviewModelEnabled.reset(); + } + + private _clearDiffRendering() { this._editor.changeViewZones((viewZoneChangeAccessor) => { for (const id of this._viewZones) { viewZoneChangeAccessor.removeZone(id); @@ -207,18 +281,11 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._viewZones = []; this._diffHunksRenderStore.clear(); this._diffVisualDecorations.clear(); - this._diffLineDecorations.clear(); - this._ctxHasEditorModification.reset(); - transaction(tx => { - this._currentEntryIndex.set(undefined, tx); - this._currentChangeIndex.set(undefined, tx); - }); this._scrollLock = false; } - private _updateWithDiff(entry: IModifiedFileEntry, diff: IDocumentDiff): void { + private _updateDiffRendering(entry: IModifiedFileEntry, diff: IDocumentDiff, reviewMode: boolean): void { - this._ctxHasEditorModification.set(!diff.identical); const originalModel = entry.originalModel; const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({ @@ -250,7 +317,6 @@ export class ChatEditorController extends Disposable implements IEditorContribut } this._viewZones = []; const modifiedVisualDecorations: IModelDeltaDecoration[] = []; - const modifiedLineDecorations: IModelDeltaDecoration[] = []; const mightContainNonBasicASCII = originalModel.mightContainNonBasicASCII(); const mightContainRTL = originalModel.mightContainRTL(); const renderOptions = RenderOptions.fromEditor(this._editor); @@ -267,18 +333,21 @@ export class ChatEditorController extends Disposable implements IEditorContribut mightContainRTL, ); const decorations: InlineDecoration[] = []; - for (const i of diffEntry.innerChanges || []) { - decorations.push(new InlineDecoration( - i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)), - diffDeleteDecoration.className!, - InlineDecorationType.Regular - )); - - // If the original range is empty, the start line number is 1 and the new range spans the entire file, don't draw an Added decoration - if (!(i.originalRange.isEmpty() && i.originalRange.startLineNumber === 1 && i.modifiedRange.endLineNumber === editorLineCount) && !i.modifiedRange.isEmpty()) { - modifiedVisualDecorations.push({ - range: i.modifiedRange, options: chatDiffAddDecoration - }); + + if (reviewMode) { + for (const i of diffEntry.innerChanges || []) { + decorations.push(new InlineDecoration( + i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)), + diffDeleteDecoration.className!, + InlineDecorationType.Regular + )); + + // If the original range is empty, the start line number is 1 and the new range spans the entire file, don't draw an Added decoration + if (!(i.originalRange.isEmpty() && i.originalRange.startLineNumber === 1 && i.modifiedRange.endLineNumber === editorLineCount) && !i.modifiedRange.isEmpty()) { + modifiedVisualDecorations.push({ + range: i.modifiedRange, options: chatDiffAddDecoration + }); + } } } @@ -312,43 +381,41 @@ export class ChatEditorController extends Disposable implements IEditorContribut options: modifiedDecoration }); } - const domNode = document.createElement('div'); - domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; - const result = renderLines(source, renderOptions, decorations, domNode); - - if (!isCreatedContent) { - const viewZoneData: IViewZone = { - afterLineNumber: diffEntry.modified.startLineNumber - 1, - heightInLines: result.heightInLines, - domNode, - ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 - }; - - this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); - } - // Add content widget for each diff change - const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); - widget.layout(diffEntry.modified.startLineNumber); + if (reviewMode) { + const domNode = document.createElement('div'); + domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; + const result = renderLines(source, renderOptions, decorations, domNode); + + if (!isCreatedContent) { - this._diffHunkWidgets.push(widget); - diffHunkDecorations.push({ - range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), - options: { - description: 'diff-hunk-widget', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + const viewZoneData: IViewZone = { + afterLineNumber: diffEntry.modified.startLineNumber - 1, + heightInLines: result.heightInLines, + domNode, + ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 + }; + + this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); } - }); - // Add line decorations for diff navigation - modifiedLineDecorations.push({ - range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), - options: ChatEditorController._diffLineDecorationData - }); + + // Add content widget for each diff change + const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); + widget.layout(diffEntry.modified.startLineNumber); + + this._diffHunkWidgets.push(widget); + diffHunkDecorations.push({ + range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), + options: { + description: 'diff-hunk-widget', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + } + }); + } } this._diffVisualDecorations.set(modifiedVisualDecorations); - this._diffLineDecorations.set(modifiedLineDecorations); }); const diffHunkDecoCollection = this._editor.createDecorationsCollection(diffHunkDecorations); @@ -423,6 +490,20 @@ export class ChatEditorController extends Disposable implements IEditorContribut })); } + private _updateDiffLineDecorations(diff: IDocumentDiff): void { + this._ctxHasEditorModification.set(!diff.identical); + + const modifiedLineDecorations: IModelDeltaDecoration[] = []; + + for (const diffEntry of diff.changes) { + modifiedLineDecorations.push({ + range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), + options: ChatEditorController._diffLineDecorationData + }); + } + this._diffLineDecorations.set(modifiedLineDecorations); + } + unlockScroll(): void { this._scrollLock = false; } @@ -441,7 +522,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut return this._reveal(false, strict); } - private _reveal(next: boolean, strict: boolean): boolean { + private _reveal(next: boolean, strict: boolean, scrollType = ScrollType.Smooth): boolean { const position = this._editor.getPosition(); if (!position) { this._currentChangeIndex.set(undefined, undefined); @@ -480,7 +561,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const targetPosition = next ? decorations[target].getStartPosition() : decorations[target].getEndPosition(); this._editor.setPosition(targetPosition); - this._editor.revealPositionInCenter(targetPosition, ScrollType.Smooth); + this._editor.revealPositionInCenter(targetPosition, scrollType); this._editor.focus(); return true; @@ -508,7 +589,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut return closestWidget; } - undoNearestChange(closestWidget: DiffHunkWidget | undefined): void { + rejectNearestChange(closestWidget: DiffHunkWidget | undefined): void { closestWidget = closestWidget ?? this._findClosestWidget(); if (closestWidget instanceof DiffHunkWidget) { closestWidget.reject(); @@ -524,10 +605,23 @@ export class ChatEditorController extends Disposable implements IEditorContribut } } - async openDiff(widget: DiffHunkWidget | undefined): Promise { + async toggleDiff(widget: DiffHunkWidget | undefined): Promise { if (!this._editor.hasModel()) { return; } + + let entry: IModifiedFileEntry | undefined; + for (const session of this._chatEditingService.editingSessionsObs.get()) { + entry = session.getEntry(this._editor.getModel().uri); + if (entry) { + break; + } + } + + if (!entry) { + return; + } + const lineRelativeTop = this._editor.getTopForLineNumber(this._editor.getPosition().lineNumber) - this._editor.getScrollTop(); let closestDistance = Number.MAX_VALUE; @@ -544,22 +638,57 @@ export class ChatEditorController extends Disposable implements IEditorContribut } } + let selection = this._editor.getSelection(); if (widget instanceof DiffHunkWidget) { - const lineNumber = widget.getStartLineNumber(); const position = lineNumber ? new Position(lineNumber, 1) : undefined; - let selection = this._editor.getSelection(); if (position && !selection.containsPosition(position)) { selection = Selection.fromPositions(position); } + } + + const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor); + if (isDiffEditor) { + // normal EDITOR + await this._editorService.openEditor({ resource: entry.modifiedURI }); + + } else { + // DIFF editor + const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; const diffEditor = await this._editorService.openEditor({ - original: { resource: widget.entry.originalURI, options: { selection: undefined } }, - modified: { resource: widget.entry.modifiedURI, options: { selection } }, + original: { resource: entry.originalURI, options: { selection: undefined } }, + modified: { resource: entry.modifiedURI, options: { selection } }, + label: defaultAgentName + ? localize('diff.agent', '{0} (changes from {1})', basename(entry.modifiedURI), defaultAgentName) + : localize('diff.generic', '{0} (changes from chat)', basename(entry.modifiedURI)) }); - // this is needed, passing the selection doesn't seem to work - diffEditor?.getControl()?.setSelection(selection); + if (diffEditor && diffEditor.input) { + + // this is needed, passing the selection doesn't seem to work + diffEditor.getControl()?.setSelection(selection); + + // close diff editor when entry is decided + const d = autorun(r => { + const state = entry.state.read(r); + if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { + d.dispose(); + + const editorIdents: IEditorIdentifier[] = []; + for (const candidate of this._editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (isDiffEditorInput(candidate.editor) + && isEqual(candidate.editor.original.resource, entry.originalURI) + && isEqual(candidate.editor.modified.resource, entry.modifiedURI) + ) { + editorIdents.push(candidate); + } + } + + this._editorService.closeEditors(editorIdents); + } + }); + } } } } @@ -597,6 +726,7 @@ class DiffHunkWidget implements IOverlayWidget { }); this._store.add(toolbar); + this._store.add(toolbar.actionRunner.onWillRun(_ => _editor.focus())); this._editor.addOverlayWidget(this); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 4b0dd3c8111..f420d0883fc 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -3,34 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/chatEditorOverlay.css'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { autorun, IObservable, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { IChatEditingSession, IModifiedFileEntry } from '../common/chatEditingService.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IActionRunner } from '../../../../base/common/actions.js'; -import { $, append, EventLike, reset } from '../../../../base/browser/dom.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { $, addDisposableGenericMouseMoveListener, append, EventLike, reset } from '../../../../base/browser/dom.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { assertType } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { AcceptAction, RejectAction } from './chatEditorActions.js'; +import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; +import './media/chatEditorOverlay.css'; +import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; +import { IChatService } from '../common/chatService.js'; +import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { rcut } from '../../../../base/common/strings.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; class ChatEditorOverlayWidget implements IOverlayWidget { - readonly allowEditorOverflow = false; + readonly allowEditorOverflow = true; private readonly _domNode: HTMLElement; private readonly _progressNode: HTMLElement; @@ -46,7 +47,9 @@ class ChatEditorOverlayWidget implements IOverlayWidget { constructor( private readonly _editor: ICodeEditor, @IEditorService editorService: IEditorService, - @IInstantiationService instaService: IInstantiationService, + @IHoverService private readonly _hoverService: IHoverService, + @IChatService private readonly _chatService: IChatService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { this._domNode = document.createElement('div'); this._domNode.classList.add('chat-editor-overlay-widget'); @@ -61,7 +64,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget { toolbarNode.classList.add('chat-editor-overlay-toolbar'); this._domNode.appendChild(toolbarNode); - this._toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { + this._toolbar = _instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { telemetrySource: 'chatEditor.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -125,6 +128,36 @@ class ChatEditorOverlayWidget implements IOverlayWidget { constructor() { super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true }); } + + override render(container: HTMLElement): void { + super.render(container); + + if (action.id === AcceptAction.ID) { + + const listener = this._store.add(new MutableDisposable()); + + this._store.add(autorun(r => { + + assertType(this.label); + assertType(this.element); + + const ctrl = that._entry.read(r)?.entry.autoAcceptController.read(r); + if (ctrl) { + + const r = -100 * (ctrl.remaining / ctrl.total); + + this.element.style.setProperty('--vscode-action-item-auto-timeout', `${r}%`); + + this.element.classList.toggle('auto', true); + listener.value = addDisposableGenericMouseMoveListener(this.element, () => ctrl.cancel()); + } else { + this.element.classList.toggle('auto', false); + listener.clear(); + } + })); + } + } + override set actionRunner(actionRunner: IActionRunner) { super.actionRunner = actionRunner; @@ -183,7 +216,32 @@ class ChatEditorOverlayWidget implements IOverlayWidget { return { preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER }; } - show(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry) { + showRequest(session: IChatEditingSession) { + + this._showStore.clear(); + + const chatModel = this._chatService.getSession(session.chatSessionId); + const chatRequest = chatModel?.getRequests().at(-1); + + if (!chatRequest || !chatRequest.response) { + this.hide(); + return; + } + + this._domNode.classList.toggle('busy', true); + + const message = rcut(chatRequest.message.text, 47); + reset(this._progressNode, message); + + this._showStore.add(this._hoverService.setupDelayedHover(this._progressNode, { + content: chatRequest.message.text, + appearance: { showPointer: true } + })); + + this._show(); + } + + showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry, indicies: { entryIndex: IObservable; changeIndex: IObservable }) { this._showStore.clear(); @@ -197,16 +255,14 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._showStore.add(autorun(r => { const value = activeEntry.rewriteRatio.read(r); reset(this._progressNode, (value === 0 - ? localize('generating', "Generating edits...") - : localize('applyingPercentage', "{0}% Applying edits...", Math.round(value * 100)))); + ? localize('generating', "Generating Edits") + : localize('applyingPercentage', "{0}% Applying Edits", Math.round(value * 100)))); })); this._showStore.add(autorun(r => { - const ctrl = ChatEditorController.get(this._editor); - - const entryIndex = ctrl?.currentEntryIndex.read(r); - const changeIndex = ctrl?.currentChangeIndex.read(r); + const entryIndex = indicies.entryIndex.read(r); + const changeIndex = indicies.changeIndex.read(r); const entries = session.entries.read(r); @@ -227,6 +283,23 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._navigationBearings.set({ changeCount: changes, activeIdx, entriesCount: entries.length }, undefined); })); + this._show(); + } + + private _show(): void { + + const editorWidthObs = observableFromEvent(this._editor.onDidLayoutChange, () => { + const diffEditor = this._instaService.invokeFunction(findDiffEditorContainingCodeEditor, this._editor); + return diffEditor + ? diffEditor.getOriginalEditor().getLayoutInfo().contentWidth + diffEditor.getModifiedEditor().getLayoutInfo().contentWidth + : this._editor.getLayoutInfo().contentWidth; + }); + + this._showStore.add(autorun(r => { + const width = editorWidthObs.read(r); + this._domNode.style.maxWidth = `${width - 20}px`; + })); + if (!this._isAdded) { this._editor.addOverlayWidget(this); this._isAdded = true; @@ -248,78 +321,42 @@ class ChatEditorOverlayWidget implements IOverlayWidget { } } -export const navigationBearingFakeActionId = 'chatEditor.navigation.bearings'; - -MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { - command: { - id: navigationBearingFakeActionId, - title: localize('label', "Navigation Status"), - precondition: ContextKeyExpr.false(), - }, - group: 'navigate', - order: -1 -}); export class ChatEditorOverlayController implements IEditorContribution { - static readonly ID = 'editor.contrib.chatOverlayController'; + static readonly ID = 'editor.contrib.chatEditorOverlayController'; - private readonly _store = new DisposableStore(); - - static get(editor: ICodeEditor) { - return editor.getContribution(ChatEditorOverlayController.ID); + static get(editor: ICodeEditor): ChatEditorOverlayController | undefined { + return editor.getContribution(ChatEditorOverlayController.ID) ?? undefined; } + private readonly _overlayWidget: ChatEditorOverlayWidget; + constructor( private readonly _editor: ICodeEditor, - @IChatEditingService chatEditingService: IChatEditingService, - @IInstantiationService instaService: IInstantiationService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { - const modelObs = observableFromEvent(this._editor.onDidChangeModel, () => this._editor.getModel()); - const widget = this._store.add(instaService.createInstance(ChatEditorOverlayWidget, this._editor)); + this._overlayWidget = this._instaService.createInstance(ChatEditorOverlayWidget, this._editor); - if (this._editor.getOption(EditorOption.inDiffEditor)) { - return; - } - - this._store.add(autorun(r => { - const model = modelObs.read(r); - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session || !model) { - widget.hide(); - return; - } - - const state = session.state.read(r); - if (state === ChatEditingSessionState.Disposed) { - widget.hide(); - return; - } - - const entries = session.entries.read(r); - const idx = entries.findIndex(e => isEqual(e.modifiedURI, model.uri)); - if (idx < 0) { - widget.hide(); - return; - } + } - const isModifyingOrModified = entries.some(e => e.state.read(r) === WorkingSetEntryState.Modified || e.isCurrentlyBeingModified.read(r)); - if (!isModifyingOrModified) { - widget.hide(); - return; - } + dispose(): void { + this.hide(); + this._overlayWidget.dispose(); + } - const entry = entries[idx]; - if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { - widget.hide(); - return; - } - widget.show(session, entry, entries[(idx + 1) % entries.length]); + showRequest(session: IChatEditingSession) { + this._overlayWidget.showRequest(session); + } - })); + showEntry(session: IChatEditingSession, + activeEntry: IModifiedFileEntry, next: IModifiedFileEntry, + indicies: { entryIndex: IObservable; changeIndex: IObservable } + ) { + this._overlayWidget.showEntry(session, activeEntry, next, indicies); } - dispose() { - this._store.dispose(); + hide() { + this._overlayWidget.hide(); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts deleted file mode 100644 index 378f2363085..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ /dev/null @@ -1,407 +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 { DeferredPromise, RunOnceScheduler } from '../../../../base/common/async.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { CancellationError } from '../../../../base/common/errors.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../base/common/map.js'; -import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; -import { assertType } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { localize } from '../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IEditorIdentifier, SaveReason } from '../../../common/editor.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { AutoSaveMode, IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatModel } from '../common/chatModel.js'; -import { IChatService } from '../common/chatService.js'; -import { ChatEditingModifiedFileEntry } from './chatEditing/chatEditingModifiedFileEntry.js'; - - -const STORAGE_KEY_AUTOSAVE_DISABLED = 'chat.editing.autosaveDisabled'; - -export class ChatEditorAutoSaveDisabler extends Disposable implements IWorkbenchContribution { - - static readonly ID: string = 'workbench.chat.autoSaveDisabler'; - - private _autosaveDisabledUris: string[] = []; - - constructor( - @IConfigurationService configService: IConfigurationService, - @IChatEditingService chatEditingService: IChatEditingService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @ILifecycleService lifecycleService: ILifecycleService, - @IStorageService storageService: IStorageService - ) { - super(); - - // on shutdown remember all files that have auto save disabled - this._store.add(lifecycleService.onWillShutdown((e) => { - storageService.store(STORAGE_KEY_AUTOSAVE_DISABLED, this._autosaveDisabledUris, StorageScope.WORKSPACE, StorageTarget.MACHINE); - })); - - const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); - - // as quickly as possible disable auto save for all files that were modified before the last shutdown - if (!alwaysSaveConfig.get()) { - const autoSaveDisabled = storageService.getObject(STORAGE_KEY_AUTOSAVE_DISABLED, StorageScope.WORKSPACE, []); - if (Array.isArray(autoSaveDisabled) && autoSaveDisabled.length > 0) { - - const initializingStore = new DisposableStore(); - for (const uriString of autoSaveDisabled) { - initializingStore.add(fileConfigService.disableAutoSave(URI.parse(uriString))); - } - chatEditingService.getOrRestoreEditingSession().finally(() => { - // by now the session is restored and the auto save handlers are in place - initializingStore.dispose(); - }); - - } - } - - // listen to session changes and update auto save settings accordingly - const saveConfig = this._store.add(new MutableDisposable()); - this._store.add(autorun(reader => { - const store = new DisposableStore(); - const autoSaveDisabled: string[] = []; - try { - if (alwaysSaveConfig.read(reader)) { - return; - } - const session = chatEditingService.currentEditingSessionObs.read(reader); - if (session) { - const entries = session.entries.read(reader); - for (const entry of entries) { - if (entry.state.read(reader) === WorkingSetEntryState.Modified) { - autoSaveDisabled.push(entry.modifiedURI.toString()); - store.add(fileConfigService.disableAutoSave(entry.modifiedURI)); - } - } - } - } finally { - saveConfig.value = store; // disposes the previous store, after we have added the new one - this._autosaveDisabledUris = autoSaveDisabled; - } - })); - } -} - - -export class ChatEditorSaving extends Disposable implements IWorkbenchContribution { - - static readonly ID: string = 'workbench.chat.editorSaving'; - - static readonly _config = 'chat.editing.alwaysSaveWithGeneratedChanges'; - - constructor( - @IConfigurationService configService: IConfigurationService, - @IChatEditingService chatEditingService: IChatEditingService, - @IChatAgentService chatAgentService: IChatAgentService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @ITextFileService textFileService: ITextFileService, - @ILabelService labelService: ILabelService, - @IDialogService dialogService: IDialogService, - @IChatService private readonly _chatService: IChatService, - ) { - super(); - - // --- report that save happened - this._store.add(autorunWithStore((r, store) => { - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return; - } - const chatSession = this._chatService.getSession(session.chatSessionId); - if (!chatSession) { - return; - } - store.add(textFileService.files.onDidSave(e => { - const entry = session.getEntry(e.model.resource); - if (entry && entry.state.get() === WorkingSetEntryState.Modified) { - this._reportSavedWhenReady(chatSession, entry); - } - })); - })); - - const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); - this._store.add(autorunWithStore((r, store) => { - - const alwaysSave = alwaysSaveConfig.read(r); - - if (alwaysSave) { - return; - } - - const saveJobs = new class { - - private _deferred?: DeferredPromise; - private readonly _soon = new RunOnceScheduler(() => this._prompt(), 0); - private readonly _uris = new ResourceSet(); - - add(uri: URI) { - this._uris.add(uri); - this._soon.schedule(); - this._deferred ??= new DeferredPromise(); - return this._deferred.p; - } - - private async _prompt() { - - // this might have changed in the meantime and there is checked again and acted upon - const alwaysSave = configService.getValue(ChatEditorSaving._config); - if (alwaysSave) { - return; - } - - const uri = Iterable.first(this._uris); - if (!uri) { - // bogous? - return; - } - - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName ?? localize('chat', "chat"); - const filelabel = labelService.getUriBasenameLabel(uri); - - const message = this._uris.size === 1 - ? localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel) - : localize('message.2', "Do you want to save the changes {0} made to {1} files?", agentName, this._uris.size); - - const result = await dialogService.confirm({ - message, - detail: localize('detail2', "AI-generated changes may be incorrect and should be reviewed before saving.", agentName), - primaryButton: localize('save', "Save"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - this._uris.clear(); - - if (result.confirmed && result.checkboxChecked) { - // remember choice - await configService.updateValue(ChatEditorSaving._config, true); - } - - if (!result.confirmed) { - // cancel the save - this._deferred?.error(new CancellationError()); - } else { - this._deferred?.complete(); - } - this._deferred = undefined; - } - }; - - store.add(textFileService.files.addSaveParticipant({ - participate: async (workingCopy, context, progress, token) => { - - if (context.reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - const session = await chatEditingService.getOrRestoreEditingSession(); - if (!session) { - return; - } - const entry = session.getEntry(workingCopy.resource); - if (!entry || entry.state.get() !== WorkingSetEntryState.Modified) { - return; - } - - return saveJobs.add(entry.modifiedURI); - } - })); - })); - - // autosave: OFF & alwaysSaveWithAIChanges - save files after accept - this._store.add(autorun(r => { - const saveConfig = fileConfigService.getAutoSaveMode(undefined); - if (saveConfig.mode !== AutoSaveMode.OFF) { - return; - } - if (!alwaysSaveConfig.read(r)) { - return; - } - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return; - } - for (const entry of session.entries.read(r)) { - if (entry.state.read(r) === WorkingSetEntryState.Accepted) { - textFileService.save(entry.modifiedURI); - } - } - })); - } - - private _reportSaved(entry: IModifiedFileEntry) { - assertType(entry instanceof ChatEditingModifiedFileEntry); - - this._chatService.notifyUserAction({ - action: { kind: 'chatEditingSessionAction', uri: entry.modifiedURI, hasRemainingEdits: false, outcome: 'saved' }, - agentId: entry.telemetryInfo.agentId, - command: entry.telemetryInfo.command, - sessionId: entry.telemetryInfo.sessionId, - requestId: entry.telemetryInfo.requestId, - result: entry.telemetryInfo.result - }); - } - - private _reportSavedWhenReady(session: IChatModel, entry: IModifiedFileEntry) { - if (!session.requestInProgress) { - this._reportSaved(entry); - return; - } - // wait until no more request is pending - const d = session.onDidChange(e => { - if (!session.requestInProgress) { - this._reportSaved(entry); - this._store.delete(d); - d.dispose(); - } - }); - this._store.add(d); - } -} - -export class ChatEditingSaveAllAction extends Action2 { - static readonly ID = 'chatEditing.saveAllFiles'; - - constructor() { - super({ - id: ChatEditingSaveAllAction.ID, - title: localize('save.allFiles', 'Save All'), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), - icon: Codicon.saveAll, - menu: [ - { - when: ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), - id: MenuId.EditorTitle, - order: 2, - group: 'navigation', - }, - { - id: MenuId.ChatEditingWidgetToolbar, - group: 'navigation', - order: 2, - // Show the option to save without accepting if the user hasn't configured the setting to always save with generated changes - when: ContextKeyExpr.and( - applyingChatEditsFailedContextKey.negate(), - hasUndecidedChatEditingResourceContextKey, - ContextKeyExpr.equals(`config.${ChatEditorSaving._config}`, false), - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession) - ) - } - ], - keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KeyS, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatInput), - weight: KeybindingWeight.WorkbenchContrib, - }, - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const editorService = accessor.get(IEditorService); - const configService = accessor.get(IConfigurationService); - const chatAgentService = accessor.get(IChatAgentService); - const dialogService = accessor.get(IDialogService); - const labelService = accessor.get(ILabelService); - - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } - - const editors: IEditorIdentifier[] = []; - for (const modifiedFileEntry of currentEditingSession.entries.get()) { - if (modifiedFileEntry.state.get() === WorkingSetEntryState.Modified) { - const modifiedFile = modifiedFileEntry.modifiedURI; - const matchingEditors = editorService.findEditors(modifiedFile); - if (matchingEditors.length === 0) { - continue; - } - const matchingEditor = matchingEditors[0]; - if (matchingEditor.editor.isDirty()) { - editors.push(matchingEditor); - } - } - } - - if (editors.length === 0) { - return; - } - - const alwaysSave = configService.getValue(ChatEditorSaving._config); - if (!alwaysSave) { - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; - - let message: string; - if (editors.length === 1) { - const resource = editors[0].editor.resource; - if (resource) { - const filelabel = labelService.getUriBasenameLabel(resource); - message = agentName - ? localize('message.batched.oneFile.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel) - : localize('message.batched.oneFile.2', "Do you want to save the changes chat made in {0}?", filelabel); - } else { - message = agentName - ? localize('message.batched.oneFile.3', "Do you want to save the changes {0} made in 1 file?", agentName) - : localize('message.batched.oneFile.4', "Do you want to save the changes chat made in 1 file?"); - } - } else { - message = agentName - ? localize('message.batched.multiFile.1', "Do you want to save the changes {0} made in {1} files?", agentName, editors.length) - : localize('message.batched.multiFile.2', "Do you want to save the changes chat made in {0} files?", editors.length); - } - - - const result = await dialogService.confirm({ - message, - detail: localize('detail2', "AI-generated changes may be incorrect and should be reviewed before saving.", agentName), - primaryButton: localize('save all', "Save All"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - if (!result.confirmed) { - return; - } - - if (result.checkboxChecked) { - await configService.updateValue(ChatEditorSaving._config, true); - } - } - - // Skip our own chat editing save blocking participant, since we already showed our own batched dialog - await editorService.save(editors, { reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); - } -} -registerAction2(ChatEditingSaveAllAction); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index cc9ebf1b7ba..888e4637dd6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -17,7 +17,7 @@ import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../.. import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; -import { IAction } from '../../../../base/common/actions.js'; +import { IAction, Separator, toAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { Promises } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -87,10 +87,11 @@ import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; -import { IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/languageModels.js'; -import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext } from './actions/chatExecuteActions.js'; +import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; +import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; +import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, IToggleAgentModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; +import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel.js'; import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; @@ -98,7 +99,6 @@ import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop, EditsDragAndDrop } from './chatDragAndDrop.js'; import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; -import { ChatEditingSaveAllAction } from './chatEditorSaving.js'; import { ChatFollowups } from './chatFollowups.js'; import { IChatViewState } from './chatWidget.js'; import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileReference.js'; @@ -124,6 +124,12 @@ interface IChatInputPartOptions { }; editorOverflowWidgetsDomNode?: HTMLElement; enableImplicitContext?: boolean; + renderWorkingSet?: boolean; +} + +export interface IWorkingSetEntry { + uri: URI; + isMarkedReadonly?: boolean; } export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { @@ -187,7 +193,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge continue; } - for (const childUri of variable.validFileReferenceUris) { + for (const childUri of variable.allValidReferencesUris) { contextArr.push({ id: variable.id, name: basename(childUri.path), @@ -200,9 +206,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + contextArr + .push(...this.instructionAttachmentsPart.chatAttachments); + return contextArr; } + /** + * Check if the chat input part has any prompt instruction attachments. + */ + public get hasInstructionAttachments(): boolean { + return !this.instructionAttachmentsPart.empty; + } + private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; private _implicitContext: ChatImplicitContext | undefined; @@ -258,12 +274,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private inputEditorHasText: IContextKey; private chatCursorAtTop: IContextKey; private inputEditorHasFocus: IContextKey; + /** + * Context key is set when prompt instructions are attached. + */ + private promptInstructionsAttached: IContextKey; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); - private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); - private _currentLanguageModel: string | undefined; + private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); + private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined; get currentLanguageModel() { - return this._currentLanguageModel; + return this._currentLanguageModel?.identifier; } private cachedDimensions: dom.Dimension | undefined; @@ -298,13 +318,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get attemptedWorkingSetEntriesCount() { return this._attemptedWorkingSetEntriesCount; } - private _combinedChatEditWorkingSetEntries: URI[] = []; + private _combinedChatEditWorkingSetEntries: IWorkingSetEntry[] = []; public get chatEditWorkingSetFiles() { return this._combinedChatEditWorkingSetEntries; } private readonly getInputState: () => IChatInputState; + /** + * Child widget of prompt instruction attachments. + * See {@linkcode InstructionAttachmentsWidget}. + */ + private instructionAttachmentsPart: InstructionAttachmentsWidget; + constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used private readonly location: ChatAgentLocation, @@ -332,6 +358,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IStorageService private readonly storageService: IStorageService, @ILabelService private readonly labelService: ILabelService, @IChatVariablesService private readonly variableService: IChatVariablesService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -354,9 +381,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService); this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); + this.promptInstructionsAttached = ChatContextKeys.instructionsAttached.bindTo(contextKeyService); this.history = this.loadHistory(); - this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); + this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], ChatInputHistoryMaxEntries, historyKeyFn))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { @@ -368,6 +396,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); + this.instructionAttachmentsPart = this._register( + instantiationService.createInstance( + InstructionAttachmentsWidget, + this.attachmentModel.promptInstructions, + this._contextResourceLabels, + ), + ); + + // trigger re-layout of chat input when number of instruction attachment changes + this.instructionAttachmentsPart.onAttachmentsCountChange(() => { + this._onDidChangeHeight.fire(); + }); + this.initSelectedModel(); } @@ -380,7 +421,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (persistedSelection) { const model = this.languageModelsService.lookupLanguageModel(persistedSelection); if (model) { - this._currentLanguageModel = persistedSelection; + this._currentLanguageModel = { metadata: model, identifier: persistedSelection }; this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel); } else { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { @@ -389,26 +430,57 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._waitForPersistedLanguageModel.clear(); if (persistedModel.metadata.isUserSelectable) { - this._currentLanguageModel = persistedSelection; + this._currentLanguageModel = { metadata: persistedModel.metadata, identifier: persistedSelection }; this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel!); } } }); } } + + this._register(this.chatAgentService.onDidChangeToolsAgentModeEnabled(() => { + if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { + this.setCurrentLanguageModelToDefault(); + } + })); + } + + private supportsVision(): boolean { + return this._currentLanguageModel?.metadata.capabilities?.vision ?? false; + } + + private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { + // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex + if (this.chatAgentService.getDefaultAgent(this.location)?.isToolsAgent) { + // Filter out models that don't support tool calling, and models that don't support enough context to have a good experience with the tools agent + return !!model.metadata.capabilities?.toolCalling && model.metadata.maxInputTokens > 40000; + } + + return true; + } + + private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + const models = this.languageModelsService.getLanguageModelIds() + .map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! })) + .filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry)); + models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + + return models; } private setCurrentLanguageModelToDefault() { - const defaultLanguageModel = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault); + const defaultLanguageModelId = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault); const hasUserSelectableLanguageModels = this.languageModelsService.getLanguageModelIds().find(id => { const model = this.languageModelsService.lookupLanguageModel(id); return model?.isUserSelectable && !model.isDefault; }); - this._currentLanguageModel = hasUserSelectableLanguageModels ? defaultLanguageModel : undefined; + this._currentLanguageModel = hasUserSelectableLanguageModels && defaultLanguageModelId ? + { metadata: this.languageModelsService.lookupLanguageModel(defaultLanguageModelId)!, identifier: defaultLanguageModelId } : + undefined; } - private setCurrentLanguageModelByUser(modelId: string) { - this._currentLanguageModel = modelId; + private setCurrentLanguageModelByUser(model: ILanguageModelChatMetadataAndIdentifier) { + this._currentLanguageModel = model; // The user changed the language model, so we don't wait for the persisted option to be registered this._waitForPersistedLanguageModel.clear(); @@ -416,7 +488,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.layout(this.cachedDimensions.height, this.cachedDimensions.width); } - this.storageService.store(this.getSelectedModelStorageKey(), modelId, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); } private loadHistory(): HistoryNavigator2 { @@ -535,7 +607,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - private saveCurrentValue(inputState: any): void { + private saveCurrentValue(inputState: IChatInputState): void { + inputState.chatContextAttachments = inputState.chatContextAttachments?.filter(attachment => !attachment.isImage); const newEntry = { text: this._inputEditor.getValue(), state: inputState }; this.history.replaceLast(newEntry); } @@ -555,7 +628,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async acceptInput(isUserQuery?: boolean): Promise { if (isUserQuery) { const userQuery = this._inputEditor.getValue(); - const entry: IChatHistoryEntry = { text: userQuery, state: this.getInputState() }; + const inputState = this.getInputState(); + inputState.chatContextAttachments = inputState.chatContextAttachments?.filter(attachment => !attachment.isImage); + const entry: IChatHistoryEntry = { text: userQuery, state: inputState }; this.history.replaceLast(entry); this.history.add({ text: '' }); } @@ -746,12 +821,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this._currentLanguageModel) { const itemDelegate: ModelPickerDelegate = { onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, - setModel: (modelId: string) => { - this.setCurrentLanguageModelByUser(modelId); - } + setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { + this.setCurrentLanguageModelByUser(model); + this.renderAttachedContext(); + }, + getModels: () => this.getModels() }; return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate, { hoverDelegate: options.hoverDelegate, keybinding: options.keybinding ?? undefined }); } + } else if (action.id === ToggleAgentModeActionId && action instanceof MenuItemAction) { + return this.instantiationService.createInstance(ToggleAgentActionViewItem, action, options as IMenuEntryActionViewItemOptions); } return undefined; @@ -837,7 +916,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Render as attachments anything that isn't a file, but still render specific ranges in a file ? [...this.attachmentModel.attachments.entries()].filter(([_, attachment]) => !attachment.isFile || attachment.isFile && typeof attachment.value === 'object' && !!attachment.value && 'range' in attachment.value) : [...this.attachmentModel.attachments.entries()]; - dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value), this.attachedContextContainer); + dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); if (!attachments.length) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; } @@ -847,6 +926,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge container.appendChild(implicitPart.domNode); } + this.promptInstructionsAttached.set(!this.instructionAttachmentsPart.empty); + container.appendChild(this.instructionAttachmentsPart.domNode); + const attachmentInitPromises: Promise[] = []; for (const [index, attachment] of attachments) { const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); @@ -883,38 +965,54 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else if (attachment.isImage) { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); - const hoverElement = dom.$('div.chat-attached-context-hover'); hoverElement.setAttribute('aria-label', ariaLabel); - // Custom label - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); + const supportsVision = this.supportsVision(); + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.name); widget.appendChild(pillIcon); widget.appendChild(textLabel); - attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { - let buffer: Uint8Array; - try { - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); - if (attachment.value instanceof URI) { - const readFile = await this.fileService.readFile(attachment.value); - if (store.isDisposed) { - return; - } - buffer = readFile.value.buffer; - } else { - buffer = attachment.value as Uint8Array; + if (attachment.references) { + widget.style.cursor = 'pointer'; + const clickHandler = () => { + if (attachment.references && URI.isUri(attachment.references[0].reference)) { + this.openResource(attachment.references[0].reference, false, undefined); } - this.createImageElements(buffer, widget, hoverElement); - } catch (error) { - console.error('Error processing attachment:', error); - } + }; + store.add(addDisposableListener(widget, 'click', clickHandler)); + } - widget.style.position = 'relative'; - store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); - resolve(); - })); + if (!supportsVision) { + widget.classList.add('warning'); + hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel)?.name : this.currentLanguageModel); + textLabel.style.textDecoration = 'line-through'; + store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); + this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + } else { + attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { + let buffer: Uint8Array; + try { + this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + if (attachment.value instanceof URI) { + const readFile = await this.fileService.readFile(attachment.value); + if (store.isDisposed) { + return; + } + buffer = readFile.value.buffer; + } else { + buffer = attachment.value as Uint8Array; + } + this.createImageElements(buffer, widget, hoverElement); + } catch (error) { + console.error('Error processing attachment:', error); + } + store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); + resolve(); + })); + } + widget.style.position = 'relative'; } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); @@ -1079,7 +1177,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) { dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - if (!chatEditingSession) { + if (!chatEditingSession || !this.options.renderWorkingSet) { dom.clearNode(this.chatEditingSessionWidgetContainer); this._chatEditsDisposables.clear(); this._chatEditList = undefined; @@ -1121,6 +1219,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge state: metadata.state, description: metadata.description, kind: 'reference', + isMarkedReadonly: metadata.isMarkedReadonly, }); seenEntries.add(file); } @@ -1250,7 +1349,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge arg: { sessionId: chatEditingSession.chatSessionId }, }, buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingSaveAllAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { return { showIcon: true, showLabel: false, isSecondary: true }; } return undefined; @@ -1262,7 +1361,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } if (currentChatEditingState === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress) { - this._chatEditsProgress ??= new ProgressBar(innerContainer); + // this._chatEditsProgress ??= new ProgressBar(innerContainer); this._chatEditsProgress?.infinite().show(500); } @@ -1309,7 +1408,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge list.getHTMLElement().style.height = `${height}px`; list.splice(0, list.length, entries); list.splice(entries.length, 0, excludedEntries); - this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? e.reference : undefined)); + this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? ({ uri: e.reference, isMarkedReadonly: e.isMarkedReadonly }) : undefined)); const addFilesElement = innerContainer.querySelector('.chat-editing-session-toolbar-actions') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-toolbar-actions')); @@ -1352,7 +1451,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const rmBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { supportIcons: false, secondary: true, - hoverDelegate + hoverDelegate, + ariaLabel: localize('chatEditingSession.removeSuggestion', 'Remove suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })), })); rmBtn.icon = Codicon.close; rmBtn.setTitle(localize('chatEditingSession.removeSuggested', 'Remove suggestion')); @@ -1520,14 +1620,15 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { } interface ModelPickerDelegate { - onDidChangeModel: Event; - setModel(selectedModelId: string): void; + onDidChangeModel: Event; + setModel(selectedModelId: ILanguageModelChatMetadataAndIdentifier): void; + getModels(): ILanguageModelChatMetadataAndIdentifier[]; } class ModelPickerActionViewItem extends MenuEntryActionViewItem { constructor( action: MenuItemAction, - private currentLanguageModel: string, + private currentLanguageModel: ILanguageModelChatMetadataAndIdentifier, private readonly delegate: ModelPickerDelegate, options: IMenuEntryActionViewItemOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -1535,8 +1636,8 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, - @ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService, - @IAccessibilityService _accessibilityService: IAccessibilityService + @IAccessibilityService _accessibilityService: IAccessibilityService, + @ICommandService private readonly _commandService: ICommandService, ) { super(action, options, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); @@ -1564,41 +1665,113 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { } protected override updateLabel(): void { - if (this.label) { - const model = this._languageModelsService.lookupLanguageModel(this.currentLanguageModel); - if (model) { - dom.reset(this.label, dom.$('span.chat-model-label', undefined, model.name), ...renderLabelWithIcons(`$(chevron-down)`)); - } + if (this.label && this.currentLanguageModel) { + dom.reset(this.label, dom.$('span.chat-model-label', undefined, this.currentLanguageModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); } } private _openContextMenu() { - const setLanguageModelAction = (id: string, modelMetadata: ILanguageModelChatMetadata): IAction => { + const setLanguageModelAction = (entry: ILanguageModelChatMetadataAndIdentifier): IAction => { return { - id, - label: modelMetadata.name, + id: entry.identifier, + label: entry.metadata.name, tooltip: '', class: undefined, enabled: true, - checked: id === this.currentLanguageModel, + checked: entry.identifier === this.currentLanguageModel.identifier, run: () => { - this.currentLanguageModel = id; + this.currentLanguageModel = entry; this.updateLabel(); - this.delegate.setModel(id); + this.delegate.setModel(entry); } }; }; - const models = this._languageModelsService.getLanguageModelIds() - .map(modelId => ({ id: modelId, model: this._languageModelsService.lookupLanguageModel(modelId)! })) - .filter(entry => entry.model?.isUserSelectable); - models.sort((a, b) => a.model.name.localeCompare(b.model.name)); + const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); this._contextMenuService.showContextMenu({ getAnchor: () => this.element!, - getActions: () => models.map(entry => setLanguageModelAction(entry.id, entry.model)), + getActions: () => { + const actions = models.map(entry => setLanguageModelAction(entry)); + + if (this._contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) { + actions.push(new Separator()); + actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Enable More Models..."), run: () => this._commandService.executeCommand('workbench.action.chat.upgradePlan') })); + } + + return actions; + }, }); } } const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); + +class ToggleAgentActionViewItem extends MenuEntryActionViewItem { + private readonly agentStateActions: IAction[]; + + constructor( + action: MenuItemAction, + options: IMenuEntryActionViewItemOptions, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IAccessibilityService _accessibilityService: IAccessibilityService + ) { + options.keybindingNotRenderedWithLabel = true; + super(action, options, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); + + this.agentStateActions = [ + { + ...this.action, + id: 'agentMode', + label: localize('chat.agentMode', "Agent"), + class: undefined, + enabled: true, + run: () => this.action.run({ agentMode: true } satisfies IToggleAgentModeArgs) + }, + { + ...this.action, + id: 'normalMode', + label: localize('chat.normalMode', "Edit"), + class: undefined, + enabled: true, + checked: !this.action.checked, + run: () => this.action.run({ agentMode: false } satisfies IToggleAgentModeArgs) + }, + ]; + } + + override async onClick(event: MouseEvent): Promise { + this._openContextMenu(); + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('chat-modelPicker-item'); + + // TODO@roblourens this should be a DropdownMenuActionViewItem, but we can't customize how it's rendered yet. + this._register(dom.addDisposableListener(container, dom.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + this._openContextMenu(); + } + })); + } + + protected override updateLabel(): void { + if (this.label) { + const state = this.agentStateActions.find(action => action.checked)?.label ?? ''; + dom.reset(this.label, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); + } + } + + private _openContextMenu() { + this._contextMenuService.showContextMenu({ + getAnchor: () => this.element!, + getActions: () => this.agentStateActions + }); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 2bc16470629..e66dde9f6c9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -559,7 +559,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: CHAT_SIDEBAR_PANEL_ID, + title: localize2('chat.viewContainer.label', "Chat"), + icon: Codicon.commentDiscussion, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]), + storageId: CHAT_SIDEBAR_PANEL_ID, + hideIfEmpty: true, + order: 100, +}, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true }); + +const chatViewDescriptor: IViewDescriptor[] = [{ + id: ChatViewId, + containerIcon: chatViewContainer.icon, + containerTitle: chatViewContainer.title.value, + singleViewPaneContainerTitle: chatViewContainer.title.value, + name: localize2('chat.viewContainer.label', "Chat"), + canToggleVisibility: false, + canMoveView: true, + openCommandActionDescriptor: { + id: CHAT_SIDEBAR_PANEL_ID, + title: chatViewContainer.title, + mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"), + keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI + } + }, + order: 1 + }, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), + when: ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.panelParticipantRegistered, + ChatContextKeys.extensionInvalid + ) +}]; +Registry.as(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer); + +// --- Edits Container & View Registration + +const editsViewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: CHAT_EDITING_SIDEBAR_PANEL_ID, + title: localize2('chatEditing.viewContainer.label', "Copilot Edits"), + icon: Codicon.editSession, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]), + storageId: CHAT_EDITING_SIDEBAR_PANEL_ID, + hideIfEmpty: true, + order: 101, +}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); + +const editsViewDescriptor: IViewDescriptor[] = [{ + id: 'workbench.panel.chat.view.edits', + containerIcon: editsViewContainer.icon, + containerTitle: editsViewContainer.title.value, + singleViewPaneContainerTitle: editsViewContainer.title.value, + name: editsViewContainer.title, + canToggleVisibility: false, + canMoveView: true, + openCommandActionDescriptor: { + id: CHAT_EDITING_SIDEBAR_PANEL_ID, + title: editsViewContainer.title, + mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"), + keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, + linux: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI + } + }, + order: 2 + }, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), + when: ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.editingParticipantRegistered + ) +}]; +Registry.as(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer); + const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', jsonSchema: { @@ -166,16 +249,12 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; - private _viewContainer: ViewContainer; private _participantRegistrationDisposables = new DisposableMap(); constructor( @IChatAgentService private readonly _chatAgentService: IChatAgentService, @ILogService private readonly logService: ILogService ) { - this._viewContainer = this.registerViewContainer(); - this.registerDefaultParticipantView(); - this.registerChatEditingView(); this.handleAndRegisterChatExtensions(); } @@ -270,109 +349,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } }); } - - private registerViewContainer(): ViewContainer { - // Register View Container - const title = localize2('chat.viewContainer.label', "Chat"); - const icon = Codicon.commentDiscussion; - const viewContainerId = CHAT_SIDEBAR_PANEL_ID; - const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: viewContainerId, - title, - icon, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), - storageId: viewContainerId, - hideIfEmpty: true, - order: 100, - }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); - - return viewContainer; - } - - private registerDefaultParticipantView(): IDisposable { - const viewDescriptor: IViewDescriptor[] = [{ - id: ChatViewId, - containerIcon: this._viewContainer.icon, - containerTitle: this._viewContainer.title.value, - singleViewPaneContainerTitle: this._viewContainer.title.value, - name: localize2('chat.viewContainer.label', "Chat"), - canToggleVisibility: false, - canMoveView: true, - openCommandActionDescriptor: { - id: CHAT_SIDEBAR_PANEL_ID, - title: this._viewContainer.title, - mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"), - keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, - mac: { - primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI - } - }, - order: 1 - }, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), - when: ContextKeyExpr.or( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed, - ChatContextKeys.panelParticipantRegistered, - ChatContextKeys.extensionInvalid - ) - }]; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); - - return toDisposable(() => { - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); - }); - } - - private registerChatEditingView(): IDisposable { - const title = localize2('chatEditing.viewContainer.label', "Copilot Edits"); - const icon = Codicon.editSession; - const viewContainerId = CHAT_EDITING_SIDEBAR_PANEL_ID; - const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: viewContainerId, - title, - icon, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), - storageId: viewContainerId, - hideIfEmpty: true, - order: 101, - }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); - - const id = 'workbench.panel.chat.view.edits'; - const viewDescriptor: IViewDescriptor[] = [{ - id, - containerIcon: viewContainer.icon, - containerTitle: title.value, - singleViewPaneContainerTitle: title.value, - name: { value: title.value, original: title.value }, - canToggleVisibility: false, - canMoveView: true, - openCommandActionDescriptor: { - id: viewContainerId, - title, - mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"), - keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, - linux: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI - } - }, - order: 2 - }, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), - when: ContextKeyExpr.or( - ChatContextKeys.Setup.installed, - ChatContextKeys.editingParticipantRegistered - ) - }]; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); - - return toDisposable(() => { - Registry.as(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(viewContainer); - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, viewContainer); - }); - } } function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 9d990d09302..5550d3d33b9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -21,6 +21,7 @@ import { Mimes } from '../../../../base/common/mime.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { basename } from '../../../../base/common/resources.js'; +import { resizeImage } from './imageUtils.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; @@ -89,19 +90,25 @@ export class PasteImageProvider implements DocumentPasteEditProvider { tempDisplayName = `${displayName} ${appendValue}`; } - const imageContext = await getImageAttachContext(currClipboard, mimeType, token, tempDisplayName); + const scaledImageData = await resizeImage(currClipboard); + if (token.isCancellationRequested || !scaledImageData) { + return; + } - if (token.isCancellationRequested || !imageContext) { + const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName); + if (token.isCancellationRequested || !scaledImageContext) { return; } + widget.attachmentModel.addContext(scaledImageContext); + // Make sure to attach only new contexts const currentContextIds = widget.attachmentModel.getAttachmentIDs(); - if (currentContextIds.has(imageContext.id)) { + if (currentContextIds.has(scaledImageContext.id)) { return; } - const edit = createCustomPasteEdit(model, imageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); + const edit = createCustomPasteEdit(model, scaledImageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); return createEditSession(edit); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/code/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 6e2ab0a5b34..0da5b5a3cfa 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -52,7 +52,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService private readonly chatQuotaExceededContextKey = ChatContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService); private readonly completionsQuotaExceededContextKey = ChatContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService); - private ExtensionQuotaContextKeys = { // TODO@bpasero move into product.json or turn into core keys + private ExtensionQuotaContextKeys = { chatQuotaExceeded: 'github.copilot.chat.quotaExceeded', completionsQuotaExceeded: 'github.copilot.completions.quotaExceeded', }; @@ -176,8 +176,6 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo static readonly ID = 'chat.quotasStatusBarEntry'; - private static readonly COPILOT_STATUS_ID = 'GitHub.copilot.status'; // TODO@bpasero unify into 1 core indicator - private readonly entry = this._register(new DisposableStore()); constructor( @@ -187,11 +185,6 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo super(); this._register(Event.runAndSubscribe(this.chatQuotasService.onDidChangeQuotas, () => this.updateStatusbarEntry())); - this._register(this.statusbarService.onDidChangeEntryVisibility(e => { - if (e.id === ChatQuotasStatusBarEntry.COPILOT_STATUS_ID) { - this.updateStatusbarEntry(); - } - })); } private updateStatusbarEntry(): void { @@ -208,30 +201,15 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo text = localize('chatAndCompletionsQuotaExceededStatus', "Copilot limit reached"); } - const isCopilotStatusVisible = this.statusbarService.isEntryVisible(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID); - if (!isCopilotStatusVisible) { - text = `$(copilot-warning) ${text}`; - } - this.entry.add(this.statusbarService.addEntry({ name: localize('indicator', "Copilot Limit Indicator"), - text, + text: `$(copilot-warning) ${text}`, ariaLabel: text, command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, showInAllWindows: true, kind: 'prominent', tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }) - }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { - id: ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, - alignment: StatusbarAlignment.RIGHT, - compact: isCopilotStatusVisible - })); - - this.entry.add(this.statusbarService.overrideEntry(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, { - text: '$(copilot-warning)', - ariaLabel: text, - kind: 'prominent' - })); + }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, 1)); } } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 75f75066e1b..5c96d139e1a 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -7,14 +7,14 @@ import { renderMarkdownAsPlaintext } from '../../../../base/browser/markdownRend import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { isResponseVM } from '../common/chatViewModel.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from './chat.js'; -export class ChatResponseAccessibleView implements IAccessibleViewImplentation { +export class ChatResponseAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'panelChat'; readonly type = AccessibleViewType.View; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0ad1f26e379..d968b47c475 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -5,9 +5,9 @@ import './media/chatViewSetup.css'; import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; -import { Button, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; +import { ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { IAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; +import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { Barrier, timeout } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -22,7 +22,7 @@ import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRend import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -45,11 +45,11 @@ import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticatio import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, IChatWidget, showChatView, showEditsView } from './chat.js'; +import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, preferCopilotEditsView, showCopilotView } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; import { IChatQuotasService } from './chatQuotasService.js'; @@ -62,6 +62,12 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm import { isWeb } from '../../../../base/common/platform.js'; import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; +import { toErrorMessage } from '../../../../base/common/errorMessage.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { equalsIgnoreCase } from '../../../../base/common/strings.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -74,6 +80,10 @@ const defaultChat = { upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', + enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '', + providerSetting: product.defaultChatAgent?.providerSetting ?? '', + providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', @@ -104,16 +114,12 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr static readonly ID = 'workbench.chat.setup'; - private readonly context = this._register(this.instantiationService.createInstance(ChatSetupContext)); - private readonly requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, this.context)); - private readonly controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, this.context, this.requests))); - constructor( @IProductService private readonly productService: IProductService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ICommandService private readonly commandService: ICommandService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -124,22 +130,26 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr return; } - this.registerChatWelcome(); - this.registerActions(); + const context = this._register(this.instantiationService.createInstance(ChatSetupContext)); + const requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, context)); + const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests))); + + this.registerChatWelcome(controller, context); + this.registerActions(controller, context, requests); this.registerUrlLinkHandler(); + this.registerSetting(context); } - private registerChatWelcome(): void { + private registerChatWelcome(controller: Lazy, context: ChatSetupContext): void { Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ title: localize('welcomeChat', "Welcome to Copilot"), when: ChatContextKeys.SetupViewCondition, icon: Codicon.copilotLarge, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.controller.value, this.context)).element, + content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, controller.value, context)).element, }); } - private registerActions(): void { - const that = this; + private registerActions(controller: Lazy, context: ChatSetupContext, requests: ChatSetupRequests): void { const chatSetupTriggerContext = ContextKeyExpr.or( ChatContextKeys.Setup.installed.negate(), @@ -163,20 +173,16 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor, startSetup: boolean | undefined): Promise { + override async run(accessor: ServicesAccessor): Promise { const viewsService = accessor.get(IViewsService); const viewDescriptorService = accessor.get(IViewDescriptorService); const configurationService = accessor.get(IConfigurationService); const layoutService = accessor.get(IWorkbenchLayoutService); - await that.context.update({ triggered: true }); + await context.update({ hidden: false }); showCopilotView(viewsService, layoutService); - ensureSideBarChatViewSize(400, viewDescriptorService, layoutService); - - if (startSetup === true) { - that.controller.value.setup(); - } + ensureSideBarChatViewSize(viewDescriptorService, layoutService); configurationService.updateValue('chat.commandCenter.enabled', true); } @@ -259,7 +265,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr openerService.open(URI.parse(defaultChat.upgradePlanUrl)); - const entitlement = that.context.state.entitlement; + const entitlement = context.state.entitlement; if (entitlement !== ChatEntitlement.Pro) { // If the user is not yet Pro, we listen to window focus to refresh the token // when the user has come back to the window assuming the user signed up. @@ -271,7 +277,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr if (focus) { windowFocusListener.clear(); - const entitlement = await that.requests.forceResolveEntitlement(undefined); + const entitlement = await requests.forceResolveEntitlement(undefined); if (entitlement === ChatEntitlement.Pro) { refreshTokens(commandService); } @@ -282,7 +288,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise { const location = viewsDescriptorService.getViewLocationById(ChatViewId); - await that.context.update({ triggered: false }); + await context.update({ hidden: true }); if (location === ViewContainerLocation.AuxiliaryBar) { const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); @@ -298,9 +304,13 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } private registerUrlLinkHandler(): void { - this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler(URI.parse(`${this.productService.urlProtocol}://${defaultChat.chatExtensionId}`), { - handleURL: async () => { - this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: 'url' }); + this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler({ + canHandleURL: url => { + return url.scheme === this.productService.urlProtocol && equalsIgnoreCase(url.authority, defaultChat.chatExtensionId); + }, + handleURL: async url => { + const params = new URLSearchParams(url.query); + this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: 'url', detail: params.get('referrer') ?? undefined }); await this.commandService.executeCommand(TRIGGER_SETUP_COMMAND_ID); @@ -308,6 +318,29 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } })); } + + private registerSetting(context: ChatSetupContext): void { + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + + let lastNode: IConfigurationNode | undefined; + const registerSetting = () => { + const node: IConfigurationNode = { + id: 'chatSidebar', + title: localize('interactiveSessionConfigurationTitle', "Chat"), + type: 'object', + properties: { + 'chat.agent.maxRequests': { + type: 'number', + description: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use in agent mode."), + default: context.state.entitlement === ChatEntitlement.Limited ? 5 : 15 + }, + } + }; + configurationRegistry.updateConfigurations({ remove: lastNode ? [lastNode] : [], add: [node] }); + lastNode = node; + }; + this._register(Event.runAndSubscribe(context.onDidChange, () => registerSetting())); + } } //#endregion @@ -315,6 +348,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr //#region Chat Setup Request Service type EntitlementClassification = { + tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' }; entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; @@ -325,6 +359,7 @@ type EntitlementClassification = { type EntitlementEvent = { entitlement: ChatEntitlement; + tid: string; quotaChat: number | undefined; quotaCompletions: number | undefined; quotaResetDate: string | undefined; @@ -335,6 +370,7 @@ interface IEntitlementsResponse { readonly assigned_date: string; readonly can_signup_for_limited: boolean; readonly chat_enabled: boolean; + readonly analytics_tracking_id: string; readonly limited_user_quotas?: { readonly chat: number; readonly completions: number; @@ -355,6 +391,14 @@ interface IChatEntitlements { class ChatSetupRequests extends Disposable { + static providerId(configurationService: IConfigurationService): string { + if (configurationService.getValue(defaultChat.providerSetting) === defaultChat.enterpriseProviderId) { + return defaultChat.enterpriseProviderId; + } + + return defaultChat.providerId; + } + private state: IChatEntitlements = { entitlement: this.context.state.entitlement }; private pendingResolveCts = new CancellationTokenSource(); @@ -368,7 +412,8 @@ class ChatSetupRequests extends Disposable { @IRequestService private readonly requestService: IRequestService, @IChatQuotasService private readonly chatQuotasService: IChatQuotasService, @IDialogService private readonly dialogService: IDialogService, - @IOpenerService private readonly openerService: IOpenerService + @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -381,19 +426,19 @@ class ChatSetupRequests extends Disposable { this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); this._register(this.authenticationService.onDidChangeSessions(e => { - if (e.providerId === defaultChat.providerId) { + if (e.providerId === ChatSetupRequests.providerId(this.configurationService)) { this.resolve(); } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { - if (e.id === defaultChat.providerId) { + if (e.id === ChatSetupRequests.providerId(this.configurationService)) { this.resolve(); } })); this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { - if (e.id === defaultChat.providerId) { + if (e.id === ChatSetupRequests.providerId(this.configurationService)) { this.resolve(); } })); @@ -440,7 +485,7 @@ class ChatSetupRequests extends Disposable { } private async findMatchingProviderSession(token: CancellationToken): Promise { - const sessions = await this.authenticationService.getSessions(defaultChat.providerId); + const sessions = await this.doGetSessions(ChatSetupRequests.providerId(this.configurationService)); if (token.isCancellationRequested) { return undefined; } @@ -456,6 +501,16 @@ class ChatSetupRequests extends Disposable { return undefined; } + private async doGetSessions(providerId: string): Promise { + try { + return await this.authenticationService.getSessions(providerId); + } catch (error) { + // ignore - errors can throw if a provider is not registered + } + + return []; + } + private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); } @@ -471,6 +526,11 @@ class ChatSetupRequests extends Disposable { } private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + if (ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + this.logService.trace('[chat setup] entitlement: enterprise provider, assuming Pro'); + return { entitlement: ChatEntitlement.Pro }; + } + if (token.isCancellationRequested) { return undefined; } @@ -537,6 +597,7 @@ class ChatSetupRequests extends Disposable { this.logService.trace(`[chat setup] entitlement: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, + tid: entitlementsResponse.analytics_tracking_id, quotaChat: entitlementsResponse.limited_user_quotas?.chat, quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, quotaResetDate: entitlementsResponse.limited_user_reset_date @@ -599,8 +660,8 @@ class ChatSetupRequests extends Disposable { const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None); if (!response) { - this.onUnknownSignUpError('[chat setup] sign-up: no response'); - return { errorCode: 1 }; + const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat setup] sign-up: no response'); + return retry ? this.signUpLimited(session) : { errorCode: 1 }; } if (response.res.statusCode && response.res.statusCode !== 200) { @@ -618,8 +679,8 @@ class ChatSetupRequests extends Disposable { // ignore - handled below } } - this.onUnknownSignUpError(`[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); - return { errorCode: response.res.statusCode }; + const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); + return retry ? this.signUpLimited(session) : { errorCode: response.res.statusCode }; } let responseText: string | null = null; @@ -630,8 +691,8 @@ class ChatSetupRequests extends Disposable { } if (!responseText) { - this.onUnknownSignUpError('[chat setup] sign-up: response has no content'); - return { errorCode: 2 }; + const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat setup] sign-up: response has no content'); + return retry ? this.signUpLimited(session) : { errorCode: 2 }; } let parsedResult: { subscribed: boolean } | undefined = undefined; @@ -639,8 +700,8 @@ class ChatSetupRequests extends Disposable { parsedResult = JSON.parse(responseText); this.logService.trace(`[chat setup] sign-up: response is ${responseText}`); } catch (err) { - this.onUnknownSignUpError(`[chat setup] sign-up: error parsing response (${err})`); - return { errorCode: 3 }; + const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat setup] sign-up: error parsing response (${err})`); + return retry ? this.signUpLimited(session) : { errorCode: 3 }; } // We have made it this far, so the user either did sign-up or was signed-up already. @@ -650,12 +711,22 @@ class ChatSetupRequests extends Disposable { return Boolean(parsedResult?.subscribed); } - private onUnknownSignUpError(logMessage: string): void { - this.dialogService.error(localize('unknownSignUpError', "An error occurred while signing up for Copilot Free."), localize('unknownSignUpErrorDetail', "Please try again.")); + private async onUnknownSignUpError(detail: string, logMessage: string): Promise { this.logService.error(logMessage); + + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSignUpError', "An error occurred while signing up for Copilot Free. Would you like to try again?"), + detail, + primaryButton: localize('retry', "Retry") + }); + + return confirmed; } private onUnprocessableSignUpError(logMessage: string, logDetails: string): void { + this.logService.error(logMessage); + this.dialogService.prompt({ type: Severity.Error, message: localize('unprocessableSignUpError', "An error occurred while signing up for Copilot Free."), @@ -671,7 +742,6 @@ class ChatSetupRequests extends Disposable { } ] }); - this.logService.error(logMessage); } override dispose(): void { @@ -689,12 +759,12 @@ type InstallChatClassification = { owner: 'bpasero'; comment: 'Provides insight into chat installation.'; installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; - signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; + installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' }; signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; }; type InstallChatEvent = { - installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted'; - signedIn: boolean; + installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession'; + installDuration: number; signUpErrorCode: number | undefined; }; @@ -710,9 +780,9 @@ class ChatSetupController extends Disposable { readonly onDidChange = this._onDidChange.event; private _step = ChatSetupStep.Initial; - get step(): ChatSetupStep { - return this._step; - } + get step(): ChatSetupStep { return this._step; } + + private willShutdown = false; constructor( private readonly context: ChatSetupContext, @@ -729,7 +799,10 @@ class ChatSetupController extends Disposable { @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IDialogService private readonly dialogService: IDialogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); @@ -738,6 +811,7 @@ class ChatSetupController extends Disposable { private registerListeners(): void { this._register(this.context.onDidChange(() => this._onDidChange.fire())); + this._register(this.lifecycleService.onWillShutdown(() => this.willShutdown = true)); } private setStep(step: ChatSetupStep): void { @@ -749,9 +823,10 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(): Promise { + async setup(options?: { forceSignIn: boolean }): Promise { + const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); - const badge = this.activityService.showViewContainerActivity(isCopilotEditsViewActive(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { + const badge = this.activityService.showViewContainerActivity(preferCopilotEditsView(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { badge: new ProgressBadge(() => title), }); @@ -760,46 +835,39 @@ class ChatSetupController extends Disposable { location: ProgressLocation.Window, command: TRIGGER_SETUP_COMMAND_ID, title, - }, () => this.doSetup()); + }, () => this.doSetup(options?.forceSignIn ?? false, watch)); } finally { badge.dispose(); } } - private async doSetup(): Promise { + private async doSetup(forceSignIn: boolean, watch: StopWatch): Promise { this.context.suspend(); // reduces flicker let focusChatInput = false; try { + const providerId = ChatSetupRequests.providerId(this.configurationService); let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; - // Entitlement Unknown: we need to sign-in user - if (this.context.state.entitlement === ChatEntitlement.Unknown) { + // Entitlement Unknown or `forceSignIn`: we need to sign-in user + if (this.context.state.entitlement === ChatEntitlement.Unknown || forceSignIn) { this.setStep(ChatSetupStep.SigningIn); - const result = await this.signIn(); + const result = await this.signIn(providerId); if (!result.session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); - return; // user cancelled + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + return; } session = result.session; entitlement = result.entitlement; } - if (!session) { - session = (await this.authenticationService.getSessions(defaultChat.providerId)).at(0); - if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); - return; // unexpected - } - } - const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({ message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") }); if (!trusted) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', signedIn: true, signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return; } @@ -807,7 +875,7 @@ class ChatSetupController extends Disposable { // Install this.setStep(ChatSetupStep.Installing); - await this.install(session, entitlement ?? this.context.state.entitlement); + await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch); const currentActiveElement = getActiveElement(); focusChatInput = activeElement === currentActiveElement || currentActiveElement === mainWindow.document.body; @@ -821,68 +889,120 @@ class ChatSetupController extends Disposable { } } - private async signIn(): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { + private async signIn(providerId: string): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; try { showCopilotView(this.viewsService, this.layoutService); - session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes[0]); + session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0]); + + this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); + this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); + entitlement = await this.requests.forceResolveEntitlement(session); + } catch (e) { + this.logService.error(`[chat setup] signIn: error ${e}`); + } - this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, defaultChat.providerId, session.account); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, defaultChat.providerId, session.account); - } catch (error) { - this.logService.error(`[chat setup] signIn: error ${error}`); + if (!session && !this.willShutdown) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName), + detail: localize('unknownSignInErrorDetail', "You must be signed in to use Copilot."), + primaryButton: localize('retry', "Retry") + }); + + if (confirmed) { + return this.signIn(providerId); + } } return { session, entitlement }; } - private async install(session: AuthenticationSession, entitlement: ChatEntitlement,): Promise { - const signedIn = !!session; - - let installResult: 'installed' | 'cancelled' | 'failedInstall' | undefined = undefined; + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch,): Promise { const wasInstalled = this.context.state.installed; - let didSignUp: boolean | { errorCode: number } | undefined = undefined; + let signUpResult: boolean | { errorCode: number } | undefined = undefined; + try { showCopilotView(this.viewsService, this.layoutService); - if (entitlement !== ChatEntitlement.Limited && entitlement !== ChatEntitlement.Pro && entitlement !== ChatEntitlement.Unavailable) { - didSignUp = await this.requests.signUpLimited(session); + if ( + entitlement !== ChatEntitlement.Limited && // User is not signed up to Copilot Free + entitlement !== ChatEntitlement.Pro && // User is not signed up to Copilot Pro + entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free + ) { + if (!session) { + try { + session = (await this.authenticationService.getSessions(providerId)).at(0); + } catch (error) { + // ignore - errors can throw if a provider is not registered + } + + if (!session) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + return; // unexpected + } + } - if (typeof didSignUp !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signedIn, signUpErrorCode: didSignUp.errorCode }); + signUpResult = await this.requests.signUpLimited(session); + + if (typeof signUpResult !== 'boolean' /* error */) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode }); } } + await this.doInstall(); + } catch (error) { + this.logService.error(`[chat setup] install: error ${error}`); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + return; + } + + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + + if (wasInstalled && signUpResult === true) { + refreshTokens(this.commandService); + } + + await Promise.race([ + timeout(5000), // helps prevent flicker with sign-in welcome view + Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 + ]); + } + + private async doInstall(): Promise { + let error: Error | undefined; + try { await this.extensionsWorkbenchService.install(defaultChat.extensionId, { enable: true, isApplicationScoped: true, // install into all profiles isMachineScoped: false, // do not ask to sync installEverywhere: true, // install in local and remote installPreReleaseVersion: this.productService.quality !== 'stable' - }, isCopilotEditsViewActive(this.viewsService) ? EditsViewId : ChatViewId); - - installResult = 'installed'; - } catch (error) { + }, preferCopilotEditsView(this.viewsService) ? EditsViewId : ChatViewId); + } catch (e) { this.logService.error(`[chat setup] install: error ${error}`); + error = e; + } - installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; - } finally { - if (wasInstalled && didSignUp) { - refreshTokens(this.commandService); - } + if (error) { + if (!this.willShutdown) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"), + detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined, + primaryButton: localize('retry', "Retry") + }); - if (installResult === 'installed') { - await Promise.race([ - timeout(5000), // helps prevent flicker with sign-in welcome view - Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 - ]); + if (confirmed) { + return this.doInstall(); + } } - } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn, signUpErrorCode: undefined }); + throw error; + } } } @@ -895,8 +1015,10 @@ class ChatSetupWelcomeContent extends Disposable { private readonly context: ChatSetupContext, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IDialogService private readonly dialogService: IDialogService, ) { super(); @@ -911,26 +1033,29 @@ class ChatSetupWelcomeContent extends Disposable { const header = localize({ key: 'header', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer.", this.context.state.installed ? 'command:github.copilot.open.walkthrough' : defaultChat.documentationUrl); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element); - const features = this.element.appendChild($('div.chat-features-container')); - this.element.appendChild(features); + const featuresParent = this.element.appendChild($('div.chat-features-container')); + this.element.appendChild(featuresParent); + + const featuresContainer = this.element.appendChild($('div')); + featuresParent.appendChild(featuresContainer); - const featureChatContainer = features.appendChild($('div.chat-feature-container')); + const featureChatContainer = featuresContainer.appendChild($('div.chat-feature-container')); featureChatContainer.appendChild(renderIcon(Codicon.code)); const featureChatLabel = featureChatContainer.appendChild($('span')); - featureChatLabel.textContent = localize('featureChat', "Code faster with completions and Inline Chat"); + featureChatLabel.textContent = localize('featureChat', "Code faster with Completions"); - const featureEditsContainer = features.appendChild($('div.chat-feature-container')); + const featureEditsContainer = featuresContainer.appendChild($('div.chat-feature-container')); featureEditsContainer.appendChild(renderIcon(Codicon.editSession)); const featureEditsLabel = featureEditsContainer.appendChild($('span')); - featureEditsLabel.textContent = localize('featureEdits', "Build features and resolve bugs with Copilot Edits"); + featureEditsLabel.textContent = localize('featureEdits', "Build features with Copilot Edits"); - const featureExploreContainer = features.appendChild($('div.chat-feature-container')); + const featureExploreContainer = featuresContainer.appendChild($('div.chat-feature-container')); featureExploreContainer.appendChild(renderIcon(Codicon.commentDiscussion)); const featureExploreLabel = featureExploreContainer.appendChild($('span')); - featureExploreLabel.textContent = localize('featureExplore', "Explore your codebase with chat"); + featureExploreLabel.textContent = localize('featureExplore', "Explore your codebase with Chat"); } // Limited SKU @@ -939,18 +1064,13 @@ class ChatSetupWelcomeContent extends Disposable { freeContainer.appendChild(this._register(markdown.render(new MarkdownString(free, { isTrusted: true, supportThemeIcons: true }))).element); // Setup Button - const actions: IAction[] = []; - if (this.context.state.installed) { - actions.push(toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signIn') })); - actions.push(toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signInGHE') })); - } const buttonContainer = this.element.appendChild($('p')); buttonContainer.classList.add('button-container'); - const button = this._register(actions.length === 0 ? new Button(buttonContainer, { - supportIcons: true, - ...defaultButtonStyles - }) : new ButtonWithDropdown(buttonContainer, { - actions, + const button = this._register(new ButtonWithDropdown(buttonContainer, { + actions: [ + toAction({ id: 'chatSetup.setupWithProvider', label: localize('setupWithProvider', "Sign in with a {0} Account", defaultChat.providerName), run: () => this.setupWithProvider(false) }), + toAction({ id: 'chatSetup.setupWithEnterpriseProvider', label: localize('setupWithEnterpriseProvider', "Sign in with a {0} Account", defaultChat.enterpriseProviderName), run: () => this.setupWithProvider(true) }) + ], addPrimaryActionToDropdown: false, contextMenuProvider: this.contextMenuService, supportIcons: true, @@ -971,7 +1091,7 @@ class ChatSetupWelcomeContent extends Disposable { this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(freeContainer, settingsContainer, button))); } - private update(freeContainer: HTMLElement, settingsContainer: HTMLElement, button: Button | ButtonWithDropdown): void { + private update(freeContainer: HTMLElement, settingsContainer: HTMLElement, button: ButtonWithDropdown): void { const showSettings = this.telemetryService.telemetryLevel !== TelemetryLevel.NONE; let showFree: boolean; let buttonLabel: string; @@ -999,7 +1119,7 @@ class ChatSetupWelcomeContent extends Disposable { switch (this.controller.step) { case ChatSetupStep.SigningIn: - buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName); + buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName); break; case ChatSetupStep.Installing: buttonLabel = localize('setupChatInstalling', "$(loading~spin) Getting Copilot Ready..."); @@ -1012,6 +1132,101 @@ class ChatSetupWelcomeContent extends Disposable { button.label = buttonLabel; button.enabled = this.controller.step === ChatSetupStep.Initial; } + + private async setupWithProvider(useEnterpriseProvider: boolean): Promise { + const registry = Registry.as(ConfigurationExtensions.Configuration); + registry.registerConfiguration({ + 'id': 'copilot.setup', + 'type': 'object', + 'properties': { + [defaultChat.providerSetting]: { + 'type': 'string' + }, + [defaultChat.providerUriSetting]: { + 'type': 'string' + } + } + }); + + if (useEnterpriseProvider) { + await this.configurationService.updateValue(defaultChat.providerSetting, defaultChat.enterpriseProviderId, ConfigurationTarget.USER); + const success = await this.handleEnterpriseInstance(); + if (!success) { + return; // not properly configured, abort + } + } else { + await this.configurationService.updateValue(defaultChat.providerSetting, undefined, ConfigurationTarget.USER); + await this.configurationService.updateValue(defaultChat.providerUriSetting, undefined, ConfigurationTarget.USER); + } + + return this.controller.setup({ forceSignIn: true }); + } + + private async handleEnterpriseInstance(): Promise { + const uri = this.configurationService.getValue(defaultChat.providerUriSetting); + if (uri) { + return true; // already setup + } + + let isSingleWord = false; + const result = await this.quickInputService.input({ + prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.enterpriseProviderName), + placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'), + validateInput: async value => { + isSingleWord = false; + if (!value) { + return undefined; + } + + if (/^[a-zA-Z\-_]+$/.test(value)) { + isSingleWord = true; + return { + content: localize('willResolveTo', "Will resolve to {0}", `https://${value}.ghe.com`), + severity: Severity.Info + }; + } else { + const regex = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/; + if (!regex.test(value)) { + return { + content: localize('invalidEnterpriseInstance', 'Please enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.enterpriseProviderName), + severity: Severity.Error + }; + } + } + + return undefined; + } + }); + + if (!result) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('enterpriseSetupError', "The provided {0} instance is invalid. Would you like to enter it again?", defaultChat.enterpriseProviderName), + primaryButton: localize('retry', "Retry") + }); + + if (confirmed) { + return this.handleEnterpriseInstance(); + } + + return false; + } + + let resolvedUri = result; + if (isSingleWord) { + resolvedUri = `https://${resolvedUri}.ghe.com`; + } else { + const normalizedUri = result.toLowerCase(); + const hasHttps = normalizedUri.startsWith('https://'); + if (!hasHttps) { + resolvedUri = `https://${result}`; + } + } + + await this.configurationService.updateValue(defaultChat.providerUriSetting, resolvedUri, ConfigurationTarget.USER); + + return true; + } } //#endregion @@ -1020,7 +1235,7 @@ class ChatSetupWelcomeContent extends Disposable { interface IChatSetupContextState { entitlement: ChatEntitlement; - triggered?: boolean; + hidden?: boolean; installed?: boolean; registered?: boolean; } @@ -1033,7 +1248,7 @@ class ChatSetupContext extends Disposable { private readonly signedOutContextKey = ChatContextKeys.Setup.signedOut.bindTo(this.contextKeyService); private readonly limitedContextKey = ChatContextKeys.Setup.limited.bindTo(this.contextKeyService); private readonly proContextKey = ChatContextKeys.Setup.pro.bindTo(this.contextKeyService); - private readonly triggeredContext = ChatContextKeys.Setup.triggered.bindTo(this.contextKeyService); + private readonly hiddenContext = ChatContextKeys.Setup.hidden.bindTo(this.contextKeyService); private readonly installedContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); private _state: IChatSetupContextState = this.storageService.getObject(ChatSetupContext.CHAT_SETUP_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown }; @@ -1063,11 +1278,11 @@ class ChatSetupContext extends Disposable { private async checkExtensionInstallation(): Promise { - // Await extensions to be ready to be queries + // Await extensions to be ready to be queried await this.extensionsWorkbenchService.queryLocal(); // Listen to change and process extensions once - this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, (e) => { + this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, e => { if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.extensionId)) { return; // unrelated event } @@ -1078,21 +1293,21 @@ class ChatSetupContext extends Disposable { } update(context: { installed: boolean }): Promise; - update(context: { triggered: boolean }): Promise; + update(context: { hidden: boolean }): Promise; update(context: { entitlement: ChatEntitlement }): Promise; - update(context: { installed?: boolean; triggered?: boolean; entitlement?: ChatEntitlement }): Promise { + update(context: { installed?: boolean; hidden?: boolean; entitlement?: ChatEntitlement }): Promise { this.logService.trace(`[chat setup] update(): ${JSON.stringify(context)}`); if (typeof context.installed === 'boolean') { this._state.installed = context.installed; if (context.installed) { - context.triggered = true; // allows to fallback to setup view if the extension is uninstalled + context.hidden = false; // allows to fallback to setup view if the extension is uninstalled } } - if (typeof context.triggered === 'boolean') { - this._state.triggered = context.triggered; + if (typeof context.hidden === 'boolean') { + this._state.hidden = context.hidden; } if (typeof context.entitlement === 'number') { @@ -1119,7 +1334,7 @@ class ChatSetupContext extends Disposable { private updateContextSync(): void { this.logService.trace(`[chat setup] updateContext(): ${JSON.stringify(this._state)}`); - if (this._state.triggered && !this._state.installed) { + if (!this._state.hidden && !this._state.installed) { // this is ugly but fixes flicker from a previous chat install this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION); @@ -1129,7 +1344,7 @@ class ChatSetupContext extends Disposable { this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); this.proContextKey.set(this._state.entitlement === ChatEntitlement.Pro); - this.triggeredContext.set(!!this._state.triggered); + this.hiddenContext.set(!!this._state.hidden); this.installedContext.set(!!this._state.installed); this._onDidChange.fire(); @@ -1149,25 +1364,6 @@ class ChatSetupContext extends Disposable { //#endregion -function isCopilotEditsViewActive(viewsService: IViewsService): boolean { - return viewsService.getFocusedView()?.id === EditsViewId; -} - -function showCopilotView(viewsService: IViewsService, layoutService: IWorkbenchLayoutService): Promise { - - // Ensure main window is in front - if (layoutService.activeContainer !== layoutService.mainContainer) { - layoutService.mainContainer.focus(); - } - - // Bring up the correct view - if (isCopilotEditsViewActive(viewsService)) { - return showEditsView(viewsService); - } else { - return showChatView(viewsService); - } -} - function refreshTokens(commandService: ICommandService): void { // ugly, but we need to signal to the extension that entitlements changed commandService.executeCommand('github.copilot.signIn'); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 742ae8cbf6b..8455f189e6e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -185,6 +185,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { }, enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, editorOverflowWidgetsDomNode: editorOverflowNode, + enableWorkingSet: this.chatOptions.location === ChatAgentLocation.EditingSession ? 'explicit' : undefined }, { listForeground: SIDE_BAR_FOREGROUND, diff --git a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts index b4f12651773..92f03d8e58b 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -15,7 +15,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorunWithStore, observableFromEvent } from '../../../../base/common/observable.js'; import { extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -38,7 +38,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; +import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; import { IChatFollowup, IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; @@ -151,6 +151,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private bodyDimension: dom.Dimension | undefined; private visibleChangeCount = 0; private requestInProgress: IContextKey; + private isRequestPaused: IContextKey; + private canRequestBePaused: IContextKey; private agentInInput: IContextKey; private _visible = false; @@ -187,6 +189,8 @@ export class ChatWidget extends Disposable implements IChatWidget { return this._viewModel; } + private _editingSession: IChatEditingSession | undefined; + private parsedChatRequest: IParsedChatRequest | undefined; get parsedInput() { if (this.parsedChatRequest === undefined) { @@ -246,41 +250,47 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this)); this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService); this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService); + this.isRequestPaused = ChatContextKeys.isRequestPaused.bindTo(contextKeyService); + this.canRequestBePaused = ChatContextKeys.canRequestBePaused.bindTo(contextKeyService); this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); - const chatEditingSessionDisposables = this._register(new DisposableStore()); - this._register(autorun(r => { - const session = this.chatEditingService.currentEditingSessionObs.read(r); + const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel); + + this._register(autorunWithStore((r, store) => { + + const viewModel = viewModelObs.read(r); + const sessions = chatEditingService.editingSessionsObs.read(r); + + const session = sessions.find(candidate => candidate.chatSessionId === viewModel?.sessionId); + this._editingSession = undefined; + this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc. + if (!session) { + // none or for a different chat widget return; } - if (session.chatSessionId !== this.viewModel?.sessionId) { - // this chat editing session is for a different chat widget - return; - } - // make sure to clean up anything related to the prev session (if any) - chatEditingSessionDisposables.clear(); - this.renderChatEditingSessionState(null); // this is necessary to make sure we dispose previous buttons, etc. - chatEditingSessionDisposables.add(session.onDidChange(() => { - this.renderChatEditingSessionState(session); + this._editingSession = session; + + store.add(session.onDidChange(() => { + this.renderChatEditingSessionState(); })); - chatEditingSessionDisposables.add(session.onDidDispose(() => { - chatEditingSessionDisposables.clear(); - this.renderChatEditingSessionState(null); + store.add(session.onDidDispose(() => { + this._editingSession = undefined; + this.renderChatEditingSessionState(); })); - chatEditingSessionDisposables.add(this.onDidChangeParsedInput(() => { - this.renderChatEditingSessionState(session); + store.add(this.onDidChangeParsedInput(() => { + this.renderChatEditingSessionState(); })); - chatEditingSessionDisposables.add(this.inputEditor.onDidChangeModelContent(() => { + store.add(this.inputEditor.onDidChangeModelContent(() => { if (this.getInput() === '') { this.refreshParsedInput(); - this.renderChatEditingSessionState(session); + this.renderChatEditingSessionState(); } })); - this.renderChatEditingSessionState(session); + this.renderChatEditingSessionState(); })); if (this._location.location === ChatAgentLocation.EditingSession) { @@ -289,7 +299,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const sessionId = this._viewModel?.sessionId; if (sessionId) { if (sessionId !== currentEditSession?.chatSessionId) { - currentEditSession = await this.chatEditingService.startOrContinueEditingSession(sessionId); + currentEditSession = await chatEditingService.startOrContinueEditingSession(sessionId); } } else { if (currentEditSession) { @@ -561,8 +571,14 @@ export class ChatWidget extends Disposable implements IChatWidget { } private renderWelcomeViewContentIfNeeded() { + if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { + return; + } + + const numItems = this.viewModel?.getItems().length ?? 0; const welcomeContent = this.viewModel?.model.welcomeMessage ?? this.persistedWelcomeMessage; - if (welcomeContent && this.welcomeMessageContainer.children.length === 0 && !this.viewOptions.renderStyle) { + if (welcomeContent && !numItems && (this.welcomeMessageContainer.children.length === 0 || this.location === ChatAgentLocation.EditingSession)) { + dom.clearNode(this.welcomeMessageContainer); const tips = this.viewOptions.supportsAdditionalParticipants ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); @@ -574,15 +590,17 @@ export class ChatWidget extends Disposable implements IChatWidget { dom.append(this.welcomeMessageContainer, welcomePart.element); } - if (!this.viewOptions.renderStyle && this.viewModel) { - const treeItems = this.viewModel.getItems(); - dom.setVisibility(treeItems.length === 0, this.welcomeMessageContainer); - dom.setVisibility(treeItems.length !== 0, this.listContainer); + if (this.viewModel) { + dom.setVisibility(numItems === 0, this.welcomeMessageContainer); + dom.setVisibility(numItems !== 0, this.listContainer); } } - private async renderChatEditingSessionState(session: IChatEditingSession | null) { - this.inputPart.renderChatEditingSessionState(session, this); + private async renderChatEditingSessionState() { + if (!this.inputPart) { + return; + } + this.inputPart.renderChatEditingSessionState(this._editingSession ?? null, this); if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); @@ -650,7 +668,8 @@ export class ChatWidget extends Disposable implements IChatWidget { noCommandDetection: true, attempt: request.attempt + 1, location: this.location, - userSelectedModelId: this.input.currentLanguageModel + userSelectedModelId: this.input.currentLanguageModel, + hasInstructionAttachments: this.input.hasInstructionAttachments, }; this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e)); } @@ -672,6 +691,7 @@ export class ChatWidget extends Disposable implements IChatWidget { keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO setRowLineHeight: false, filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined, + scrollToActiveElement: true, overrideStyles: { listFocusBackground: this.styles.listBackground, listInactiveFocusBackground: this.styles.listBackground, @@ -761,7 +781,8 @@ export class ChatWidget extends Disposable implements IChatWidget { renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, - enableImplicitContext: this.viewOptions.enableImplicitContext + enableImplicitContext: this.viewOptions.enableImplicitContext, + renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit' }, this.styles, () => this.collectInputState() @@ -827,16 +848,21 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidChangeContentHeight.fire(); })); this._register(this.inputPart.attachmentModel.onDidChangeContext(() => { - if (this.chatEditingService.currentEditingSession && this.chatEditingService.currentEditingSession?.chatSessionId === this.viewModel?.sessionId) { + if (this._editingSession) { // TODO still needed? Do this inside input part and fire onDidChangeHeight? - this.renderChatEditingSessionState(this.chatEditingService.currentEditingSession); + this.renderChatEditingSessionState(); } })); this._register(this.inputEditor.onDidChangeModelContent(() => { this.parsedChatRequest = undefined; this.updateChatInputContext(); })); - this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); + this._register(this.chatAgentService.onDidChangeAgents(() => { + this.parsedChatRequest = undefined; + + // Tools agent loads -> welcome content changes + this.renderWelcomeViewContentIfNeeded(); + })); } private onDidStyleChange(): void { @@ -845,6 +871,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); } + togglePaused() { + this.viewModel?.model.toggleLastRequestPaused(); + } + setModel(model: IChatModel, viewState: IChatViewState): void { if (!this.container) { throw new Error('Call render() before setModel()'); @@ -864,14 +894,16 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.requestInProgress.set(this.viewModel.requestInProgress); + this.isRequestPaused.set(this.viewModel.requestPausibility === ChatPauseState.Paused); + this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); this.onDidChangeItems(); if (events.some(e => e?.kind === 'addRequest') && this.visible) { this.scrollToEnd(); } - if (this.chatEditingService.currentEditingSession && this.chatEditingService.currentEditingSession?.chatSessionId === this.viewModel?.sessionId) { - this.renderChatEditingSessionState(this.chatEditingService.currentEditingSession); + if (this._editingSession) { + this.renderChatEditingSessionState(); } })); this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { @@ -985,7 +1017,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } private async _acceptInput(query: { query: string } | { prefix: string } | undefined, options?: IChatAcceptInputOptions): Promise { - if (this.viewModel?.requestInProgress) { + if (this.viewModel?.requestInProgress && this.viewModel.requestPausibility !== ChatPauseState.Paused) { return; } @@ -1002,25 +1034,35 @@ export class ChatWidget extends Disposable implements IChatWidget { `${query.prefix} ${editorValue}`; const isUserQuery = !query || 'prefix' in query; - + const { promptInstructions } = this.inputPart.attachmentModel; + const instructionsEnabled = promptInstructions.featureEnabled; + if (instructionsEnabled) { + // instruction files may have nested child references to other prompt + // files that are resolved asynchronously, hence we need to wait for + // the entire prompt instruction tree to be processed + const instructionsStarted = performance.now(); + await promptInstructions.allSettled(); + // allow-any-unicode-next-line + this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); + } let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); let workingSet: URI[] | undefined; - if (this.location === ChatAgentLocation.EditingSession) { - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); + if (this.viewOptions.enableWorkingSet !== undefined) { + const currentEditingSession = this._editingSession; const unconfirmedSuggestions = new ResourceSet(); const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set const editingSessionAttachedContext: IChatRequestVariableEntry[] = []; // Pick up everything that the user sees is part of the working set. // This should never exceed the maximum file entries limit above. - for (const v of this.inputPart.chatEditWorkingSetFiles) { + for (const { uri, isMarkedReadonly } of this.inputPart.chatEditWorkingSetFiles) { // Skip over any suggested files that haven't been confirmed yet in the working set - if (currentEditingSession?.workingSet.get(v)?.state === WorkingSetEntryState.Suggested) { - unconfirmedSuggestions.add(v); + if (currentEditingSession?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) { + unconfirmedSuggestions.add(uri); } else { - uniqueWorkingSetEntries.add(v); - editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(v)); + uniqueWorkingSetEntries.add(uri); + editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(uri, undefined, isMarkedReadonly)); } } let maximumFileEntries = this.chatEditingService.editingSessionFileLimit - editingSessionAttachedContext.length; @@ -1032,6 +1074,13 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + // add prompt instruction references to the attached context, if enabled + const promptInstructionUris = new ResourceSet(promptInstructions.chatAttachments.map((v) => v.value) as URI[]); + if (instructionsEnabled) { + editingSessionAttachedContext + .push(...promptInstructions.chatAttachments); + } + for (const file of uniqueWorkingSetEntries) { // Make sure that any files that we sent are part of the working set // but do not permanently add file variables from previous requests to the working set @@ -1045,7 +1094,7 @@ export class ChatWidget extends Disposable implements IChatWidget { for (const variable of request.variableData.variables) { if (URI.isUri(variable.value) && variable.isFile && maximumFileEntries > 0) { const uri = variable.value; - if (!uniqueWorkingSetEntries.has(uri)) { + if (!uniqueWorkingSetEntries.has(uri) && !promptInstructionUris.has(uri)) { editingSessionAttachedContext.push(variable); uniqueWorkingSetEntries.add(variable.value); maximumFileEntries -= 1; @@ -1070,6 +1119,8 @@ export class ChatWidget extends Disposable implements IChatWidget { currentEditingSession?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } + this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { userSelectedModelId: this.inputPart.currentLanguageModel, location: this.location, @@ -1078,6 +1129,7 @@ export class ChatWidget extends Disposable implements IChatWidget { attachedContext, workingSet, noCommandDetection: options?.noCommandDetection, + hasInstructionAttachments: this.inputPart.hasInstructionAttachments, }); if (result) { @@ -1095,6 +1147,13 @@ export class ChatWidget extends Disposable implements IChatWidget { } } }); + + const RESPONSE_TIMEOUT = 20000; + setTimeout(() => { + // Stop the signal if the promise is still unresolved + this.chatAccessibilityService.acceptResponse(undefined, requestId, options?.isVoiceInput); + }, RESPONSE_TIMEOUT); + return result.responseCreatedPromise; } } diff --git a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css index 92049236a3a..44f05c472b0 100644 --- a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -57,7 +57,7 @@ } .interactive-item-container .value .rendered-markdown [data-code] { - margin: 16px 0; + margin: 0 0 16px 0; } .interactive-result-code-block { diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index f4a6d525ce8..e25bcbb6c82 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -25,8 +25,8 @@ import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; import { IChatRequestVariableValue, IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; -import { PromptFileReference } from '../../common/promptFileReference.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; @@ -130,7 +130,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { // use `ChatFileReference` for file references and `IDynamicVariable` for other variables - const promptSnippetsEnabled = PromptFileReference.promptSnippetsEnabled(this.configService); + const promptSnippetsEnabled = PromptFilesConfig.enabled(this.configService); const variable = (ref.id === 'vscode.file' && promptSnippetsEnabled) ? this.instantiationService.createInstance(ChatFileReference, ref) : ref; @@ -140,14 +140,14 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC this.widget.refreshParsedInput(); // if the `prompt snippets` feature is enabled, and file is a `prompt snippet`, - // start resolving nested file references immediatelly and subscribe to updates - if (variable instanceof ChatFileReference && variable.isPromptSnippetFile) { + // start resolving nested file references immediately and subscribe to updates + if (variable instanceof ChatFileReference && variable.isPromptSnippet) { // subscribe to variable changes variable.onUpdate(() => { this.updateDecorations(); }); // start resolving the file references - variable.resolve(); + variable.start(); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index 2a5ecdf5048..9b225f8c189 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -7,23 +7,25 @@ import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; import { IDynamicVariable } from '../../../common/chatVariables.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; -import { PromptFileReference } from '../../../common/promptFileReference.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** * A wrapper class for an `IDynamicVariable` object that that adds functionality * to parse nested file references of this variable. - * See {@link PromptFileReference} for details. + * See {@link FilePromptParser} for details. */ -export class ChatFileReference extends PromptFileReference implements IDynamicVariable { +export class ChatFileReference extends FilePromptParser implements IDynamicVariable { /** * @throws if the `data` reference is no an instance of `URI`. */ constructor( public readonly reference: IDynamicVariable, - @IFileService fileService: IFileService, + @IInstantiationService initService: IInstantiationService, @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, ) { const { data } = reference; @@ -32,7 +34,7 @@ export class ChatFileReference extends PromptFileReference implements IDynamicVa `Variable data must be an URI, got '${data}'.`, ); - super(data, fileService, configService); + super(data, [], initService, configService, logService); } /** diff --git a/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts new file mode 100644 index 00000000000..20114790df8 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +/** + * Resizes an image provided as a UInt8Array string. Resizing is based on Open AI's algorithm for tokenzing images. + * https://platform.openai.com/docs/guides/vision#calculating-costs + * @param data - The UInt8Array string of the image to resize. + * @returns A promise that resolves to the UInt8Array string of the resized image. + */ + +export async function resizeImage(data: Uint8Array): Promise { + const blob = new Blob([data]); + const img = new Image(); + const url = URL.createObjectURL(blob); + img.src = url; + + return new Promise((resolve, reject) => { + img.onload = () => { + URL.revokeObjectURL(url); + let { width, height } = img; + + if (width < 768 || height < 768) { + resolve(data); + return; + } + + // Calculate the new dimensions while maintaining the aspect ratio + if (width > 2048 || height > 2048) { + const scaleFactor = 2048 / Math.max(width, height); + width = Math.round(width * scaleFactor); + height = Math.round(height * scaleFactor); + } + + const scaleFactor = 768 / Math.min(width, height); + width = Math.round(width * scaleFactor); + height = Math.round(height * scaleFactor); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(img, 0, 0, width, height); + canvas.toBlob((blob) => { + if (blob) { + const reader = new FileReader(); + reader.onload = () => { + resolve(new Uint8Array(reader.result as ArrayBuffer)); + }; + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(blob); + } else { + reject(new Error('Failed to create blob from canvas')); + } + }, 'image/png'); + } else { + reject(new Error('Failed to get canvas context')); + } + }; + img.onerror = (error) => { + URL.revokeObjectURL(url); + reject(error); + }; + }); +} diff --git a/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index d53c0db6859..9e3d66aeca9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -187,7 +187,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); const invocationMessage = prepared?.invocationMessage ?? defaultMessage; if (tool.data.id !== 'vscode_editFile') { - toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); + toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.pastTenseMessage, prepared?.tooltip, prepared?.confirmationMessages); model.acceptResponseProgress(request, toolInvocation); if (prepared?.confirmationMessages) { const userConfirmed = await toolInvocation.confirmed.p; diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chat.css b/code/src/vs/workbench/contrib/chat/browser/media/chat.css index f9065621637..d931944021c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -200,6 +200,10 @@ margin-bottom: 8px; } +.interactive-item-container .value .rendered-markdown .codicon { + font-size: inherit; +} + .interactive-item-container .value .rendered-markdown blockquote { margin: 0px; padding: 0px 16px 0 10px; @@ -260,6 +264,7 @@ overflow-wrap: anywhere; } +.interactive-item-container .value > :last-child, .interactive-item-container .value > :last-child.rendered-markdown > :last-child { margin-bottom: 0px; } @@ -291,8 +296,8 @@ margin: 16px 0 8px 0; } -.interactive-item-container.editing-session .value .rendered-markdown p { - margin: 0; +.interactive-item-container.editing-session .value .rendered-markdown p:has(+ [data-code] > .chat-codeblock-pill-widget) { + margin-bottom: 8px; } .interactive-item-container.editing-session .value .rendered-markdown h3 { @@ -301,10 +306,6 @@ font-weight: unset; } -.interactive-item-container.editing-session .value .rendered-markdown [data-code] { - margin: 8px 0 16px 0; -} - .interactive-item-container .value .rendered-markdown { /* Codicons next to text need to be aligned with the text */ .codicon { @@ -334,7 +335,7 @@ } } -.interactive-item-container .value .rendered-markdown p { +.interactive-item-container .value .rendered-markdown { line-height: 1.5em; } @@ -387,22 +388,19 @@ have to be updated for changes to the rules above, or to support more deeply nes /* #endregion list indent rules */ -.interactive-item-container .value .rendered-markdown li { - line-height: 1.3rem; -} - .interactive-item-container .value .rendered-markdown img { max-width: 100%; } -.interactive-item-container .monaco-tokenized-source, -.interactive-item-container code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; +.chat-tool-hover, .interactive-item-container { + .monaco-tokenized-source, code { + font-family: var(--monaco-monospace-font); + font-size: 12px; + color: var(--vscode-textPreformat-foreground); + background-color: var(--vscode-textPreformat-background); + padding: 1px 3px; + border-radius: 4px; + } } .interactive-item-container.interactive-item-compact { @@ -566,6 +564,12 @@ have to be updated for changes to the rules above, or to support more deeply nes display: inherit; } +.interactive-session .chat-editing-session .monaco-list-row .chat-collapsible-list-action-bar .action-label.checked { + color: var(--vscode-inputOption-activeForeground); + background-color: var(--vscode-inputOption-activeBackground); + box-shadow: inset 0 0 0 1px var(--vscode-inputOption-activeBorder); +} + .interactive-session .chat-editing-session .chat-editing-session-container.show-file-icons .monaco-scrollable-element .monaco-list-rows .monaco-list-row { border-radius: 2px; } @@ -800,6 +804,10 @@ have to be updated for changes to the rules above, or to support more deeply nes } } +.chat-execute-toolbar .codicon.codicon-debug-pause { + color: var(--vscode-icon-foreground) !important; +} + .interactive-session .chat-input-toolbars .chat-modelPicker-item .action-label { height: 16px; padding: 3px 0px 3px 6px; @@ -925,6 +933,70 @@ have to be updated for changes to the rules above, or to support more deeply nes border-color: var(--vscode-notificationsWarningIcon-foreground); } +/** + * Styles for the `prompt instructions` attachment widget. + */ +.chat-attached-context .chat-prompt-instructions-attachments { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} +.chat-attached-context .chat-prompt-instructions-attachment { + display: flex; + gap: 4px; +} +.chat-attached-context .chat-prompt-instructions-attachment .codicon { + color: inherit; + text-decoration: none; +} +.chat-attached-context .chat-prompt-instructions-attachment .chat-implicit-hint { + opacity: 0.7; + font-size: .9em; +} +.chat-attached-context .chat-prompt-instructions-attachment.warning { + color: var(--vscode-notificationsWarningIcon-foreground); +} +.chat-attached-context .chat-prompt-instructions-attachment.error { + color: var(--vscode-notificationsErrorIcon-foreground); +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled { + border-style: dashed; + opacity: 0.75; +} +/* + * This overly-specific CSS selector is needed to beat priority of some + * styles applied on the the `.chat-attached-context-attachment` element. + */ +.chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.error.implicit, +.chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.warning.implicit { + border: 1px solid currentColor; +} +/* + * If in one of the non-normal states, make sure the `main icon` of + * the component has the same color as the component itself + */ +.chat-attached-context .chat-prompt-instructions-attachment.error .monaco-icon-label::before, +.chat-attached-context .chat-prompt-instructions-attachment.warning .monaco-icon-label::before, +.chat-attached-context .chat-prompt-instructions-attachment.disabled .monaco-icon-label::before { + color: inherit; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled .monaco-icon-label::before { + font-style: italic; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled:hover { + opacity: 1; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled .chat-implicit-hint, +.chat-attached-context .chat-prompt-instructions-attachment.disabled .label-name { + font-style: italic; + text-decoration: line-through; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled:focus { + outline: none; + border-color: var(--vscode-focusBorder); +} + .chat-notification-widget .chat-warning-codicon .codicon-warning, .chat-quota-error-widget .codicon-warning { color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ @@ -1380,6 +1452,10 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 4px 8px; } +.interactive-item-container .chat-confirmation-widget .rendered-markdown [data-code] { + margin-bottom: 8px; +} + .interactive-item-container .chat-command-button .monaco-button .codicon { margin-left: 0; margin-top: 1px; diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index b7a54ada1a2..f33b5a589c9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -13,6 +13,7 @@ align-items: center; z-index: 10; box-shadow: 0 2px 8px var(--vscode-widget-shadow); + overflow: hidden; } @keyframes pulse { @@ -37,6 +38,8 @@ padding: 0px 5px; font-size: 12px; font-variant-numeric: tabular-nums; + overflow: hidden; + white-space: nowrap; } .chat-editor-overlay-widget.busy .chat-editor-overlay-progress { @@ -48,6 +51,33 @@ /* font-style: italic; */ } +@keyframes ellipsis { + 0% { + content: ""; + } + 25% { + content: "."; + } + 50% { + content: ".."; + } + 75% { + content: "..."; + } + 100% { + content: ""; + } +} + +.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .busy-label::after { + content: ""; + display: inline-flex; + white-space: nowrap; + overflow: hidden; + width: 3ch; + animation: ellipsis steps(4, end) 1s infinite; +} + .chat-editor-overlay-widget.busy .chat-editor-overlay-toolbar { display: none; } @@ -88,3 +118,19 @@ color: var(--vscode-button-foreground); opacity: 1; } + +.chat-editor-overlay-widget .action-item.auto { + position: relative; + overflow: hidden; +} + +.chat-editor-overlay-widget .action-item.auto::before { + content: ''; + position: absolute; + top: 0; + left: var(--vscode-action-item-auto-timeout, -100%); + width: 100%; + height: 100%; + background-color: var(--vscode-toolbar-hoverBackground); + transition: left 0.5s linear; +} diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index 8b3a81f428e..785aeaa2fa7 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -11,6 +11,8 @@ } .chat-features-container { + display: flex; + justify-content: center; text-align: initial; border-radius: 2px; border: 1px solid var(--vscode-chat-requestBorder); @@ -46,12 +48,4 @@ .monaco-button-dropdown .monaco-text-button { width: 100%; } - - /** Single Button */ - p > .monaco-button { - text-align: center; - display: inline-block; - width: 100%; - padding: 4px 7px; - } } diff --git a/code/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/code/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 359b47eb454..259e9ecc71e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/code/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -5,17 +5,20 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { SaveReason } from '../../../../common/editor.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -29,8 +32,8 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo super(); const editTool = instantiationService.createInstance(EditTool); - this._register(toolsService.registerToolData(editTool)); - this._register(toolsService.registerToolImplementation(editTool.id, editTool)); + this._register(toolsService.registerToolData(editToolData)); + this._register(toolsService.registerToolImplementation(editToolData.id, editTool)); } } @@ -49,7 +52,7 @@ Avoid repeating existing code, instead use comments to represent regions of unch { changed code } // ...existing code... -Here is an example of how you should format an edit to an existing Person class: +Here is an example of how you should use format an edit to an existing Person class: class Person { // ...existing code... age: number; @@ -60,53 +63,60 @@ class Person { } `; -class EditTool implements IToolData, IToolImpl { - readonly id = 'vscode_editFile'; - readonly tags = ['vscode_editing']; - readonly displayName = localize('chat.tools.editFile', "Edit File"); - readonly modelDescription = `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. ${codeInstructions}`; - readonly inputSchema: IJSONSchema; +const editToolData: IToolData = { + id: 'vscode_editFile', + tags: ['vscode_editing'], + displayName: localize('chat.tools.editFile', "Edit File"), + modelDescription: `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`, + inputSchema: { + type: 'object', + properties: { + explanation: { + type: 'string', + description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', + }, + filePath: { + type: 'string', + description: 'An absolute path to the file to edit', + }, + code: { + type: 'string', + description: 'The code change to apply to the file. ' + codeInstructions + } + }, + required: ['explanation', 'filePath', 'code'] + } +}; + +class EditTool implements IToolImpl { constructor( @IChatService private readonly chatService: IChatService, @IChatEditingService private readonly chatEditingService: IChatEditingService, - @ICodeMapperService private readonly codeMapperService: ICodeMapperService - ) { - this.inputSchema = { - type: 'object', - properties: { - filePath: { - type: 'string', - description: 'An absolute path to the file to edit', - }, - explanation: { - type: 'string', - description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', - }, - code: { - type: 'string', - description: 'The code change to apply to the file. ' + codeInstructions - } - }, - required: ['filePath', 'explanation', 'code'] - }; - } + @ICodeMapperService private readonly codeMapperService: ICodeMapperService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, + @ITextFileService private readonly textFileService: ITextFileService, + ) { } async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { if (!invocation.context) { throw new Error('toolInvocationToken is required for this tool'); } - const parameters = invocation.parameters as EditToolParams; - if (!parameters.filePath || !parameters.explanation || !parameters.code) { - throw new Error(`Invalid tool input: ${JSON.stringify(parameters)}`); + const uri = URI.file(parameters.filePath); + if (!this.workspaceContextService.isInsideWorkspace(uri)) { + throw new Error(`File ${parameters.filePath} can't be edited because it's not inside the current workspace`); + } + + if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { + throw new Error(`File ${parameters.filePath} can't be edited because it is configured to be ignored by Copilot`); } const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; const request = model.getRequests().at(-1)!; - const uri = URI.file(parameters.filePath); model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('\n````\n') @@ -126,7 +136,8 @@ class EditTool implements IToolData, IToolImpl { const result = await this.codeMapperService.mapCode({ codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], - conversation: [] + location: 'tool', + chatRequestId: invocation.chatRequestId }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); @@ -139,19 +150,35 @@ class EditTool implements IToolData, IToolImpl { throw new Error(result.errorMessage); } + let dispose: IDisposable; await new Promise((resolve) => { - autorun((r) => { + // The file will not be modified until the first edits start streaming in, + // so wait until we see that it _was_ modified before waiting for it to be done. + let wasFileBeingModified = false; + + dispose = autorun((r) => { const currentEditingSession = this.chatEditingService.currentEditingSessionObs.read(r); const entries = currentEditingSession?.entries.read(r); const currentFile = entries?.find((e) => e.modifiedURI.toString() === uri.toString()); - if (currentFile && !currentFile.isCurrentlyBeingModified.read(r)) { - resolve(true); + if (currentFile) { + if (currentFile.isCurrentlyBeingModified.read(r)) { + wasFileBeingModified = true; + } else if (wasFileBeingModified) { + resolve(true); + } } }); + }).finally(() => { + dispose.dispose(); + }); + + await this.textFileService.save(uri, { + reason: SaveReason.AUTO, + skipSaveParticipants: true, }); return { - content: [{ kind: 'text', value: 'Success' }] + content: [{ kind: 'text', value: 'The file was edited successfully' }] }; } } diff --git a/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 6942c082ded..2e40bd0232f 100644 --- a/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Event } from '../../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -17,7 +17,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { ChatAgentLocation } from '../../common/chatAgents.js'; +import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -127,6 +127,7 @@ export class ChatViewWelcomePart extends Disposable { @IOpenerService private openerService: IOpenerService, @IInstantiationService private instantiationService: IInstantiationService, @ILogService private logService: ILogService, + @IChatAgentService chatAgentService: IChatAgentService, ) { super(); this.element = dom.$('.chat-welcome-view'); @@ -145,9 +146,11 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function') { + if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function' && chatAgentService.toolsAgentModeEnabled) { + // Override welcome message for the agent. Sort of a hack, should it come from the participant? This case is different because the welcome content typically doesn't change per ChatWidget + content.message = new MarkdownString('Ask Copilot to edit your files in agent mode. Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.'); const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); - featureIndicator.textContent = localize('preview', 'PREVIEW'); + featureIndicator.textContent = localize('preview', "PREVIEW"); } // Message diff --git a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts index 8045fc3d091..2a0c9041319 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -95,6 +95,7 @@ export function isChatWelcomeMessageContent(obj: any): obj is IChatWelcomeMessag export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + setRequestPaused?(requestId: string, isPaused: boolean): void; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult; provideChatTitle?: (history: IChatAgentHistoryEntry[], token: CancellationToken) => Promise; @@ -206,8 +207,9 @@ export interface IChatAgentService { * undefined when an agent was removed */ readonly onDidChangeAgents: Event; + readonly onDidChangeToolsAgentModeEnabled: Event; readonly toolsAgentModeEnabled: boolean; - toggleToolsAgentMode(): void; + toggleToolsAgentMode(enabled?: boolean): void; registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; @@ -217,6 +219,7 @@ export interface IChatAgentService { detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined>; hasChatParticipantDetectionProviders(): boolean; invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + setRequestPaused(agent: string, requestId: string, isPaused: boolean): void; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getAgent(id: string, includeDisabled?: boolean): IChatAgentData | undefined; @@ -252,6 +255,9 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + private readonly _onDidChangeToolsAgentModeEnabled = new Emitter(); + readonly onDidChangeToolsAgentModeEnabled: Event = this._onDidChangeToolsAgentModeEnabled.event; + private readonly _agentsContextKeys = new Set(); private readonly _hasDefaultAgent: IContextKey; private readonly _defaultAgentRegistered: IContextKey; @@ -421,8 +427,9 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return !!this._hasToolsAgentContextKey.get() && !!this._agentModeContextKey.get(); } - toggleToolsAgentMode(): void { - this._agentModeContextKey.set(!this._agentModeContextKey.get()); + toggleToolsAgentMode(enabled?: boolean): void { + this._agentModeContextKey.set(enabled ?? !this._agentModeContextKey.get()); + this._onDidChangeToolsAgentModeEnabled.fire(); this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); } @@ -496,6 +503,15 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return await data.impl.invoke(request, progress, history, token); } + setRequestPaused(id: string, requestId: string, isPaused: boolean) { + const data = this._agents.get(id); + if (!data?.impl) { + throw new Error(`No activated agent with id "${id}"`); + } + + data.impl.setRequestPaused?.(requestId, isPaused); + } + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); if (!data?.impl) { @@ -603,6 +619,12 @@ export class MergedChatAgent implements IChatAgent { return this.impl.invoke(request, progress, history, token); } + setRequestPaused(requestId: string, isPaused: boolean): void { + if (this.impl.setRequestPaused) { + this.impl.setRequestPaused(requestId, isPaused); + } + } + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { if (this.impl.provideFollowups) { return this.impl.provideFollowups(request, result, history, token); diff --git a/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 097ca6d8915..386758af35e 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -4,18 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { CharCode } from '../../../../base/common/charCode.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { splitLinesIncludeSeparators } from '../../../../base/common/strings.js'; -import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { DocumentContextItem, isLocation, TextEdit } from '../../../../editor/common/languages.js'; +import { TextEdit } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatAgentResult } from './chatAgents.js'; -import { IChatResponseModel } from './chatModel.js'; -import { IChatContentReference } from './chatService.js'; - export interface ICodeMapperResponse { textEdit: (resource: URI, textEdit: TextEdit[]) => void; @@ -27,21 +19,10 @@ export interface ICodeMapperCodeBlock { readonly markdownBeforeBlock?: string; } -export interface ConversationRequest { - readonly type: 'request'; - readonly message: string; -} - -export interface ConversationResponse { - readonly type: 'response'; - readonly message: string; - readonly result?: IChatAgentResult; - readonly references?: DocumentContextItem[]; -} - export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; - readonly conversation: (ConversationResponse | ConversationRequest)[]; + readonly chatRequestId?: string; + readonly location?: string; } export interface ICodeMapperResult { @@ -49,6 +30,7 @@ export interface ICodeMapperResult { } export interface ICodeMapperProvider { + readonly displayName: string; mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } @@ -56,15 +38,15 @@ export const ICodeMapperService = createDecorator('codeMappe export interface ICodeMapperService { readonly _serviceBrand: undefined; + readonly providers: ICodeMapperProvider[]; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; - mapCodeFromResponse(responseModel: IChatResponseModel, response: ICodeMapperResponse, token: CancellationToken): Promise; } export class CodeMapperService implements ICodeMapperService { _serviceBrand: undefined; - private readonly providers: ICodeMapperProvider[] = []; + public readonly providers: ICodeMapperProvider[] = []; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable { this.providers.push(provider); @@ -81,128 +63,11 @@ export class CodeMapperService implements ICodeMapperService { async mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) { for (const provider of this.providers) { const result = await provider.mapCode(request, response, token); - if (result) { - return result; + if (token.isCancellationRequested) { + return undefined; } + return result; } return undefined; } - - async mapCodeFromResponse(responseModel: IChatResponseModel, response: ICodeMapperResponse, token: CancellationToken) { - const fenceLanguageRegex = /^`{3,}/; - const codeBlocks: ICodeMapperCodeBlock[] = []; - - const currentBlock = []; - const markdownBeforeBlock = []; - let currentBlockUri = undefined; - - let fence = undefined; // if set, we are in a block - - for (const lineOrUri of iterateLinesOrUris(responseModel)) { - if (isString(lineOrUri)) { - const fenceLanguageIdMatch = lineOrUri.match(fenceLanguageRegex); - if (fenceLanguageIdMatch) { - // we found a line that starts with a fence - if (fence !== undefined && fenceLanguageIdMatch[0] === fence) { - // we are in a code block and the fence matches the opening fence: Close the code block - fence = undefined; - if (currentBlockUri) { - // report the code block if we have a URI - codeBlocks.push({ code: currentBlock.join(''), resource: currentBlockUri, markdownBeforeBlock: markdownBeforeBlock.join('') }); - currentBlock.length = 0; - markdownBeforeBlock.length = 0; - currentBlockUri = undefined; - } - } else { - // we are not in a code block. Open the block - fence = fenceLanguageIdMatch[0]; - } - } else { - if (fence !== undefined) { - currentBlock.push(lineOrUri); - } else { - markdownBeforeBlock.push(lineOrUri); - } - } - } else { - currentBlockUri = lineOrUri; - } - } - const conversation: (ConversationRequest | ConversationResponse)[] = []; - for (const request of responseModel.session.getRequests()) { - const response = request.response; - if (!response || response === responseModel) { - break; - } - conversation.push({ - type: 'request', - message: request.message.text - }); - conversation.push({ - type: 'response', - message: response.response.getMarkdown(), - result: response.result, - references: getReferencesAsDocumentContext(response.contentReferences) - }); - } - return this.mapCode({ codeBlocks, conversation }, response, token); - } -} - -function iterateLinesOrUris(responseModel: IChatResponseModel): Iterable { - return { - *[Symbol.iterator](): Iterator { - let lastIncompleteLine = undefined; - for (const part of responseModel.response.value) { - if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { - const lines = splitLinesIncludeSeparators(part.content.value); - if (lines.length > 0) { - if (lastIncompleteLine !== undefined) { - lines[0] = lastIncompleteLine + lines[0]; // merge the last incomplete line with the first markdown line - } - lastIncompleteLine = isLineIncomplete(lines[lines.length - 1]) ? lines.pop() : undefined; - for (const line of lines) { - yield line; - } - } - } else if (part.kind === 'codeblockUri') { - yield part.uri; - } - } - if (lastIncompleteLine !== undefined) { - yield lastIncompleteLine; - } - } - }; -} - -function isLineIncomplete(line: string) { - const lastChar = line.charCodeAt(line.length - 1); - return lastChar !== CharCode.LineFeed && lastChar !== CharCode.CarriageReturn; -} - - -export function getReferencesAsDocumentContext(res: readonly IChatContentReference[]): DocumentContextItem[] { - const map = new ResourceMap(); - for (const r of res) { - let uri; - let range; - if (URI.isUri(r.reference)) { - uri = r.reference; - } else if (isLocation(r.reference)) { - uri = r.reference.uri; - range = r.reference.range; - } - if (uri) { - const item = map.get(uri); - if (item) { - if (range) { - item.ranges.push(range); - } - } else { - map.set(uri, { uri, version: -1, ranges: range ? [range] : [] }); - } - } - } - return [...map.values()]; } diff --git a/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 68c50c90600..7723d67602f 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -16,6 +16,8 @@ export namespace ChatContextKeys { export const responseIsFiltered = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); export const responseHasError = new RawContextKey('chatSessionResponseError', false, { type: 'boolean', description: localize('chatResponseErrored', "True when the chat response resulted in an error.") }); export const requestInProgress = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); + export const isRequestPaused = new RawContextKey('chatRequestIsPaused', false, { type: 'boolean', description: localize('chatRequestIsPaused', "True when the current request is paused.") }); + export const canRequestBePaused = new RawContextKey('chatCanRequestBePaused', false, { type: 'boolean', description: localize('chatCanRequestBePaused', "True when the current request can be paused.") }); export const isResponse = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); export const isRequest = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); @@ -28,6 +30,7 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); + export const instructionsAttached = new RawContextKey('chatInstructionsAttached', false, { type: 'boolean', description: localize('chatInstructionsAttachedContextDescription', "True when the chat has a prompt instructions attached.") }); export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); @@ -49,7 +52,7 @@ export namespace ChatContextKeys { // State signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. - triggered: new RawContextKey('chatSetupTriggered', false, true), // True when chat setup is triggered. + hidden: new RawContextKey('chatSetupHidden', false, true), // True when chat setup is explicitly hidden. installed: new RawContextKey('chatSetupInstalled', false, true), // True when the chat extension is installed. // Plans @@ -58,10 +61,10 @@ export namespace ChatContextKeys { pro: new RawContextKey('chatPlanPro', false, true) // True when user is a chat pro user. }; - export const SetupViewKeys = new Set([ChatContextKeys.Setup.triggered.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); + export const SetupViewKeys = new Set([ChatContextKeys.Setup.hidden.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); export const SetupViewCondition = ContextKeyExpr.or( ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.hidden.negate(), ChatContextKeys.Setup.installed.negate() ), ContextKeyExpr.and( diff --git a/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 57e0924ce48..23d97adc411 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -24,11 +24,6 @@ export interface IChatEditingService { _serviceBrand: undefined; - /** - * emitted when a session is created, changed or disposed - */ - readonly onDidChangeEditingSession: Event; - readonly currentEditingSessionObs: IObservable; readonly currentEditingSession: IChatEditingSession | null; @@ -38,9 +33,21 @@ export interface IChatEditingService { startOrContinueEditingSession(chatSessionId: string): Promise; getOrRestoreEditingSession(): Promise; + + hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + + /** + * All editing sessions, sorted by recency, e.g the last created session comes first. + */ + readonly editingSessionsObs: IObservable; + + /** + * Creates a new short lived editing session + */ + createAdhocEditingSession(chatSessionId: string): Promise; } export interface IChatRequestDraft { @@ -62,19 +69,25 @@ export interface IChatRelatedFilesProvider { provideRelatedFiles(chatRequest: IChatRequestDraft, token: CancellationToken): Promise; } -export interface WorkingSetDisplayMetadata { state: WorkingSetEntryState; description?: string } +export interface WorkingSetDisplayMetadata { + state: WorkingSetEntryState; + description?: string; + isMarkedReadonly?: boolean; +} export interface IChatEditingSession { + readonly isGlobalEditingSession: boolean; readonly chatSessionId: string; readonly onDidChange: Event; readonly onDidDispose: Event; readonly state: IObservable; readonly entries: IObservable; readonly workingSet: ResourceMap; - readonly isVisible: boolean; + readonly isToolsAgentSession: boolean; addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; show(): Promise; remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; + markIsReadonly(uri: URI, isReadonly?: boolean): void; accept(...uris: URI[]): Promise; reject(...uris: URI[]): Promise; getEntry(uri: URI): IModifiedFileEntry | undefined; @@ -126,6 +139,10 @@ export interface IModifiedFileEntry { readonly lastModifyingRequestId: string; accept(transaction: ITransaction | undefined): Promise; reject(transaction: ITransaction | undefined): Promise; + + reviewMode: IObservable; + autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>; + enableReviewModeUntilSettled(): void; } export interface IChatEditingSessionStream { @@ -142,6 +159,8 @@ export const enum ChatEditingSessionState { export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source'; export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); +export const chatEditingWidgetFileReadonlyContextKey = new RawContextKey('chatEditingWidgetFileReadonly', undefined, localize('chatEditingWidgetFileReadonly', "Whether the file has been marked as read-only in the chat editing widget")); +export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)")); export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); export const inChatEditingSessionContextKey = new RawContextKey('inChatEditingSession', undefined); diff --git a/code/src/vs/workbench/contrib/chat/common/chatModel.ts b/code/src/vs/workbench/contrib/chat/common/chatModel.ts index 2b23c26cffd..40689635719 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -30,6 +30,7 @@ export interface IBaseChatRequestVariableEntry { fullName?: string; icon?: ThemeIcon; name: string; + isMarkedReadonly?: boolean; modelDescription?: string; range?: IOffsetRange; value: IChatRequestVariableValue; @@ -131,6 +132,7 @@ export interface IChatRequestModel { readonly attachedContext?: IChatRequestVariableEntry[]; readonly workingSet?: URI[]; readonly isCompleteAddedRequest: boolean; + readonly paused: boolean; readonly response?: IChatResponseModel; shouldBeRemovedOnSend: boolean; } @@ -207,6 +209,7 @@ export interface IChatResponseModel { readonly response: IResponse; readonly isComplete: boolean; readonly isCanceled: boolean; + readonly isPendingConfirmation: boolean; readonly shouldBeRemovedOnSend: boolean; readonly isCompleteAddedRequest: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ @@ -268,6 +271,15 @@ export class ChatRequestModel implements IChatRequestModel { return this._workingSet; } + private _paused = false; + public get paused(): boolean { + return this._paused; + } + + public set paused(paused: boolean) { + this._paused = paused; + } + constructor( private _session: ChatModel, public readonly message: IParsedChatRequest, @@ -352,7 +364,9 @@ export class Response extends Disposable implements IResponse { // The last part can't be merged with- not markdown, or markdown with different permissions this._responseParts.push(progress); } else { - lastResponsePart.content = appendMarkdownString(lastResponsePart.content, progress.content); + // Don't modify the current object, since it's being diffed by the renderer + const idx = this._responseParts.indexOf(lastResponsePart); + this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) }; } this._updateRepr(quiet); } else if (progress.kind === 'textEdit') { @@ -396,6 +410,14 @@ export class Response extends Disposable implements IResponse { this._updateRepr(false); }); + } else if (progress.kind === 'toolInvocation') { + if (progress.confirmationMessages) { + progress.confirmed.p.then(() => { + this._updateRepr(false); + }); + } + this._responseParts.push(progress); + this._updateRepr(quiet); } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -592,6 +614,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._isStale; } + public get isPendingConfirmation() { + return this._response.value.some(part => + part.kind === 'toolInvocation' && part.isConfirmed === undefined + || part.kind === 'confirmation' && part.isUsed === false); + } + constructor( _response: IMarkdownString | ReadonlyArray, private _session: ChatModel, @@ -704,6 +732,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } } +export const enum ChatPauseState { + NotPausable, + Paused, + Unpaused, +} + export interface IChatModel { readonly onDidDispose: Event; readonly onDidChange: Event; @@ -714,7 +748,9 @@ export interface IChatModel { readonly welcomeMessage: IChatWelcomeMessageContent | undefined; readonly sampleQuestions: IChatFollowup[] | undefined; readonly requestInProgress: boolean; + readonly requestPausibility: ChatPauseState; readonly inputPlaceholder?: string; + toggleLastRequestPaused(paused?: boolean): void; disableRequests(requestIds: ReadonlyArray): void; getRequests(): IChatRequestModel[]; toExport(): IExportableChatData; @@ -976,7 +1012,24 @@ export class ChatModel extends Disposable implements IChatModel { get requestInProgress(): boolean { const lastRequest = this.lastRequest; - return !!lastRequest?.response && !lastRequest.response.isComplete; + if (!lastRequest?.response) { + return false; + } + + if (lastRequest.response.isPendingConfirmation) { + return false; + } + + return !lastRequest.response.isComplete; + } + + get requestPausibility(): ChatPauseState { + const lastRequest = this.lastRequest; + if (!lastRequest?.response?.agent || lastRequest.response.isComplete || lastRequest.response.isPendingConfirmation) { + return ChatPauseState.NotPausable; + } + + return lastRequest.paused ? ChatPauseState.Paused : ChatPauseState.Unpaused; } get hasRequests(): boolean { @@ -1145,6 +1198,15 @@ export class ChatModel extends Disposable implements IChatModel { }; } + toggleLastRequestPaused(isPaused?: boolean) { + if (this.requestPausibility !== ChatPauseState.NotPausable && this.lastRequest?.response?.agent) { + const pausedValue = isPaused ?? !this.lastRequest.paused; + this.lastRequest.paused = pausedValue; + this.chatAgentService.setRequestPaused(this.lastRequest.response.agent.id, this.lastRequest.id, pausedValue); + this._onDidChange.fire({ kind: 'changedRequest', request: this.lastRequest }); + } + } + startInitialize(): void { if (this.initState !== ChatModelInitState.Created) { throw new Error(`ChatModel is in the wrong state for startInitialize: ${ChatModelInitState[this.initState]}`); @@ -1226,6 +1288,11 @@ export class ChatModel extends Disposable implements IChatModel { this._customTitle = title; } + setRequestPaused(request: ChatRequestModel, isPaused: boolean) { + request.paused = isPaused; + this._onDidChange.fire({ kind: 'changedRequest', request }); + } + updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) { request.variableData = variableData; this._onDidChange.fire({ kind: 'changedRequest', request }); diff --git a/code/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/code/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 76e572c91df..2b216ac49bc 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -38,6 +38,8 @@ export class ChatToolInvocation implements IChatToolInvocation { constructor( public readonly invocationMessage: string | IMarkdownString, + public readonly pastTenseMessage: string | IMarkdownString | undefined, + public readonly tooltip: string | IMarkdownString | undefined, private _confirmationMessages: IToolConfirmationMessages | undefined) { if (!_confirmationMessages) { // No confirmation needed @@ -63,6 +65,8 @@ export class ChatToolInvocation implements IChatToolInvocation { return { kind: 'toolInvocationSerialized', invocationMessage: this.invocationMessage, + pastTenseMessage: this.pastTenseMessage, + tooltip: this.tooltip, isConfirmed: this._isConfirmed ?? false, isComplete: this._isComplete, }; diff --git a/code/src/vs/workbench/contrib/chat/common/chatService.ts b/code/src/vs/workbench/contrib/chat/common/chatService.ts index f56ac22858a..4251fe54356 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatService.ts @@ -203,6 +203,8 @@ export interface IChatToolInvocation { /** A 3-way: undefined=don't know yet. */ isConfirmed: boolean | undefined; invocationMessage: string | IMarkdownString; + pastTenseMessage: string | IMarkdownString | undefined; + tooltip: string | IMarkdownString | undefined; isComplete: boolean; isCompleteDeferred: DeferredPromise; @@ -214,6 +216,8 @@ export interface IChatToolInvocation { */ export interface IChatToolInvocationSerialized { invocationMessage: string | IMarkdownString; + pastTenseMessage: string | IMarkdownString | undefined; + tooltip: string | IMarkdownString | undefined; isConfirmed: boolean; isComplete: boolean; kind: 'toolInvocationSerialized'; @@ -431,6 +435,11 @@ export interface IChatSendRequestOptions { * The label of the confirmation action that was selected. */ confirmation?: string; + + /** + * Flag to indicate whether a prompt instructions attachment is present. + */ + hasInstructionAttachments?: boolean; } export const IChatService = createDecorator('IChatService'); @@ -441,7 +450,7 @@ export interface IChatService { isEnabled(location: ChatAgentLocation): boolean; hasSessions(): boolean; - startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined; + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel; getSession(sessionId: string): IChatModel | undefined; getOrRestoreSession(sessionId: string): IChatModel | undefined; loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined; diff --git a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index aad0cd8dab1..6e2741852cf 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -475,13 +475,21 @@ export class ChatService extends Disposable implements IChatService { locationData: request.locationData, attachedContext: request.attachedContext, workingSet: request.workingSet, + hasInstructionAttachments: options?.hasInstructionAttachments ?? false, }; await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - if (!request.trim() && !options?.slashCommand && !options?.agentId) { + + // if text is not provided, but chat input has `prompt instructions` + // attached, use the default prompt text to avoid empty messages + if (!request.trim() && options?.hasInstructionAttachments) { + request = 'Follow these instructions.'; + } + + if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.hasInstructionAttachments) { this.trace('sendRequest', 'Rejected empty message'); return; } diff --git a/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 5345af9ac65..6f58726d167 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { annotateVulnerabilitiesInText } from './annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from './chatAgents.js'; -import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; +import { ChatModelInitState, ChatPauseState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from './chatService.js'; import { countWords } from './chatWordCounter.js'; @@ -53,6 +53,7 @@ export interface IChatViewModel { readonly onDidDisposeModel: Event; readonly onDidChange: Event; readonly requestInProgress: boolean; + readonly requestPausibility: ChatPauseState; readonly inputPlaceholder?: string; getItems(): (IChatRequestViewModel | IChatResponseViewModel)[]; setInputPlaceholder(text: string): void; @@ -228,6 +229,10 @@ export class ChatViewModel extends Disposable implements IChatViewModel { return this._model.requestInProgress; } + get requestPausibility(): ChatPauseState { + return this._model.requestPausibility; + } + get initState() { return this._model.initState; } @@ -577,18 +582,23 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi const now = Date.now(); const wordCount = countWords(_model.response.getMarkdown()); - const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 1000); - const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); - const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (newTotalTime / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); - this._contentUpdateTimings = { - totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? - newTotalTime : - this._contentUpdateTimings.totalTime, - lastUpdateTime: now, - impliedWordLoadRate, - lastWordCount: wordCount - }; + if (wordCount > 0 && wordCount === this._contentUpdateTimings.lastWordCount) { + this.trace('onDidChange', `Update- no new words`); + } else { + const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 1000); + const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); + const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (newTotalTime / 1000); + this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); + this._contentUpdateTimings = { + totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? + newTotalTime : + this._contentUpdateTimings.totalTime, + lastUpdateTime: now, + impliedWordLoadRate, + lastWordCount: wordCount + }; + } + } else { this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); } diff --git a/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index 2ea99b4bcfa..d2ab31d58ce 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -40,6 +40,8 @@ interface IChatHistory { history: { [providerId: string]: IChatHistoryEntry[] }; } +export const ChatInputHistoryMaxEntries = 40; + export class ChatWidgetHistoryService implements IChatWidgetHistoryService { _serviceBrand: undefined; @@ -78,7 +80,7 @@ export class ChatWidgetHistoryService implements IChatWidgetHistoryService { } const key = this.getKey(location); - this.viewState.history[key] = history; + this.viewState.history[key] = history.slice(-ChatInputHistoryMaxEntries); this.memento.saveMemento(); } diff --git a/code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts b/code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts deleted file mode 100644 index 57b7f0955b8..00000000000 --- a/code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts +++ /dev/null @@ -1,45 +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 { FileReference } from './tokens/fileReference.js'; -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { ReadableStream } from '../../../../../../base/common/stream.js'; -import { BaseDecoder } from '../../../../../../base/common/codecs/baseDecoder.js'; -import { Word } from '../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; -import { SimpleDecoder, TSimpleToken } from '../../../../../../editor/common/codecs/simpleCodec/simpleDecoder.js'; - -/** - * Tokens handled by the `ChatPromptDecoder` decoder. - */ -export type TChatPromptToken = FileReference; - -/** - * Decoder for the common chatbot prompt message syntax. - * For instance, the file references `#file:./path/file.md` are handled by this decoder. - */ -export class ChatPromptDecoder extends BaseDecoder { - constructor( - stream: ReadableStream, - ) { - super(new SimpleDecoder(stream)); - } - - protected override onStreamData(simpleToken: TSimpleToken): void { - // handle the word tokens only - if (!(simpleToken instanceof Word)) { - return; - } - - // handle file references only for now - const { text } = simpleToken; - if (!text.startsWith(FileReference.TOKEN_START)) { - return; - } - - this._onData.fire( - FileReference.fromWord(simpleToken), - ); - } -} diff --git a/code/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/code/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index f69be4fed5c..2e48c18bf08 100644 --- a/code/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/code/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -34,6 +34,7 @@ export interface IToolInvocation { parameters: Object; tokenBudget?: number; context: IToolInvocationContext | undefined; + chatRequestId?: string; } export interface IToolInvocationContext { @@ -65,6 +66,8 @@ export interface IToolConfirmationMessages { export interface IPreparedToolInvocation { invocationMessage?: string | IMarkdownString; + pastTenseMessage?: string | IMarkdownString; + tooltip?: string | IMarkdownString; confirmationMessages?: IToolConfirmationMessages; } diff --git a/code/src/vs/workbench/contrib/chat/common/languageModels.ts b/code/src/vs/workbench/contrib/chat/common/languageModels.ts index 66fee6ef6c2..8424946dac5 100644 --- a/code/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/code/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -86,6 +86,10 @@ export interface ILanguageModelChatMetadata { readonly providerLabel: string; readonly accountLabel?: string; }; + readonly capabilities?: { + readonly vision?: boolean; + readonly toolCalling?: boolean; + }; } export interface ILanguageModelChatResponse { @@ -111,11 +115,13 @@ export interface ILanguageModelChatSelector { export const ILanguageModelsService = createDecorator('ILanguageModelsService'); +export interface ILanguageModelChatMetadataAndIdentifier { + metadata: ILanguageModelChatMetadata; + identifier: string; +} + export interface ILanguageModelsChangeEvent { - added?: { - identifier: string; - metadata: ILanguageModelChatMetadata; - }[]; + added?: ILanguageModelChatMetadataAndIdentifier[]; removed?: string[]; } diff --git a/code/src/vs/workbench/contrib/chat/common/promptFileReference.ts b/code/src/vs/workbench/contrib/chat/common/promptFileReference.ts deleted file mode 100644 index 878c800d785..00000000000 --- a/code/src/vs/workbench/contrib/chat/common/promptFileReference.ts +++ /dev/null @@ -1,414 +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 { URI } from '../../../../base/common/uri.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { extUri } from '../../../../base/common/resources.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { ChatPromptCodec } from './codecs/chatPromptCodec/chatPromptCodec.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference } from './promptFileReferenceErrors.js'; -import { FileChangesEvent, FileChangeType, IFileService, IFileStreamContent } from '../../../../platform/files/common/files.js'; - -/** - * Error conditions that may happen during the file reference resolution. - */ -export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; - -/** - * File extension for the prompt snippet files. - */ -const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; - -/** - * Configuration key for the prompt snippets feature. - */ -const PROMPT_SNIPPETS_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; - -/** - * Represents a file reference in the chatbot prompt, e.g. `#file:./path/to/file.md`. - * Contains logic to resolve all nested file references in the target file and all - * referenced child files recursively, if any. - * - * ## Examples - * - * ```typescript - * const fileReference = new PromptFileReference( - * URI.file('/path/to/file.md'), - * fileService, - * ); - * - * // subscribe to updates to the file reference tree - * fileReference.onUpdate(() => { - * // .. do something with the file reference tree .. - * // e.g. get URIs of all resolved file references in the tree - * const resolved = fileReference - * // get all file references as a flat array - * .flatten() - * // remove self from the list if only child references are needed - * .slice(1) - * // filter out unresolved references - * .filter(reference => reference.resolveFailed === flase) - * // convert to URIs only - * .map(reference => reference.uri); - * - * console.log(resolved); - * }); - * - * // *optional* if need to re-resolve file references when target files change - * // note that this does not sets up filesystem listeners for nested file references - * fileReference.addFilesystemListeners(); - * - * // start resolving the file reference tree; this can also be `await`ed if needed - * // to wait for the resolution on the main file reference to complete (the nested - * // references can still be resolving in the background) - * fileReference.resolve(); - * - * // don't forget to dispose when no longer needed! - * fileReference.dispose(); - * ``` - */ -export class PromptFileReference extends Disposable { - /** - * Child references of the current one. - */ - protected readonly children: PromptFileReference[] = []; - - /** - * The event is fired when nested prompt snippet references are updated, if any. - */ - private readonly _onUpdate = this._register(new Emitter()); - - private _errorCondition?: TErrorCondition; - /** - * If file reference resolution fails, this attribute will be set - * to an error instance that describes the error condition. - */ - public get errorCondition(): TErrorCondition | undefined { - return this._errorCondition; - } - - /** - * Whether file reference resolution was attempted at least once. - */ - private _resolveAttempted: boolean = false; - /** - * Whether file references resolution failed. - * Set to `undefined` if the `resolve` method hasn't been ever called yet. - */ - public get resolveFailed(): boolean | undefined { - if (!this._resolveAttempted) { - return undefined; - } - - return !!this._errorCondition; - } - - constructor( - private readonly _uri: URI | Location, - @IFileService private readonly fileService: IFileService, - @IConfigurationService private readonly configService: IConfigurationService, - ) { - super(); - this.onFilesChanged = this.onFilesChanged.bind(this); - - // make sure the variable is updated on file changes - // but only for the prompt snippet files - if (this.isPromptSnippetFile) { - this.addFilesystemListeners(); - } - } - - /** - * Subscribe to the `onUpdate` event. - * @param callback - */ - public onUpdate(callback: () => unknown) { - this._register(this._onUpdate.event(callback)); - } - - /** - * Check if the prompt snippets feature is enabled. - * @see {@link PROMPT_SNIPPETS_CONFIG_KEY} - */ - public static promptSnippetsEnabled( - configService: IConfigurationService, - ): boolean { - const value = configService.getValue(PROMPT_SNIPPETS_CONFIG_KEY); - - if (!value) { - return false; - } - - if (typeof value === 'string') { - return value.trim().toLowerCase() === 'true'; - } - - return !!value; - } - - /** - * Check if the current reference points to a prompt snippet file. - */ - public get isPromptSnippetFile(): boolean { - return this.uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION); - } - - /** - * Associated URI of the reference. - */ - public get uri(): URI { - return this._uri instanceof URI - ? this._uri - : this._uri.uri; - } - - /** - * Get the directory name of the file reference. - */ - public get dirname() { - return URI.joinPath(this.uri, '..'); - } - - /** - * Check if the current reference points to a given resource. - */ - public sameUri(other: URI | Location): boolean { - const otherUri = other instanceof URI ? other : other.uri; - - return this.uri.toString() === otherUri.toString(); - } - - /** - * Add file system event listeners for the current file reference. - */ - private addFilesystemListeners(): this { - this._register( - this.fileService.onDidFilesChange(this.onFilesChanged), - ); - - return this; - } - - /** - * Event handler for the `onDidFilesChange` event. - */ - private onFilesChanged(event: FileChangesEvent) { - const fileChanged = event.contains(this.uri, FileChangeType.UPDATED); - const fileDeleted = event.contains(this.uri, FileChangeType.DELETED); - if (!fileChanged && !fileDeleted) { - return; - } - - // if file is changed or deleted, re-resolve the file reference - // in the case when the file is deleted, this should result in - // failure to open the file, so the `errorCondition` field will - // be updated to an appropriate error instance and the `children` - // field will be cleared up - this.resolve(); - } - - /** - * Get file stream, if the file exsists. - */ - private async getFileStream(): Promise { - try { - // read the file first - const result = await this.fileService.readFileStream(this.uri); - - // if file exists but not a prompt snippet file, set appropriate error - // condition and return null so we don't resolve nested references in it - if (this.uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION) === false) { - this._errorCondition = new NonPromptSnippetFile(this.uri); - - return null; - } - - return result; - } catch (error) { - this._errorCondition = new FileOpenFailed(this.uri, error); - - return null; - } - } - - /** - * Resolve the current file reference on the disk and - * all nested file references that may exist in the file. - * - * @param waitForChildren Whether need to block until all child references are resolved. - */ - public async resolve( - waitForChildren: boolean = false, - ): Promise { - return await this.resolveReference(waitForChildren); - } - - /** - * Private implementation of the {@link resolve} method, that allows - * to pass `seenReferences` list to the recursive calls to prevent - * infinite file reference recursion. - */ - private async resolveReference( - waitForChildren: boolean = false, - seenReferences: string[] = [], - ): Promise { - // remove current error condition from the previous resolve attempt, if any - delete this._errorCondition; - - // dispose current child references, if any exist from a previous resolve - this.disposeChildren(); - - // to prevent infinite file recursion, we keep track of all references in - // the current branch of the file reference tree and check if the current - // file reference has been already seen before - if (seenReferences.includes(this.uri.path)) { - seenReferences.push(this.uri.path); - - this._errorCondition = new RecursiveReference(this.uri, seenReferences); - this._resolveAttempted = true; - this._onUpdate.fire(); - - return this; - } - - // we don't care if reading the file fails below, hence can add the path - // of the current reference to the `seenReferences` set immediately, - - // even if the file doesn't exist, we would never end up in the recursion - seenReferences.push(this.uri.path); - - // try to get stream for the contents of the file, it may - // fail to multiple reasons, e.g. file doesn't exist, etc. - const fileStream = await this.getFileStream(); - this._resolveAttempted = true; - - // failed to open the file, nothing to resolve - if (fileStream === null) { - this._onUpdate.fire(); - - return this; - } - - // get all file references in the file contents - const references = await ChatPromptCodec.decode(fileStream.value).consumeAll(); - - // recursively resolve all references and add to the `children` array - // - // Note! we don't register the children references as disposables here, because we dispose them - // explicitly in the `dispose` override method of this class. This is done to prevent - // the disposables store to be littered with already-disposed child instances due to - // the fact that the `resolve` method can be called multiple times on target file changes - const childPromises = []; - for (const reference of references) { - const childUri = extUri.resolvePath(this.dirname, reference.path); - - const child = new PromptFileReference( - childUri, - this.fileService, - this.configService, - ); - - // subscribe to child updates - child.onUpdate( - this._onUpdate.fire.bind(this._onUpdate), - ); - this.children.push(child); - - // start resolving the child in the background, including its children - // Note! we have to clone the `seenReferences` list here to ensure that - // different tree branches don't interfere with each other as we - // care about the parent references when checking for recursion - childPromises.push( - child.resolveReference(waitForChildren, [...seenReferences]), - ); - } - - // if should wait for all children to resolve, block here - if (waitForChildren) { - await Promise.all(childPromises); - } - - this._onUpdate.fire(); - - return this; - } - - /** - * Dispose current child file references. - */ - private disposeChildren(): this { - for (const child of this.children) { - child.dispose(); - } - - this.children.length = 0; - this._onUpdate.fire(); - - return this; - } - - /** - * Flatten the current file reference tree into a single array. - */ - public flatten(): readonly PromptFileReference[] { - const result = []; - - // then add self to the result - result.push(this); - - // get flattened children references first - for (const child of this.children) { - result.push(...child.flatten()); - } - - return result; - } - - /** - * Get list of all valid child references. - */ - public get validChildReferences(): readonly PromptFileReference[] { - return this.flatten() - // skip the root reference itself (this variable) - .slice(1) - // filter out unresolved references - .filter((reference) => { - return (reference.resolveFailed === false) || - (reference.errorCondition instanceof NonPromptSnippetFile); - }); - } - - /** - * Get list of all valid child references as URIs. - */ - public get validFileReferenceUris(): readonly URI[] { - return this.validChildReferences - .map(child => child.uri); - } - - /** - * Check if the current reference is equal to a given one. - */ - public equals(other: PromptFileReference): boolean { - if (!this.sameUri(other.uri)) { - return false; - } - - return true; - } - - /** - * Returns a string representation of this reference. - */ - public override toString() { - return `#file:${this.uri.path}`; - } - - public override dispose() { - this.disposeChildren(); - super.dispose(); - } -} diff --git a/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index 60da24e52a7..1538f9e2316 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -6,11 +6,15 @@ import { URI } from '../../../../base/common/uri.js'; /** - * Base resolve error class used when file reference resolution fails. + * Base prompt parsing error class. */ -abstract class ResolveError extends Error { +export abstract class ParseError extends Error { + /** + * Error type name. + */ + public readonly abstract errorType: string; + constructor( - public readonly uri: URI, message?: string, options?: ErrorOptions, ) { @@ -36,16 +40,50 @@ abstract class ResolveError extends Error { } } +/** + * A generic error for failing to resolve prompt contents stream. + */ +export class FailedToResolveContentsStream extends ParseError { + public override errorType = 'FailedToResolveContentsStream'; + + constructor( + public readonly uri: URI, + public readonly originalError: unknown, + message: string = `Failed to resolve prompt contents stream for '${uri.toString()}': ${originalError}.`, + ) { + super(message); + } +} + + +/** + * Base resolve error class used when file reference resolution fails. + */ +export abstract class ResolveError extends ParseError { + public abstract override errorType: string; + + constructor( + public readonly uri: URI, + message?: string, + options?: ErrorOptions, + ) { + super(message, options); + } +} + /** * Error that reflects the case when attempt to open target file fails. */ -export class FileOpenFailed extends ResolveError { +export class FileOpenFailed extends FailedToResolveContentsStream { + public override errorType = 'FileOpenError'; + constructor( uri: URI, - public readonly originalError: unknown, + originalError: unknown, ) { super( uri, + originalError, `Failed to open file '${uri.toString()}': ${originalError}.`, ); } @@ -66,6 +104,8 @@ export class FileOpenFailed extends ResolveError { * ``` */ export class RecursiveReference extends ResolveError { + public override errorType = 'RecursiveReferenceError'; + constructor( uri: URI, public readonly recursivePath: string[], @@ -114,6 +154,8 @@ export class RecursiveReference extends ResolveError { * a prompt snippet file, hence was not attempted to be resolved. */ export class NonPromptSnippetFile extends ResolveError { + public override errorType = 'NonPromptSnippetFileError'; + constructor( uri: URI, message: string = '', diff --git a/code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptCodec.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts similarity index 100% rename from code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptCodec.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts new file mode 100644 index 00000000000..06d8e1620ec --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts @@ -0,0 +1,262 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { FileReference } from './tokens/fileReference.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { ReadableStream } from '../../../../../../base/common/stream.js'; +import { BaseDecoder } from '../../../../../../base/common/codecs/baseDecoder.js'; +import { Tab } from '../../../../../../editor/common/codecs/simpleCodec/tokens/tab.js'; +import { Word } from '../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; +import { Hash } from '../../../../../../editor/common/codecs/simpleCodec/tokens/hash.js'; +import { Space } from '../../../../../../editor/common/codecs/simpleCodec/tokens/space.js'; +import { Colon } from '../../../../../../editor/common/codecs/simpleCodec/tokens/colon.js'; +import { NewLine } from '../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; +import { FormFeed } from '../../../../../../editor/common/codecs/simpleCodec/tokens/formFeed.js'; +import { VerticalTab } from '../../../../../../editor/common/codecs/simpleCodec/tokens/verticalTab.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { CarriageReturn } from '../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; +import { ParserBase, TAcceptTokenResult } from '../../../../../../editor/common/codecs/simpleCodec/parserBase.js'; +import { MarkdownDecoder, TMarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/markdownDecoder.js'; + +/** + * Tokens produced by this decoder. + */ +export type TChatPromptToken = MarkdownLink | FileReference; + +/** + * The Parser responsible for processing a `prompt variable name` syntax from + * a sequence of tokens (e.g., `#variable:`). + * + * The parsing process starts with single `#` token, then can accept `file` word, + * followed by the `:` token, resulting in the tokens sequence equivalent to + * the `#file:` text sequence. In this successful case, the parser transitions into + * the {@linkcode PartialPromptFileReference} parser to continue the parsing process. + */ +class PartialPromptVariableName extends ParserBase { + constructor(token: Hash) { + super([token]); + } + + public accept(token: TMarkdownToken): TAcceptTokenResult { + // given we currently hold the `#` token, if we receive a `file` word, + // we can successfully proceed to the next token in the sequence + if (token instanceof Word) { + if (token.text === 'file') { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // if we receive the `:` token, we can successfully proceed to the next + // token in the sequence `only if` the previous token was a `file` word + // therefore for currently tokens sequence equivalent to the `#file` text + if (token instanceof Colon) { + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + if (lastToken instanceof Word) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: new PartialPromptFileReference(this.currentTokens), + wasTokenConsumed: true, + }; + } + } + + // all other cases are failures and we don't consume the offending token + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * List of characters that stop a prompt variable sequence. + */ +const PROMPT_FILE_REFERENCE_STOP_CHARACTERS: readonly string[] = [Space, Tab, CarriageReturn, NewLine, VerticalTab, FormFeed] + .map((token) => { return token.symbol; }); + +/** + * Parser responsible for processing the `file reference` syntax part from + * a sequence of tokens (e.g., #variable:`./some/file/path.md`). + * + * The parsing process starts with the sequence of `#`, `file`, and `:` tokens, + * then can accept a sequence of tokens until one of the tokens defined in + * the {@linkcode PROMPT_FILE_REFERENCE_STOP_CHARACTERS} list is encountered. + * This sequence of tokens is treated as a `file path` part of the `#file:` variable, + * and in the successful case, the parser transitions into the {@linkcode FileReference} + * token which signifies the end of the file reference text parsing process. + */ +class PartialPromptFileReference extends ParserBase { + /** + * Set of tokens that were accumulated so far. + */ + private readonly fileReferenceTokens: (Hash | Word | Colon)[]; + + constructor(tokens: (Hash | Word | Colon)[]) { + super([]); + + this.fileReferenceTokens = tokens; + } + + /** + * List of tokens that were accumulated so far. + */ + public override get tokens(): readonly (Hash | Word | Colon)[] { + return [...this.fileReferenceTokens, ...this.currentTokens]; + } + + /** + * Return the `FileReference` instance created from the current object. + */ + public asFileReference(): FileReference { + // use only tokens in the `currentTokens` list to + // create the path component of the file reference + const path = this.currentTokens + .map((token) => { return token.text; }) + .join(''); + + const firstToken = this.tokens[0]; + + const range = new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + firstToken.range.startLineNumber, + firstToken.range.startColumn + FileReference.TOKEN_START.length + path.length, + ); + + return new FileReference(range, path); + } + + public accept(token: TMarkdownToken): TAcceptTokenResult { + // any of stop characters is are breaking a prompt variable sequence + if (PROMPT_FILE_REFERENCE_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'success', + wasTokenConsumed: false, + nextParser: this.asFileReference(), + }; + } + + // any other token can be included in the sequence so accumulate + // it and continue with using the current parser instance + this.currentTokens.push(token); + return { + result: 'success', + wasTokenConsumed: true, + nextParser: this, + }; + } +} + +/** + * Decoder for the common chatbot prompt message syntax. + * For instance, the file references `#file:./path/file.md` are handled by this decoder. + */ +export class ChatPromptDecoder extends BaseDecoder { + /** + * Currently active parser object that is used to parse a well-known equence of + * tokens, for instance, a `file reference` that consists of `hash`, `word`, and + * `colon` tokens sequence plus following file path part. + */ + private current?: PartialPromptVariableName; + + constructor( + stream: ReadableStream, + ) { + super(new MarkdownDecoder(stream)); + } + + protected override onStreamData(token: TMarkdownToken): void { + // prompt variables always start with the `#` character, hence + // initiate a parser object if we encounter respective token and + /// there is no active parser object present at the moment + if (token instanceof Hash && !this.current) { + this.current = new PartialPromptVariableName(token); + + return; + } + + // if current parser was not yet initiated, - we are in the general + // "text" mode, therefore re-emit the token immediately and return + if (!this.current) { + // at the moment, the decoder outputs only specific markdown tokens, like + // the `markdown link` one, so re-emit only these tokens ignoring the rest + // + // note! to make the decoder consistent with others we would need to: + // - re-emit all tokens here + // - collect all "text" sequences of tokens and emit them as a single + // "text" sequence token + if (token instanceof MarkdownLink) { + this._onData.fire(token); + } + + return; + } + + // if there is a current parser object, submit the token to it + // so it can progress with parsing the tokens sequence + const parseResult = this.current.accept(token); + + // process the parse result next + switch (parseResult.result) { + // in the case of success there might be 2 cases: + // 1) parsing fully completed and an parsed entity is returned back, in this case, + // emit the parsed token (e.g., a `link`) and reset current parser object + // 2) parsing is still in progress and the next parser object is returned, hence + // we need to update the current praser object with a new one and continue + case 'success': { + if (parseResult.nextParser instanceof FileReference) { + this._onData.fire(parseResult.nextParser); + delete this.current; + } else { + this.current = parseResult.nextParser; + } + + break; + } + // in the case of failure, reset the current parser object + case 'failure': { + delete this.current; + + // note! when this decoder becomes consistent with other ones and hence starts emitting + // all token types, not just links, we would need to re-emit all the tokens that + // the parser object has accumulated so far + break; + } + } + + // if token was not consumed by the parser, call `onStreamData` again + // so the token is properly handled by the decoder in the case when a + // new sequence starts with this token + if (!parseResult.wasTokenConsumed) { + this.onStreamData(token); + } + } + + protected override onStreamEnd(): void { + // if the stream has ended and there is a current `PartialPromptFileReference` + // parser object, then the file reference was terminated by the end of the stream + if (this.current && this.current instanceof PartialPromptFileReference) { + this._onData.fire(this.current.asFileReference()); + delete this.current; + } + + super.onStreamEnd(); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts similarity index 80% rename from code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts index 5a68344a757..5efc84d9a6c 100644 --- a/code/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts @@ -4,18 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { assert } from '../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; +import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; import { Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; -// Start sequence for a file reference token in a prompt. +/** + * Start sequence for a file reference token in a prompt. + */ const TOKEN_START: string = '#file:'; /** * Object represents a file reference token inside a chatbot prompt. */ export class FileReference extends BaseToken { - // Start sequence for a file reference token in a prompt. + /** + * Start sequence for a file reference token in a prompt. + */ public static readonly TOKEN_START = TOKEN_START; constructor( @@ -89,6 +93,24 @@ export class FileReference extends BaseToken { return this.text === other.text; } + /** + * Get the range of the `link part` of the token (e.g., + * the `/path/to/file.md` part of `#file:/path/to/file.md`). + */ + public get linkRange(): IRange | undefined { + if (this.path.length === 0) { + return undefined; + } + + const { range } = this; + return new Range( + range.startLineNumber, + range.startColumn + TOKEN_START.length, + range.endLineNumber, + range.endColumn, + ); + } + /** * Return a string representation of the token. */ diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts new file mode 100644 index 00000000000..e8222202db5 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; + +/** + * Configuration helper for the `prompt files` feature. + * @see {@link CONFIG_KEY} and {@link DEFAULT_LOCATION} + * + * ### Functions + * + * - {@link getValue} allows to current read configuration value + * - {@link enabled} allows to check if the feature is enabled + * - {@link sourceLocations} gets the source folder locations for prompt files + * + * ### Configuration Examples + * + * Enable the feature, using defaults for prompt files source folder locations + * (see {@link DEFAULT_LOCATION}): + * ```json + * { + * "chat.experimental.promptSnippets": true, + * } + * ``` + * + * Enable the feature, specifying a single prompt files source folder location: + * ```json + * { + * "chat.experimental.promptSnippets": '.github/prompts', + * } + * ``` + * + * Enable the feature, specifying multiple prompt files source folder location: + * ```json + * { + * "chat.experimental.promptSnippets": [ + * '.github/prompts', + * '.copilot/prompts', + * '/Users/legomushroom/repos/prompts', + * ], + * } + * ``` + * + * See the next section for details on how we treat the config value. + * + * ### Possible Values + * + * - `undefined`/`null`: feature is disabled + * - `boolean`: + * - `true`: feature is enabled, prompt files source folder locations + * fallback to {@link DEFAULT_LOCATION} + * - `false`: feature is disabled + * - `string`: + * - values that can be mapped to `boolean`(`"true"`, `"FALSE", "TrUe"`, etc.) + * are treated as `boolean` above + * - any other `non-empty` string value is treated as a single prompt files source folder path + * - `array`: + * - `string` items in the array are treated as prompt files source folder paths + * - all `non-string` items in the array are `ignored` + * - if the resulting array is empty, the feature is considered `enabled`, prompt files source + * folder locations fallback to defaults (see {@linkcode DEFAULT_LOCATION}) + * + * ### File Paths Resolution + * + * We resolve only `*.prompt.md` files inside the resulting source folder locations and + * all `relative` folder paths are resolved relative to: + * + * - the current workspace `root`, if applicable, in other words one of the workspace folders + * can be used as a prompt files source folder + * - root of each top-level folder in the workspace (if there are multiple workspace folders) + * - current root folder (if a single folder is open) + */ +export namespace PromptFilesConfig { + /** + * Configuration key for the `prompt files` feature (also + * known as `prompt files`, `prompt instructions`, etc.). + */ + export const CONFIG_KEY: string = 'chat.promptFiles'; + + /** + * Documentation link for the prompt snippets feature. + */ + export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; + + /** + * Default prompt instructions source folder paths. + */ + const DEFAULT_LOCATION = ['.github/prompts']; + + /** + * Get value of the `prompt files` configuration setting. + */ + export const getValue = ( + configService: IConfigurationService, + ): string | readonly string[] | boolean | undefined => { + const value = configService.getValue(CONFIG_KEY); + + if (value === undefined || value === null) { + return undefined; + } + + if (typeof value === 'string') { + const cleanValue = value.trim().toLowerCase(); + if (cleanValue === 'true') { + return true; + } + + if (cleanValue === 'false') { + return false; + } + + if (!cleanValue) { + return undefined; + } + + return value; + } + + if (typeof value === 'boolean') { + return value; + } + + if (Array.isArray(value)) { + return value.filter((item) => { + return typeof item === 'string'; + }); + } + + return undefined; + }; + + /** + * Checks if feature is enabled. + */ + export const enabled = ( + configService: IConfigurationService, + ): boolean => { + const value = getValue(configService); + + return value !== undefined && value !== false; + }; + + /** + * Gets the source folder locations for prompt files. + * Defaults to {@link DEFAULT_LOCATION}. + */ + export const sourceLocations = ( + configService: IConfigurationService, + ): readonly string[] => { + const value = getValue(configService); + + if (value === undefined) { + return DEFAULT_LOCATION; + } + + if (typeof value === 'string') { + return [value]; + } + + if (Array.isArray(value)) { + if (value.length !== 0) { + return value; + } + + return DEFAULT_LOCATION; + } + + return DEFAULT_LOCATION; + }; +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts new file mode 100644 index 00000000000..9bda1b1acf3 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptContentsProvider } from './types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; +import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { FileOpenFailed, NonPromptSnippetFile } from '../../promptFileReferenceErrors.js'; +import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; + +/** + * Prompt contents provider for a file on the disk referenced by the provided {@linkcode URI}. + */ +export class FilePromptContentProvider extends PromptContentsProviderBase implements IPromptContentsProvider { + constructor( + public readonly uri: URI, + @IFileService private readonly fileService: IFileService, + ) { + super(); + + // make sure the object is updated on file changes + this._register( + this.fileService.onDidFilesChange((event) => { + // if file was added or updated, forward the event to + // the `getContentsStream()` produce a new stream for file contents + if (event.contains(this.uri, FileChangeType.ADDED, FileChangeType.UPDATED)) { + // we support only full file parsing right now because + // the event doesn't contain a list of changed lines + return this.onChangeEmitter.fire('full'); + } + + // if file was deleted, forward the event to + // the `getContentsStream()` produce an error + if (event.contains(this.uri, FileChangeType.DELETED)) { + return this.onChangeEmitter.fire(event); + } + }), + ); + } + + /** + * Creates a stream of lines from the file based on the changes listed in + * the provided event. + * + * @param event - event that describes the changes in the file; `'full'` is + * the special value that means that all contents have changed + * @param cancellationToken - token that cancels this operation + */ + protected async getContentsStream( + _event: FileChangesEvent | 'full', + cancellationToken?: CancellationToken, + ): Promise { + assert( + !cancellationToken?.isCancellationRequested, + new CancellationError(), + ); + + // get the binary stream of the file contents + let fileStream; + try { + fileStream = await this.fileService.readFileStream(this.uri); + } catch (error) { + throw new FileOpenFailed(this.uri, error); + } + + assertDefined( + fileStream, + new FileOpenFailed(this.uri, 'Failed to open file stream.'), + ); + + // after the promise above complete, this object can be already disposed or + // the cancellation could be requested, in that case destroy the stream and + // throw cancellation error + if (this.disposed || cancellationToken?.isCancellationRequested) { + fileStream.value.destroy(); + throw new CancellationError(); + } + + // if URI doesn't point to a prompt snippet file, don't try to resolve it + if (!this.isPromptSnippet()) { + throw new NonPromptSnippetFile(this.uri); + } + + return fileStream.value; + } + + /** + * String representation of this object. + */ + public override toString() { + return `file-prompt-contents-provider:${this.uri.path}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts new file mode 100644 index 00000000000..005a78f1e42 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptContentsProvider } from './types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; +import { FailedToResolveContentsStream, ParseError } from '../../promptFileReferenceErrors.js'; +import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js'; + +/** + * File extension for the prompt snippets. + */ +export const PROMPT_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; + +/** + * Base class for prompt contents providers. Classes that extend this one are responsible to: + * + * - implement the {@linkcode getContentsStream} method to provide the contents stream + * of a prompt; this method should throw a `ParseError` or its derivative if the contents + * cannot be parsed for any reason + * - fire a {@linkcode TChangeEvent} event on the {@linkcode onChangeEmitter} event when + * prompt contents change + * - misc: + * - provide the {@linkcode uri} property that represents the URI of a prompt that + * the contents are for + * - implement the {@linkcode toString} method to return a string representation of this + * provider type to aid with debugging/tracing + */ +export abstract class PromptContentsProviderBase< + TChangeEvent extends NonNullable, +> extends ObservableDisposable implements IPromptContentsProvider { + /** + * Internal event emitter for the prompt contents change event. Classes that extend + * this abstract class are responsible to use this emitter to fire the contents change + * event when the prompt contents get modified. + */ + protected readonly onChangeEmitter = this._register(new Emitter()); + + constructor() { + super(); + // ensure that the `onChangeEmitter` always fires with the correct context + this.onChangeEmitter.fire = this.onChangeEmitter.fire.bind(this.onChangeEmitter); + // subscribe to the change event emitted by an extending class + this._register(this.onChangeEmitter.event(this.onContentsChanged, this)); + } + + /** + * Function to get contents stream for the provider. This function should + * throw a `ParseError` or its derivative if the contents cannot be parsed. + * + * @param changesEvent The event that triggered the change. The special + * `'full'` value means that everything has changed hence entire prompt + * contents need to be re-parsed from scratch. + */ + protected abstract getContentsStream( + changesEvent: TChangeEvent | 'full', + cancellationToken?: CancellationToken, + ): Promise; + + /** + * URI reference associated with the prompt contents. + */ + public abstract readonly uri: URI; + + /** + * Return a string representation of this object + * for debugging/tracing purposes. + */ + public abstract override toString(): string; + + /** + * Event emitter for the prompt contents change event. + * See {@linkcode onContentChanged} for more details. + */ + private readonly onContentChangedEmitter = this._register(new Emitter()); + + /** + * Event that fires when the prompt contents change. The event is either + * a `VSBufferReadableStream` stream with changed contents or an instance of + * the `ParseError` class representing a parsing failure case. + * + * `Note!` this field is meant to be used by the external consumers of the prompt + * contents provider that the classes that extend this abstract class. + * Please use the {@linkcode onChangeEmitter} event to provide a change + * event in your prompt contents implementation instead. + */ + public readonly onContentChanged = this.onContentChangedEmitter.event; + + /** + * Internal common implementation of the event that should be fired when + * prompt contents change. + */ + @cancelPreviousCalls + private onContentsChanged( + event: TChangeEvent | 'full', + cancellationToken?: CancellationToken, + ): this { + const promise = (cancellationToken?.isCancellationRequested) + ? Promise.reject(new CancellationError()) + : this.getContentsStream(event, cancellationToken); + + promise + .then((stream) => { + if (cancellationToken?.isCancellationRequested || this.disposed) { + stream.destroy(); + throw new CancellationError(); + } + + this.onContentChangedEmitter.fire(stream); + }) + .catch((error) => { + if (error instanceof ParseError) { + this.onContentChangedEmitter.fire(error); + + return; + } + + this.onContentChangedEmitter.fire( + new FailedToResolveContentsStream(this.uri, error), + ); + }); + + return this; + } + + /** + * Start producing the prompt contents data. + */ + public start(): this { + assert( + !this.disposed, + 'Cannot start contents provider that was already disposed.', + ); + + // `'full'` means "everything has changed" + this.onContentsChanged('full'); + + return this; + } + + /** + * Check if the current URI points to a prompt snippet. + */ + public isPromptSnippet(): boolean { + return this.uri.path.endsWith(PROMPT_SNIPPET_FILE_EXTENSION); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts new file mode 100644 index 00000000000..b980da871fb --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { newWriteableStream, ReadableStream } from '../../../../../../base/common/stream.js'; +import { IModelContentChangedEvent } from '../../../../../../editor/common/textModelEvents.js'; + +/** + * Prompt contents provider for a {@linkcode ITextModel} instance. + */ +export class TextModelContentsProvider extends PromptContentsProviderBase { + /** + * URI component of the prompt associated with this contents provider. + */ + public readonly uri = this.model.uri; + + constructor( + private readonly model: ITextModel, + ) { + super(); + + this._register(this.model.onWillDispose(this.dispose.bind(this))); + this._register(this.model.onDidChangeContent(this.onChangeEmitter.fire)); + } + + /** + * Creates a stream of binary data from the text model based on the changes + * listed in the provided event. + * + * Note! this method implements a basic logic which does not take into account + * the `_event` argument for incremental updates. This needs to be improved. + * + * @param _event - event that describes the changes in the text model; `'full'` is + * the special value that means that all contents have changed + * @param cancellationToken - token that cancels this operation + */ + protected override async getContentsStream( + _event: IModelContentChangedEvent | 'full', + cancellationToken?: CancellationToken, + ): Promise> { + const stream = newWriteableStream(null); + const linesCount = this.model.getLineCount(); + + // provide the changed lines to the stream incrementaly and asynchronously + // to avoid blocking the main thread and save system resources used + let i = 1; + const interval = setInterval(() => { + // if we have written all lines or lines count is zero, + // end the stream and stop the interval timer + if (i >= linesCount) { + clearInterval(interval); + stream.end(); + stream.destroy(); + } + + // if model was disposed or cancellation was requested, + // end the stream with an error and stop the interval timer + if (this.model.isDisposed() || cancellationToken?.isCancellationRequested) { + clearInterval(interval); + stream.error(new CancellationError()); + stream.destroy(); + return; + } + + try { + // write the current line to the stream + stream.write( + VSBuffer.fromString(this.model.getLineContent(i)), + ); + + // for all lines exept the last one, write the EOL character + // to separate the lines in the stream + if (i !== linesCount) { + stream.write( + VSBuffer.fromString(this.model.getEOL()), + ); + } + } catch (error) { + console.log(this.uri, i, error); + } + + // use the next line in the next iteration + i++; + }, 1); + + return stream; + } + + /** + * String representation of this object. + */ + public override toString() { + return `text-model-prompt-contents-provider:${this.uri.path}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts new file mode 100644 index 00000000000..5d03ef2848c --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../base/common/uri.js'; +import { ParseError } from '../../promptFileReferenceErrors.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; + +/** + * Interface for a prompt contents provider. Prompt contents providers are + * responsible for providing contents of a prompt as a byte streams and + * allow to subscribe to the change events of the prompt contents. + */ +export interface IPromptContentsProvider extends IDisposable { + /** + * URI component of the prompt associated with this contents provider. + */ + readonly uri: URI; + + /** + * Start the contents provider to produce the underlying contents. + */ + start(): this; + + /** + * Event that fires when the prompt contents change. The event is either a + * {@linkcode VSBufferReadableStream} stream with changed contents or + * an instance of the {@linkcode ParseError} error. + */ + onContentChanged( + callback: (streamOrError: VSBufferReadableStream | ParseError) => void, + ): IDisposable; +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts new file mode 100644 index 00000000000..854272e736e --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { NonPromptSnippetFile } from '../../promptFileReferenceErrors.js'; +import { ObjectCache } from '../../../../../../base/common/objectCache.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Registry } from '../../../../../../platform/registry/common/platform.js'; +import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { ILink, ILinksList, LinkProvider } from '../../../../../../editor/common/languages.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js'; + +/** + * Prompt files language selector. + */ +const languageSelector = { + pattern: `**/*${PROMPT_SNIPPET_FILE_EXTENSION}`, +}; + +/** + * Provides link references for prompt files. + */ +export class PromptLinkProvider extends Disposable implements LinkProvider { + /** + * Cache of text model content prompt parsers. + */ + private readonly parserProvider: ObjectCache; + + constructor( + @IInstantiationService private readonly initService: IInstantiationService, + @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, + ) { + super(); + + this.languageService.linkProvider.register(languageSelector, this); + this.parserProvider = this._register(new ObjectCache(this.createParser.bind(this))); + } + + /** + * Create new prompt parser instance for the provided text model. + * + * @param model - text model to create the parser for + * @param initService - the instantiation service + */ + private createParser( + model: ITextModel, + ): TextModelPromptParser & { disposed: false } { + const parser: TextModelPromptParser = this.initService.createInstance( + TextModelPromptParser, + model, + [], + ); + + parser.assertNotDisposed( + 'Created prompt parser must not be disposed.', + ); + + return parser; + } + + /** + * Provide list of links for the provided text model. + */ + public async provideLinks( + model: ITextModel, + token: CancellationToken, + ): Promise { + assert( + !token.isCancellationRequested, + new CancellationError(), + ); + + const parser = this.parserProvider.get(model); + assert( + !parser.disposed, + 'Prompt parser must not be disposed.', + ); + + // start the parser in case it was not started yet, + // and wait for it to settle to a final result + const { references } = await parser + .start() + .settled(); + + // validate that the cancellation was not yet requested + assert( + !token.isCancellationRequested, + new CancellationError(), + ); + + // filter out references that are not valid links + const links: ILink[] = references + .filter((reference) => { + const { errorCondition, linkRange } = reference; + if (!errorCondition && linkRange) { + return true; + } + + return errorCondition instanceof NonPromptSnippetFile; + }) + .map((reference) => { + const { linkRange } = reference; + + // must always be true because of the filter above + assertDefined( + linkRange, + 'Link range must be defined.', + ); + + + return { + range: linkRange, + url: reference.uri, + }; + }); + + return { + links, + }; + } +} + +// register the text model prompt decorators provider as a workbench contribution +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts new file mode 100644 index 00000000000..85e7f645b75 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../editor/common/core/range.js'; +import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; + +/** + * Decoration object. + */ +export interface ITextModelDecoration { + /** + * Range of the decoration. + */ + range: IRange; + + /** + * Associated decoration options. + */ + options: ModelDecorationOptions; +} + +/** + * Decoration CSS class names. + */ +export enum DecorationClassNames { + /** + * CSS class name for `default` prompt syntax decoration. + */ + default = 'prompt-decoration', + + /** + * CSS class name for `file reference` prompt syntax decoration. + */ + fileReference = DecorationClassNames.default, +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts new file mode 100644 index 00000000000..3c6cf31d099 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -0,0 +1,642 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../nls.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { IPromptFileReference, IResolveError } from './types.js'; +import { FileReference } from '../codecs/tokens/fileReference.js'; +import { ChatPromptDecoder } from '../codecs/chatPromptDecoder.js'; +import { IRange } from '../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { IPromptContentsProvider } from '../contentProviders/types.js'; +import { DeferredPromise } from '../../../../../../base/common/async.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { basename, extUri } from '../../../../../../base/common/resources.js'; +import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; +import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; +import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; + +/** + * Well-known localized error messages. + */ +const errorMessages = { + recursion: localize('chatPromptInstructionsRecursiveReference', 'Recursive reference found'), + fileOpenFailed: localize('chatPromptInstructionsFileOpenFailed', 'Failed to open file'), + streamOpenFailed: localize('chatPromptInstructionsStreamOpenFailed', 'Failed to open contents stream'), + brokenChild: localize('chatPromptInstructionsBrokenReference', 'Contains a broken reference that will be ignored'), +}; + +/** + * Error conditions that may happen during the file reference resolution. + */ +export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; + +/** + * Base prompt parser class that provides a common interface for all + * prompt parsers that are responsible for parsing chat prompt syntax. + */ +export abstract class BasePromptParser extends ObservableDisposable { + /** + * List of file references in the current branch of the file reference tree. + */ + private readonly _references: PromptFileReference[] = []; + + /** + * The event is fired when lines or their content change. + */ + private readonly _onUpdate = this._register(new Emitter()); + + /** + * Subscribe to the `onUpdate` event that is fired when prompt tokens are updated. + * @param callback The callback function to be called on updates. + */ + public onUpdate(callback: () => void): this { + this._register(this._onUpdate.event(callback)); + + return this; + } + + private _errorCondition?: ParseError; + + /** + * If file reference resolution fails, this attribute will be set + * to an error instance that describes the error condition. + */ + public get errorCondition(): ParseError | undefined { + return this._errorCondition; + } + + /** + * Whether file references resolution failed. + * Set to `undefined` if the `resolve` method hasn't been ever called yet. + */ + public get resolveFailed(): boolean | undefined { + if (!this.firstParseResult.gotFirstResult) { + return undefined; + } + + return !!this._errorCondition; + } + + /** + * The promise is resolved when at least one parse result (a stream or + * an error) has been received from the prompt contents provider. + */ + private firstParseResult = new FirstParseResult(); + + /** + * Returned promise is resolved when the parser process is settled. + * The settled state means that the prompt parser stream exists and + * has ended, or an error condition has been set in case of failure. + * + * Furthermore, this function can be called multiple times and will + * block until the latest prompt contents parsing logic is settled + * (e.g., for every `onContentChanged` event of the prompt source). + */ + public async settled(): Promise { + assert( + this.started, + 'Cannot wait on the parser that did not start yet.', + ); + + await this.firstParseResult.promise; + + if (this.errorCondition) { + return this; + } + + assertDefined( + this.stream, + 'No stream reference found.', + ); + + await this.stream.settled; + + return this; + } + + /** + * Same as {@linkcode settled} but also waits for all possible + * nested child prompt references and their children to be settled. + */ + public async allSettled(): Promise { + await this.settled(); + + await Promise.allSettled( + this.references.map((reference) => { + return reference.allSettled(); + }), + ); + + return this; + } + + constructor( + private readonly promptContentsProvider: T, + seenReferences: string[] = [], + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IConfigurationService protected readonly configService: IConfigurationService, + @ILogService protected readonly logService: ILogService, + ) { + super(); + + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + this._register(promptContentsProvider); + + // to prevent infinite file recursion, we keep track of all references in + // the current branch of the file reference tree and check if the current + // file reference has been already seen before + if (seenReferences.includes(this.uri.path)) { + seenReferences.push(this.uri.path); + + this._errorCondition = new RecursiveReference(this.uri, seenReferences); + this._onUpdate.fire(); + this.firstParseResult.complete(); + + return this; + } + + // we don't care if reading the file fails below, hence can add the path + // of the current reference to the `seenReferences` set immediately, - + // even if the file doesn't exist, we would never end up in the recursion + seenReferences.push(this.uri.path); + + this._register( + this.promptContentsProvider.onContentChanged((streamOrError) => { + // process the the received message + this.onContentsChanged(streamOrError, seenReferences); + + // indicate that we've received at least one `onContentChanged` event + this.firstParseResult.complete(); + }), + ); + } + + /** + * The latest received stream of prompt tokens, if any. + */ + private stream: ChatPromptDecoder | undefined; + + /** + * Handler the event event that is triggered when prompt contents change. + * + * @param streamOrError Either a binary stream of file contents, or an error object + * that was generated during the reference resolve attempt. + * @param seenReferences List of parent references that we've have already seen + * during the process of traversing the references tree. It's + * used to prevent the tree navigation to fall into an infinite + * references recursion. + */ + private onContentsChanged( + streamOrError: VSBufferReadableStream | ParseError, + seenReferences: string[], + ): void { + // dispose and cleanup the previously received stream + // object or an error condition, if any received yet + this.stream?.dispose(); + delete this.stream; + delete this._errorCondition; + + // dispose all currently existing references + this.disposeReferences(); + + // if an error received, set up the error condition and stop + if (streamOrError instanceof ParseError) { + this._errorCondition = streamOrError; + this._onUpdate.fire(); + + return; + } + + // decode the byte stream to a stream of prompt tokens + this.stream = ChatPromptCodec.decode(streamOrError); + + // on error or stream end, dispose the stream and fire the update event + this.stream.on('error', this.onStreamEnd.bind(this, this.stream)); + this.stream.on('end', this.onStreamEnd.bind(this, this.stream)); + + // when some tokens received, process and store the references + this.stream.on('data', (token) => { + if (token instanceof FileReference) { + this.onReference(token, [...seenReferences]); + } + + // note! the `isURL` is a simple check and needs to be improved to truly + // handle only file references, ignoring broken URLs or references + if (token instanceof MarkdownLink && !token.isURL) { + this.onReference(token, [...seenReferences]); + } + }); + + // calling `start` on a disposed stream throws, so we warn and return instead + if (this.stream.disposed) { + this.logService.warn( + `[prompt parser][${basename(this.uri)}] cannot start stream that has been already disposed, aborting`, + ); + + return; + } + + // start receiving data on the stream + this.stream.start(); + } + + /** + * Handle a new reference token inside prompt contents. + */ + private onReference( + token: FileReference | MarkdownLink, + seenReferences: string[], + ): this { + const fileReference = this.instantiationService + .createInstance(PromptFileReference, token, this.dirname, seenReferences); + + this._references.push(fileReference); + + fileReference.onUpdate(this._onUpdate.fire); + fileReference.start(); + + this._onUpdate.fire(); + + return this; + } + + /** + * Handle the `stream` end event. + * + * @param stream The stream that has ended. + * @param error Optional error object if stream ended with an error. + */ + private onStreamEnd( + _stream: ChatPromptDecoder, + error?: Error, + ): this { + if (error) { + this.logService.warn( + `[prompt parser][${basename(this.uri)}] received an error on the chat prompt decoder stream: ${error}`, + ); + } + + this._onUpdate.fire(); + + return this; + } + + /** + * Dispose all currently held references. + */ + private disposeReferences() { + for (const reference of [...this._references]) { + reference.dispose(); + } + + this._references.length = 0; + } + + /** + * Private attribute to track if the {@linkcode start} + * method has been already called at least once. + */ + private started: boolean = false; + + /** + * Start the prompt parser. + */ + public start(): this { + // if already started, nothing to do + if (this.started) { + return this; + } + this.started = true; + + + // if already in the error state that could be set + // in the constructor, then nothing to do + if (this.errorCondition) { + return this; + } + + this.promptContentsProvider.start(); + return this; + } + + /** + * Associated URI of the prompt. + */ + public get uri(): URI { + return this.promptContentsProvider.uri; + } + + /** + * Get the parent folder of the file reference. + */ + public get dirname() { + return URI.joinPath(this.uri, '..'); + } + + /** + * Get a list of immediate child references of the prompt. + */ + public get references(): readonly IPromptFileReference[] { + return [...this._references]; + } + + /** + * Get a list of all references of the prompt, including + * all possible nested references its children may have. + */ + public get allReferences(): readonly IPromptFileReference[] { + const result: IPromptFileReference[] = []; + + for (const reference of this.references) { + result.push(reference); + + if (reference.type === 'file') { + result.push(...reference.allReferences); + } + } + + return result; + } + + /** + * Get list of all valid references. + */ + public get allValidReferences(): readonly IPromptFileReference[] { + return this.allReferences + // filter out unresolved references + .filter((reference) => { + const { errorCondition } = reference; + + return !errorCondition || (errorCondition instanceof NonPromptSnippetFile); + }); + } + + /** + * Get list of all valid child references as URIs. + */ + public get allValidReferencesUris(): readonly URI[] { + return this.allValidReferences + .map(child => child.uri); + } + + /** + * List of all errors that occurred while resolving the current + * reference including all possible errors of nested children. + */ + public get allErrors(): ParseError[] { + const result: ParseError[] = []; + + // collect error conditions of all child references + const childErrorConditions = this + // get entire reference tree + .allReferences + // filter out children without error conditions or + // the ones that are non-prompt snippet files + .filter((childReference) => { + const { errorCondition } = childReference; + + return errorCondition && !(errorCondition instanceof NonPromptSnippetFile); + }) + // map to error condition objects + .map((childReference): ParseError => { + const { errorCondition } = childReference; + + // `must` always be `true` because of the `filter` call above + assertDefined( + errorCondition, + `Error condition must be present for '${childReference.uri.path}'.`, + ); + + return errorCondition; + }); + + result.push(...childErrorConditions); + + return result; + } + + /** + * The top most error of the current reference or any of its + * possible child reference errors. + */ + public get topError(): IResolveError | undefined { + // get all errors, including error of this object + const errors = []; + if (this.errorCondition) { + errors.push(this.errorCondition); + } + errors.push(...this.allErrors); + + // if no errors, nothing to do + if (errors.length === 0) { + return undefined; + } + + + // if the first error is the error of the root reference, + // then return it as an `error` otherwise use `warning` + const [firstError, ...restErrors] = errors; + const isRootError = (firstError === this.errorCondition); + + // if a child error - the error is somewhere in the nested references tree, + // then use message prefix to highlight that this is not a root error + const prefix = (!isRootError) + ? `${errorMessages.brokenChild}: ` + : ''; + + const moreSuffix = restErrors.length > 0 + ? `\n-\n +${restErrors.length} more error${restErrors.length > 1 ? 's' : ''}` + : ''; + + const errorMessage = this.getErrorMessage(firstError); + return { + isRootError, + message: `${prefix}${errorMessage}${moreSuffix}`, + }; + } + + /** + * Get message for the provided error condition object. + * + * @param error Error object. + * @returns Error message. + */ + protected getErrorMessage(error: ParseError): string { + // if failed to resolve prompt contents stream, return + // the approprivate message and the prompt path + if (error instanceof FailedToResolveContentsStream) { + return `${errorMessages.streamOpenFailed} '${error.uri.path}'.`; + } + + // if a recursion, provide the entire recursion path so users + // can use it for the debugging purposes + if (error instanceof RecursiveReference) { + const { recursivePath } = error; + + const recursivePathString = recursivePath + .map((path) => { + return basename(URI.file(path)); + }) + .join(' -> '); + + return `${errorMessages.recursion}:\n${recursivePathString}`; + } + + return error.message; + } + + /** + * Check if the current reference points to a given resource. + */ + public sameUri(otherUri: URI): boolean { + return this.uri.toString() === otherUri.toString(); + } + + /** + * Check if the provided URI points to a prompt snippet. + */ + public static isPromptSnippet(uri: URI): boolean { + return uri.path.endsWith(PROMPT_SNIPPET_FILE_EXTENSION); + } + + /** + * Check if the current reference points to a prompt snippet file. + */ + public get isPromptSnippet(): boolean { + return BasePromptParser.isPromptSnippet(this.uri); + } + + /** + * Returns a string representation of this object. + */ + public override toString(): string { + return `prompt:${this.uri.path}`; + } + + /** + * @inheritdoc + */ + public override dispose() { + if (this.disposed) { + return; + } + + this.disposeReferences(); + this.stream?.dispose(); + this._onUpdate.fire(); + + super.dispose(); + } +} + +/** + * Prompt file reference object represents any file reference inside prompt + * text contents. For instanve the file variable(`#file:/path/to/file.md`) + * or a markdown link(`[#file:file.md](/path/to/file.md)`). + */ +export class PromptFileReference extends BasePromptParser implements IPromptFileReference { + public readonly type = 'file'; + + public readonly range = this.token.range; + public readonly path: string = this.token.path; + public readonly text: string = this.token.text; + + constructor( + public readonly token: FileReference | MarkdownLink, + dirname: URI, + seenReferences: string[] = [], + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, + ) { + const fileUri = extUri.resolvePath(dirname, token.path); + const provider = initService.createInstance(FilePromptContentProvider, fileUri); + + super(provider, seenReferences, initService, configService, logService); + } + + /** + * Get the range of the `link` part of the reference. + */ + public get linkRange(): IRange | undefined { + // `#file:` references + if (this.token instanceof FileReference) { + return this.token.linkRange; + } + + // `markdown link` references + if (this.token instanceof MarkdownLink) { + return this.token.linkRange; + } + + return undefined; + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + const prefix = (this.token instanceof FileReference) + ? FileReference.TOKEN_START + : 'md-link:'; + + return `${prefix}${this.uri.path}`; + } + + /** + * @inheritdoc + */ + protected override getErrorMessage(error: ParseError): string { + // if failed to open a file, return approprivate message and the file path + if (error instanceof FileOpenFailed) { + return `${errorMessages.fileOpenFailed} '${error.uri.path}'.`; + } + + return super.getErrorMessage(error); + } +} + +/** + * A tiny utility object that helps us to track existance + * of at least one parse result from the content provider. + */ +class FirstParseResult extends DeferredPromise { + /** + * Private attribute to track if we have + * received at least one result. + */ + private _gotResult = false; + + /** + * Whether we've received at least one result. + */ + public get gotFirstResult(): boolean { + return this._gotResult; + } + + /** + * Get underlying promise reference. + */ + public get promise(): Promise { + return this.p; + } + + /** + * Complete the underlying promise. + */ + public override complete() { + this._gotResult = true; + return super.complete(void 0); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts new file mode 100644 index 00000000000..5154016fcb8 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BasePromptParser } from './basePromptParser.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Class capable of parsing prompt syntax out of a provided file, + * including all the nested child file references it may have. + */ +export class FilePromptParser extends BasePromptParser { + constructor( + uri: URI, + seenReferences: string[] = [], + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, + ) { + const contentsProvider = initService.createInstance(FilePromptContentProvider, uri); + super(contentsProvider, seenReferences, initService, configService, logService); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `file-prompt:${this.uri.path}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts new file mode 100644 index 00000000000..75139c71685 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BasePromptParser } from './basePromptParser.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Class capable of parsing prompt syntax out of a provided text model, + * including all the nested child file references it may have. + */ +export class TextModelPromptParser extends BasePromptParser { + constructor( + model: ITextModel, + seenReferences: string[] = [], + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, + ) { + const contentsProvider = initService.createInstance(TextModelContentsProvider, model); + super(contentsProvider, seenReferences, initService, configService, logService); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `text-model-prompt:${this.uri.path}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts new file mode 100644 index 00000000000..91aef1479f1 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../../base/common/uri.js'; +import { ParseError } from '../../promptFileReferenceErrors.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IRange, Range } from '../../../../../../editor/common/core/range.js'; + +/** + * Interface for a resolve error. + */ +export interface IResolveError { + /** + * Localized error message. + */ + message: string; + + /** + * Whether this error is for the root reference + * object, or for one of its possible children. + */ + isRootError: boolean; +} + +/** + * List of all available prompt reference types. + */ +type PromptReferenceTypes = 'file'; + +/** + * Interface for a generic prompt reference. + */ +export interface IPromptReference extends IDisposable { + /** + * Type of the prompt reference. + */ + readonly type: PromptReferenceTypes; + + /** + * URI component of the associated with this reference. + */ + readonly uri: URI; + + /** + * The full range of the prompt reference in the source text, + * including the {@linkcode linkRange} and any additional + * parts the reference may contain (e.g., the `#file:` prefix). + */ + readonly range: Range; + + /** + * Range of the link part that the reference points to. + */ + readonly linkRange: IRange | undefined; + + /** + * Whether the current reference points to a prompt snippet file. + */ + readonly isPromptSnippet: boolean; + + /** + * Flag that indicates if resolving this reference failed. + * The `undefined` means that no attempt to resolve the reference + * was made so far or such an attempt is still in progress. + * + * See also {@linkcode errorCondition}. + */ + readonly resolveFailed: boolean | undefined; + + /** + * If failed to resolve the reference this property contains + * an error object that describes the failure reason. + * + * See also {@linkcode resolveFailed}. + */ + readonly errorCondition: ParseError | undefined; + + /** + * List of all errors that occurred while resolving the current + * reference including all possible errors of nested children. + */ + readonly allErrors: readonly ParseError[]; + + /** + * The top most error of the current reference or any of its + * possible child reference errors. + */ + readonly topError: IResolveError | undefined; + + /** + * All references that the current reference may have, + * including the all possible nested child references. + */ + allReferences: readonly IPromptReference[]; + + /** + * All *valid* references that the current reference may have, + * including the all possible nested child references. + * + * A valid reference is the one that points to an existing resource, + * without creating a circular reference loop or having any other + * issues that would make the reference resolve logic to fail. + */ + allValidReferences: readonly IPromptReference[]; + + /** + * Returns a promise that resolves when the reference contents + * are completely parsed and all existing tokens are returned. + */ + settled(): Promise; + + /** + * Returns a promise that resolves when the reference contents, + * and contents for all possible nested child references are + * completely parsed and entire tree of references is built. + * + * The same as {@linkcode settled} but for all prompts in + * the reference tree. + */ + allSettled(): Promise; +} + +/** + * The special case of the {@linkcode IPromptReference} that pertains + * to a file resource on the disk. + */ +export interface IPromptFileReference extends IPromptReference { + readonly type: 'file'; +} diff --git a/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index e78ef045c61..e75fb61664a 100644 --- a/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -3,22 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; -import { DisposableMap, Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ILanguageModelToolsService, IToolData } from '../languageModelToolsService.js'; +import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; +import { isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; +import { ILanguageModelToolsService, IToolData } from '../languageModelToolsService.js'; import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../../services/extensionManagement/common/extensionFeatures.js'; export interface IRawToolContribution { name: string; @@ -66,8 +67,8 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r name: { description: localize('toolName', "A unique name for this tool. This name must be a globally unique identifier, and is also used as a name when presenting this tool to a language model."), type: 'string', - // Borrow OpenAI's requirement for tool names - pattern: '^[\\w-]+$' + // [\\w-]+ is OpenAI's requirement for tool names + pattern: '^(?!copilot_|vscode_)[\\w-]+$' }, toolReferenceName: { markdownDescription: localize('toolName2', "If {0} is enabled for this tool, the user may use '#' with this name to invoke the tool in a query. Otherwise, the name is not required. Name must not contain whitespace.", '`canBeReferencedInPrompt`'), @@ -121,7 +122,8 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r description: localize('toolTags', "A set of tags that roughly describe the tool's capabilities. A tool user may use these to filter the set of tools to just ones that are relevant for the task at hand, or they may want to pick a tag that can be used to identify just the tools contributed by this extension."), type: 'array', items: { - type: 'string' + type: 'string', + pattern: '^(?!copilot_|vscode_)' } } } @@ -160,6 +162,16 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri continue; } + if ((rawTool.name.startsWith('copilot_') || rawTool.name.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with name starting with "vscode_" or "copilot_"`); + continue; + } + + if (rawTool.tags?.some(tag => tag.startsWith('copilot_') || tag.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`); + continue; + } + const rawIcon = rawTool.icon; let icon: IToolData['icon'] | undefined; if (typeof rawIcon === 'string') { diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 31fbb654b63..841b65ccc08 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -26,7 +26,7 @@ export class MockChatService implements IChatService { getProviderInfos(): IChatProviderInfo[] { throw new Error('Method not implemented.'); } - startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined { + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel { throw new Error('Method not implemented.'); } addSession(session: IChatModel): void { diff --git a/code/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptCodec.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts similarity index 75% rename from code/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptCodec.test.ts rename to code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts index 01fd02cd1c0..dceacea29d5 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptCodec.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; -import { newWriteableStream } from '../../../../../../base/common/stream.js'; -import { TestDecoder } from '../../../../../../editor/test/common/utils/testDecoder.js'; -import { FileReference } from '../../../common/codecs/chatPromptCodec/tokens/fileReference.js'; -import { ChatPromptCodec } from '../../../common/codecs/chatPromptCodec/chatPromptCodec.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ChatPromptDecoder, TChatPromptToken } from '../../../common/codecs/chatPromptCodec/chatPromptDecoder.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { newWriteableStream } from '../../../../../../../base/common/stream.js'; +import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; +import { ChatPromptCodec } from '../../../../common/promptSyntax/codecs/chatPromptCodec.js'; +import { FileReference } from '../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ChatPromptDecoder, TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; /** * A reusable test utility that asserts that a `ChatPromptDecoder` instance diff --git a/code/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts similarity index 59% rename from code/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts rename to code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts index 2ebadb798f1..1f73d7febae 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; -import { newWriteableStream } from '../../../../../../base/common/stream.js'; -import { TestDecoder } from '../../../../../../editor/test/common/utils/testDecoder.js'; -import { FileReference } from '../../../common/codecs/chatPromptCodec/tokens/fileReference.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ChatPromptDecoder, TChatPromptToken } from '../../../common/codecs/chatPromptCodec/chatPromptDecoder.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { newWriteableStream } from '../../../../../../../base/common/stream.js'; +import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; +import { FileReference } from '../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { MarkdownLink } from '../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { ChatPromptDecoder, TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; /** * A reusable test utility that asserts that a `ChatPromptDecoder` instance @@ -50,23 +51,39 @@ suite('ChatPromptDecoder', () => { new TestChatPromptDecoder(), ); + const contents = [ + '', + 'haalo!', + ' message 👾 message #file:./path/to/file1.md', + '', + '## Heading Title', + ' \t#file:a/b/c/filename2.md\t🖖\t#file:other-file.md', + ' [#file:reference.md](./reference.md)some text #file:/some/file/with/absolute/path.md', + ]; + await test.run( - '\nhaalo!\n message 👾 message #file:./path/to/file1.md \n\n \t#file:a/b/c/filename2.md\t🖖\t#file:other-file.md\nsome text #file:/some/file/with/absolute/path.md\t', + contents, [ new FileReference( new Range(3, 21, 3, 21 + 24), './path/to/file1.md', ), new FileReference( - new Range(5, 3, 5, 3 + 24), + new Range(6, 3, 6, 3 + 24), 'a/b/c/filename2.md', ), new FileReference( - new Range(5, 31, 5, 31 + 19), + new Range(6, 31, 6, 31 + 19), 'other-file.md', ), + new MarkdownLink( + 7, + 2, + '[#file:reference.md]', + '(./reference.md)', + ), new FileReference( - new Range(6, 11, 6, 11 + 38), + new Range(7, 48, 7, 48 + 38), '/some/file/with/absolute/path.md', ), ], diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts new file mode 100644 index 00000000000..4a48b3a850d --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { randomInt } from '../../../../../../../../base/common/numbers.js'; +import { Range } from '../../../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { FileReference } from '../../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; +import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; + +suite('FileReference', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('`linkRange`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const path = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; + const columnEndNumber = columnStartNumber + path.length; + + const range = new Range( + lineNumber, + columnStartNumber, + lineNumber, + columnEndNumber, + ); + const fileReference = new FileReference(range, path); + const { linkRange } = fileReference; + + assertDefined( + linkRange, + 'The link range must be defined.', + ); + + const expectedLinkRange = new Range( + lineNumber, + columnStartNumber + '#file:'.length, + lineNumber, + columnStartNumber + path.length, + ); + assert( + expectedLinkRange.equalsRange(linkRange), + `Expected link range to be ${expectedLinkRange}, got ${linkRange}.`, + ); + }); + + test('`path`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const link = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; + const columnEndNumber = columnStartNumber + link.length; + + const range = new Range( + lineNumber, + columnStartNumber, + lineNumber, + columnEndNumber, + ); + const fileReference = new FileReference(range, link); + + assert.strictEqual( + fileReference.path, + link, + 'Must return the correct link path.', + ); + }); + + test('extends `BaseToken`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const link = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; + const columnEndNumber = columnStartNumber + link.length; + + const range = new Range( + lineNumber, + columnStartNumber, + lineNumber, + columnEndNumber, + ); + const fileReference = new FileReference(range, link); + + assert( + fileReference instanceof BaseToken, + 'Must extend `BaseToken`.', + ); + }); +}); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.ts new file mode 100644 index 00000000000..bfb7e3672d7 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.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 assert from 'assert'; +import { randomInt } from '../../../../../../../../base/common/numbers.js'; +import { Range } from '../../../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; +import { MarkdownLink } from '../../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; +import { MarkdownToken } from '../../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownToken.js'; + +suite('FileReference', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('`linkRange`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const caption = `[link-caption-${randomInt(Number.MAX_SAFE_INTEGER)}]`; + const link = `(/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.md)`; + + const markdownLink = new MarkdownLink( + lineNumber, + columnStartNumber, + caption, + link, + ); + const { linkRange } = markdownLink; + + assertDefined( + linkRange, + 'The link range must be defined.', + ); + + const expectedLinkRange = new Range( + lineNumber, + // `+1` for the openning `(` character of the link + columnStartNumber + caption.length + 1, + lineNumber, + // `+1` for the openning `(` character of the link, and + // `-2` for the enclosing `()` part of the link + columnStartNumber + caption.length + 1 + link.length - 2, + ); + assert( + expectedLinkRange.equalsRange(linkRange), + `Expected link range to be ${expectedLinkRange}, got ${linkRange}.`, + ); + }); + + test('`path`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const caption = `[link-caption-${randomInt(Number.MAX_SAFE_INTEGER)}]`; + const rawLink = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.md`; + const link = `(${rawLink})`; + + const markdownLink = new MarkdownLink( + lineNumber, + columnStartNumber, + caption, + link, + ); + const { path } = markdownLink; + + assert.strictEqual( + path, + rawLink, + 'Must return the correct link value.', + ); + }); + + test('extends `MarkdownToken`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const caption = `[link-caption-${randomInt(Number.MAX_SAFE_INTEGER)}]`; + const rawLink = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.md`; + const link = `(${rawLink})`; + + const markdownLink = new MarkdownLink( + lineNumber, + columnStartNumber, + caption, + link, + ); + + assert( + markdownLink instanceof MarkdownToken, + 'Must extend `MarkdownToken`.', + ); + + assert( + markdownLink instanceof BaseToken, + 'Must extend `BaseToken`.', + ); + }); +}); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts new file mode 100644 index 00000000000..42b2933defc --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.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 assert from 'assert'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; +import { randomInt } from '../../../../../../../base/common/numbers.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; +import { wait } from '../../../../../../../base/test/common/testUtils.js'; +import { ReadableStream } from '../../../../../../../base/common/stream.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { FileService } from '../../../../../../../platform/files/common/fileService.js'; +import { NullPolicyService } from '../../../../../../../platform/policy/common/policy.js'; +import { Line } from '../../../../../../../editor/common/codecs/linesCodec/tokens/line.js'; +import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { LinesDecoder } from '../../../../../../../editor/common/codecs/linesCodec/linesDecoder.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { ConfigurationService } from '../../../../../../../platform/configuration/common/configurationService.js'; +import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { FilePromptContentProvider } from '../../../../common/promptSyntax/contentProviders/filePromptContentsProvider.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; + +suite('FilePromptContentsProvider', function () { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + setup(async () => { + const nullPolicyService = new NullPolicyService(); + const nullLogService = testDisposables.add(new NullLogService()); + const nullFileService = testDisposables.add(new FileService(nullLogService)); + const nullConfigService = testDisposables.add(new ConfigurationService( + URI.file('/config.json'), + nullFileService, + nullPolicyService, + nullLogService, + )); + instantiationService = testDisposables.add(new TestInstantiationService()); + + const fileSystemProvider = testDisposables.add(new InMemoryFileSystemProvider()); + testDisposables.add(nullFileService.registerProvider(Schemas.file, fileSystemProvider)); + + instantiationService.stub(IFileService, nullFileService); + instantiationService.stub(ILogService, nullLogService); + instantiationService.stub(IConfigurationService, nullConfigService); + }); + + test('provides contents of a file', async function () { + const fileService = instantiationService.get(IFileService); + + const fileName = `file-${randomInt(10000)}.prompt.md`; + const fileUri = URI.file(`/${fileName}`); + + if (await fileService.exists(fileUri)) { + await fileService.del(fileUri); + } + await fileService.writeFile(fileUri, VSBuffer.fromString('Hello, world!')); + await wait(5); + + const contentsProvider = testDisposables.add(instantiationService.createInstance( + FilePromptContentProvider, + fileUri, + )); + + let streamOrError: ReadableStream | Error | undefined; + testDisposables.add(contentsProvider.onContentChanged((event) => { + streamOrError = event; + })); + contentsProvider.start(); + + await wait(25); + + assertDefined( + streamOrError, + 'The `streamOrError` must be defined.', + ); + + assert( + !(streamOrError instanceof Error), + `Provider must produce a byte stream, got '${streamOrError}'.`, + ); + + const stream = new LinesDecoder(streamOrError); + + const receivedLines = await stream.consumeAll(); + assert.strictEqual( + receivedLines.length, + 1, + 'Must read the correct number of lines from the provider.', + ); + + const expectedLine = new Line(1, 'Hello, world!'); + const receivedLine = receivedLines[0]; + assert( + receivedLine.equals(expectedLine), + `Expected to receive '${expectedLine}', got '${receivedLine}'.`, + ); + }); +}); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts similarity index 58% rename from code/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts rename to code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index e7256cf7099..ec3a103dd1c 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -4,22 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { URI } from '../../../../../base/common/uri.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { isWindows } from '../../../../../base/common/platform.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { FileService } from '../../../../../platform/files/common/fileService.js'; -import { NullPolicyService } from '../../../../../platform/policy/common/policy.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { PromptFileReference, TErrorCondition } from '../../common/promptFileReference.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ConfigurationService } from '../../../../../platform/configuration/common/configurationService.js'; -import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; -import { FileOpenFailed, RecursiveReference, NonPromptSnippetFile } from '../../common/promptFileReferenceErrors.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { extUri } from '../../../../../../base/common/resources.js'; +import { isWindows } from '../../../../../../base/common/platform.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IPromptFileReference } from '../../../common/promptSyntax/parsers/types.js'; +import { FileService } from '../../../../../../platform/files/common/fileService.js'; +import { NullPolicyService } from '../../../../../../platform/policy/common/policy.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TErrorCondition } from '../../../common/promptSyntax/parsers/basePromptParser.js'; +import { FileReference } from '../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; +import { waitRandom, randomBoolean } from '../../../../../../base/test/common/testUtils.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ConfigurationService } from '../../../../../../platform/configuration/common/configurationService.js'; +import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { NonPromptSnippetFile, RecursiveReference, FileOpenFailed } from '../../../common/promptFileReferenceErrors.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; /** * Represents a file system node. @@ -46,32 +53,25 @@ interface IFolder extends IFilesystemNode { * Represents a file reference with an expected * error condition value for testing purposes. */ -class ExpectedReference extends PromptFileReference { +class ExpectedReference { + /** + * URI component of the expected reference. + */ + public readonly uri: URI; + constructor( - uri: URI, - public readonly error: TErrorCondition | undefined, + dirname: URI, + public readonly lineToken: FileReference, + public readonly errorCondition?: TErrorCondition, ) { - const nullLogService = new NullLogService(); - const nullPolicyService = new NullPolicyService(); - const nullFileService = new FileService(nullLogService); - const nullConfigService = new ConfigurationService( - URI.file('/config.json'), - nullFileService, - nullPolicyService, - nullLogService, - ); - super(uri, nullFileService, nullConfigService); - - this._register(nullFileService); - this._register(nullConfigService); + this.uri = extUri.resolvePath(dirname, lineToken.path); } /** - * Override the error condition getter to - * return the provided expected error value. + * String representation of the expected reference. */ - public override get errorCondition() { - return this.error; + public toString(): string { + return `file-prompt:${this.uri.path}`; } } @@ -84,15 +84,10 @@ class TestPromptFileReference extends Disposable { private readonly rootFileUri: URI, private readonly expectedReferences: ExpectedReference[], @IFileService private readonly fileService: IFileService, - @IConfigurationService private readonly configService: IConfigurationService, + @IInstantiationService private readonly initService: IInstantiationService, ) { super(); - // ensure all the expected references are disposed - for (const expectedReference of this.expectedReferences) { - this._register(expectedReference); - } - // create in-memory file system const fileSystemProvider = this._register(new InMemoryFileSystemProvider()); this._register(this.fileService.registerProvider(Schemas.file, fileSystemProvider)); @@ -108,35 +103,37 @@ class TestPromptFileReference extends Disposable { this.fileStructure, ); + // randomly test with and without delay to ensure that the file + // reference resolution is not suseptible to race conditions + if (randomBoolean()) { + await waitRandom(5); + } + // start resolving references for the specified root file - const rootReference = this._register(new PromptFileReference( - this.rootFileUri, - this.fileService, - this.configService, - )); + const rootReference = this._register( + this.initService.createInstance( + FilePromptParser, + this.rootFileUri, + [], + ), + ).start(); - // resolve the root file reference including all nested references - const resolvedReferences = (await rootReference.resolve(true)) - .flatten(); + // wait until entire prompts tree is resolved + await rootReference.allSettled(); - assert.strictEqual( - resolvedReferences.length, - this.expectedReferences.length, - [ - `\nExpected(${this.expectedReferences.length}): [\n ${this.expectedReferences.join('\n ')}\n]`, - `Received(${resolvedReferences.length}): [\n ${resolvedReferences.join('\n ')}\n]`, - ].join('\n') - ); + // resolve the root file reference including all nested references + const resolvedReferences: readonly (IPromptFileReference | undefined)[] = rootReference.allReferences; for (let i = 0; i < this.expectedReferences.length; i++) { const expectedReference = this.expectedReferences[i]; const resolvedReference = resolvedReferences[i]; assert( - resolvedReference.equals(expectedReference), + (resolvedReference) && + (resolvedReference.uri.toString() === expectedReference.uri.toString()), [ - `Expected ${i}th resolved reference to be ${expectedReference}`, - `got ${resolvedReference}.`, + `Expected ${i}th resolved reference URI to be '${expectedReference.uri}'`, + `got '${resolvedReference?.uri}'.`, ].join(', '), ); @@ -159,6 +156,15 @@ class TestPromptFileReference extends Disposable { ].join(', '), ); } + + assert.strictEqual( + resolvedReferences.length, + this.expectedReferences.length, + [ + `\nExpected(${this.expectedReferences.length}): [\n ${this.expectedReferences.join('\n ')}\n]`, + `Received(${resolvedReferences.length}): [\n ${resolvedReferences.join('\n ')}\n]`, + ].join('\n') + ); } /** @@ -192,6 +198,28 @@ class TestPromptFileReference extends Disposable { } } +/** + * Create expected file reference for testing purposes. + * + * @param filePath The expected path of the file reference (without the `#file:` prefix). + * @param lineNumber The expected line number of the file reference. + * @param startColumnNumber The expected start column number of the file reference. + */ +const createTestFileReference = ( + filePath: string, + lineNumber: number, + startColumnNumber: number, +): FileReference => { + const range = new Range( + lineNumber, + startColumnNumber, + lineNumber, + startColumnNumber + `#file:${filePath}`.length, + ); + + return new FileReference(range, filePath); +}; + suite('PromptFileReference (Unix)', function () { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -235,7 +263,7 @@ suite('PromptFileReference (Unix)', function () { }, { name: 'file2.prompt.md', - contents: '## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this #file:./folder1/some-other-folder/file4.prompt.md please!\n ', + contents: '## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!\n ', }, { name: 'folder1', @@ -249,7 +277,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'file4.prompt.md', - contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\nand some non-prompt #file:./some-non-prompt-file.js', + contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\n\nand some\n non-prompt #file:./some-non-prompt-file.md', }, { name: 'file.txt', @@ -260,7 +288,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'another-file.prompt.md', - contents: 'another-file.prompt.md contents\t #file:../file.txt', + contents: 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', }, { name: 'one_more_file_just_in_case.prompt.md', @@ -268,10 +296,6 @@ suite('PromptFileReference (Unix)', function () { }, ], }, - { - name: 'some-non-prompt-file.js', - contents: 'some-non-prompt-file.js contents', - }, ], }, ], @@ -286,43 +310,46 @@ suite('PromptFileReference (Unix)', function () { * The expected references to be resolved. */ [ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/file3.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 2, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `/${rootFolderName}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 3, + 26, + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + createTestFileReference('../file.txt', 1, 35), new NonPromptSnippetFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), - 'Ughh oh!', + 'Ughh oh, that is not a prompt file!', ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file4.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), + ), + new ExpectedReference( + rootUri, + createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), new FileOpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), - 'Some error message.', + 'Failed to open non-existring prompt snippets file', ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.js'), - new NonPromptSnippetFile( - URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.js'), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-prompt-file.md', 5, 13), + new FileOpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), 'Oh no!', ), - )), + ), ] )); @@ -351,14 +378,14 @@ suite('PromptFileReference (Unix)', function () { }, { name: 'file2.prompt.md', - contents: `## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this #file:./folder1/some-other-folder/file4.prompt.md\n\n#file:${rootFolder}/folder1/some-other-folder/file5.prompt.md\t please!\n\t#file:./file1.md `, + contents: `## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this #file:./folder1/some-other-folder/file4.prompt.md\n\n#file:${rootFolder}/folder1/some-other-folder/file5.prompt.md\t please!\n\t[some (snippet!) #name))](./file1.md)`, }, { name: 'folder1', children: [ { name: 'file3.prompt.md', - contents: `\n\n\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents\n some more\t content`, + contents: `\n\n\t- some seemingly random [another-file.prompt.md](${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md) contents\n some more\t content`, }, { name: 'some-other-folder', @@ -399,25 +426,25 @@ suite('PromptFileReference (Unix)', function () { * The expected references to be resolved. */ [ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/file3.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md'), - undefined, - )), + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 2, 9), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 3, + 23, + ), + ), /** - * This reference should be resolved as - * a recursive reference error condition. - * (the absolute reference case) + * This reference should be resolved with a recursive + * reference error condition. (the absolute reference case) */ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + createTestFileReference(`${rootFolder}/file2.prompt.md`, 2, 6), new RecursiveReference( URI.joinPath(rootUri, './file2.prompt.md'), [ @@ -427,29 +454,36 @@ suite('PromptFileReference (Unix)', function () { '/infinite-recursion/file2.prompt.md', ], ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file4.prompt.md'), + ), + new ExpectedReference( + rootUri, + createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-non-existing/file.prompt.md'), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('../some-non-existing/file.prompt.md', 1, 30), new FileOpenFailed( URI.joinPath(rootUri, './folder1/some-non-existing/file.prompt.md'), - 'Some error message.', + 'Uggh ohh!', + ), + ), + new ExpectedReference( + rootUri, + createTestFileReference( + `${rootFolder}/folder1/some-other-folder/file5.prompt.md`, + 5, + 1, ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file5.prompt.md'), undefined, - )), + ), /** - * This reference should be resolved as - * a recursive reference error condition. - * (the relative reference case) + * This reference should be resolved with a recursive + * reference error condition. (the relative reference case) */ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('../../file2.prompt.md', 1, 36), new RecursiveReference( URI.joinPath(rootUri, './file2.prompt.md'), [ @@ -458,14 +492,15 @@ suite('PromptFileReference (Unix)', function () { '/infinite-recursion/file2.prompt.md', ], ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file1.md'), + ), + new ExpectedReference( + rootUri, + createTestFileReference('./file1.md', 6, 2), new NonPromptSnippetFile( URI.joinPath(rootUri, './file1.md'), 'Uggh oh!', ), - )), + ), ] )); diff --git a/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index f84b3ba640b..0005bca091e 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -66,9 +66,11 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; + readonly onDidChangeToolsAgentModeEnabled = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } + setRequestPaused(agent: string, requestId: string, isPaused: boolean): void { throw new Error('not implemented'); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getActivatedAgents(): IChatAgent[] { return agents; } getAgents(): IChatAgent[] { return agents; } diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts index f73fd0bd85c..f2262f6a498 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -8,7 +8,7 @@ import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from '../../../../ import { DiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; import { localize } from '../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyEqualsExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -16,7 +16,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { getCommentCommandInfo } from '../../accessibility/browser/editorAccessibilityHelp.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation { +export class DiffEditorAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'diff-editor'; readonly when = ContextKeyEqualsExpr.create('isInDiffEditor', true); diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/code/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 76bc1b9ddb0..524d3457777 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -12,6 +12,7 @@ import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.j import { IActiveCodeEditor, ICodeEditor, IDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorAction, EditorContributionInstantiation, ServicesAccessor, registerDiffEditorContribution, registerEditorAction, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { IDiffEditorContribution, IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -19,6 +20,7 @@ import { ITextModel } from '../../../../editor/common/model.js'; import * as nls from '../../../../nls.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -68,6 +70,7 @@ class ToggleWordWrapAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { const codeEditorService = accessor.get(ICodeEditorService); + const instaService = accessor.get(IInstantiationService); if (!canToggleWordWrap(codeEditorService, editor)) { return; @@ -93,7 +96,7 @@ class ToggleWordWrapAction extends EditorAction { writeTransientState(model, newState, codeEditorService); // if we are in a diff editor, update the other editor (if possible) - const diffEditor = findDiffEditorContainingCodeEditor(editor, codeEditorService); + const diffEditor = instaService.invokeFunction(findDiffEditorContainingCodeEditor, editor); if (diffEditor) { const originalEditor = diffEditor.getOriginalEditor(); const modifiedEditor = diffEditor.getModifiedEditor(); @@ -106,24 +109,6 @@ class ToggleWordWrapAction extends EditorAction { } } -/** - * If `editor` is the original or modified editor of a diff editor, it returns it. - * It returns null otherwise. - */ -function findDiffEditorContainingCodeEditor(editor: ICodeEditor, codeEditorService: ICodeEditorService): IDiffEditor | null { - if (!editor.getOption(EditorOption.inDiffEditor)) { - return null; - } - for (const diffEditor of codeEditorService.listDiffEditors()) { - const originalEditor = diffEditor.getOriginalEditor(); - const modifiedEditor = diffEditor.getModifiedEditor(); - if (originalEditor === editor || modifiedEditor === editor) { - return diffEditor; - } - } - return null; -} - class ToggleWordWrapController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.toggleWordWrapController'; diff --git a/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 3cf98a0cde9..be76ed22a85 100644 --- a/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -142,8 +142,9 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'comments.thread.confirmOnCollapse': { type: 'string', enum: ['whenHasUnsubmittedComments', 'never'], + enumDescriptions: [nls.localize('confirmOnCollapse.whenHasUnsubmittedComments', "Show a confirmation dialog when collapsing a comment thread with unsubmitted comments."), nls.localize('confirmOnCollapse.never', "Never show a confirmation dialog when collapsing a comment thread.")], default: 'never', - description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread with unsubmitted comments.") + description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread.") } } }); diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/code/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 4a52b4036ff..89a885968f0 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -12,7 +12,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { CommentCommandId } from '../common/commentCommandIds.js'; import { ToggleTabFocusModeAction } from '../../../../editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.js'; import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -44,7 +44,7 @@ export class CommentsAccessibilityHelpProvider extends Disposable implements IAc } } -export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { +export class CommentsAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 110; readonly name = 'comments'; readonly type = AccessibleViewType.Help; diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/code/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts index 655618ac92c..c18066cbe58 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -7,7 +7,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IMenuService } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -24,7 +24,7 @@ import { URI } from '../../../../base/common/uri.js'; import { CommentThread, Comment } from '../../../../editor/common/languages.js'; import { IRange } from '../../../../editor/common/core/range.js'; -export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplentation { +export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplementation { readonly priority = 90; readonly name = 'comment'; readonly when = CONTEXT_KEY_COMMENT_FOCUSED; @@ -50,7 +50,7 @@ export class CommentsAccessibleView extends Disposable implements IAccessibleVie } -export class CommentThreadAccessibleView extends Disposable implements IAccessibleViewImplentation { +export class CommentThreadAccessibleView extends Disposable implements IAccessibleViewImplementation { readonly priority = 85; readonly name = 'commentThread'; readonly when = CommentContextKeys.commentFocused; diff --git a/code/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts b/code/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts index 299374e3154..3b994c5e514 100644 --- a/code/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts @@ -5,7 +5,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { getReplView, Repl } from './repl.js'; @@ -13,7 +13,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { localize } from '../../../../nls.js'; -export class ReplAccessibilityHelp implements IAccessibleViewImplentation { +export class ReplAccessibilityHelp implements IAccessibleViewImplementation { priority = 120; name = 'replHelp'; when = ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view'); diff --git a/code/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts b/code/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts index 43ec21a1cd3..638dd70b513 100644 --- a/code/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts @@ -6,7 +6,7 @@ import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { IReplElement } from '../common/debug.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { getReplView, Repl } from './repl.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; @@ -15,7 +15,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Position } from '../../../../editor/common/core/position.js'; -export class ReplAccessibleView implements IAccessibleViewImplentation { +export class ReplAccessibleView implements IAccessibleViewImplementation { priority = 70; name = 'debugConsole'; when = ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view'); diff --git a/code/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts b/code/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts index 90920dc95c0..fcdf564b93c 100644 --- a/code/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts @@ -6,7 +6,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -17,7 +17,7 @@ import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneString import { FocusedViewContext, SidebarFocusContext } from '../../../common/contextkeys.js'; import { BREAKPOINTS_VIEW_ID, CALLSTACK_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, VARIABLES_VIEW_ID, WATCH_VIEW_ID } from '../common/debug.js'; -export class RunAndDebugAccessibilityHelp implements IAccessibleViewImplentation { +export class RunAndDebugAccessibilityHelp implements IAccessibleViewImplementation { priority = 120; name = 'runAndDebugHelp'; when = ContextKeyExpr.or( diff --git a/code/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts b/code/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts index 4bdae2d1cf4..08ff7d57671 100644 --- a/code/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts +++ b/code/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts @@ -7,6 +7,7 @@ import { joinPath } from '../../../../base/common/resources.js'; import { localize } from '../../../../nls.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { AbstractLogger, ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { windowLogGroup } from '../../../services/log/common/logConstants.js'; import { IEditSessionsLogService, editSessionsLogId } from './editSessions.js'; export class EditSessionsLogService extends AbstractLogger implements IEditSessionsLogService { @@ -19,7 +20,7 @@ export class EditSessionsLogService extends AbstractLogger implements IEditSessi @IEnvironmentService environmentService: IEnvironmentService ) { super(); - this.logger = this._register(loggerService.createLogger(joinPath(environmentService.logsHome, `${editSessionsLogId}.log`), { id: editSessionsLogId, name: localize('cloudChangesLog', "Cloud Changes") })); + this.logger = this._register(loggerService.createLogger(joinPath(environmentService.logsHome, `${editSessionsLogId}.log`), { id: editSessionsLogId, name: localize('cloudChangesLog', "Cloud Changes"), group: windowLogGroup })); } trace(message: string, ...args: any[]): void { diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index a47806e80f4..8542167a79b 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -87,7 +87,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { ByteSize, IFileService } from '../../../../platform/files/common/files.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; @@ -95,19 +95,6 @@ function toDateString(date: Date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}, ${date.toLocaleTimeString(language, { hourCycle: 'h23' })}`; } -function toMemoryString(bytes: number) { - if (bytes < 1024) { - return `${bytes} B`; - } - if (bytes < 1024 * 1024) { - return `${(bytes / 1024).toFixed(1)} KB`; - } - if (bytes < 1024 * 1024 * 1024) { - return `${(bytes / 1024 / 1024).toFixed(1)} MB`; - } - return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; -} - class NavBar extends Disposable { private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>()); @@ -1188,7 +1175,7 @@ class AdditionalDetailsWidget extends Disposable { } } if (extension.size) { - const element = $('div', undefined, toMemoryString(extension.size)); + const element = $('div', undefined, ByteSize.formatSize(extension.size)); append(installInfo, $('.more-info-entry', undefined, $('div.more-info-entry-name', { title: localize('size when installed', "Size when installed") }, localize('size', "Size")), @@ -1209,7 +1196,7 @@ class AdditionalDetailsWidget extends Disposable { if (!cacheSize) { return; } - const element = $('div', undefined, toMemoryString(cacheSize)); + const element = $('div', undefined, ByteSize.formatSize(cacheSize)); append(installInfo, $('.more-info-entry', undefined, $('div.more-info-entry-name', { title: localize('disk space used', "Cache size") }, localize('cache size', "Cache")), diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 16ed8c627dd..ba4ab190746 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -30,13 +30,6 @@ import { IRemoteExtensionsScannerService } from '../../../../platform/remote/com import { IUserDataInitializationService } from '../../../services/userData/browser/userDataInit.js'; import { isString } from '../../../../base/common/types.js'; -type IgnoreRecommendationClassification = { - owner: 'sandy081'; - comment: 'Report when a recommendation is ignored'; - recommendationReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Reason why extension is recommended' }; - extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Id of the extension recommendation that is being ignored' }; -}; - export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { declare readonly _serviceBrand: undefined; @@ -115,14 +108,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ]); this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); - this._register(this.extensionRecommendationsManagementService.onDidChangeGlobalIgnoredRecommendation(({ extensionId, isRecommended }) => { - if (!isRecommended) { - const reason = this.getAllRecommendationsWithReason()[extensionId]; - if (reason && reason.reasonId) { - this.telemetryService.publicLog2<{ extensionId: string; recommendationReason: ExtensionRecommendationReason }, IgnoreRecommendationClassification>('extensionsRecommendations:ignoreRecommendation', { extensionId, recommendationReason: reason.reasonId }); - } - } - })); await new Promise(resolve => setTimeout(resolve, 3000)); this.promptWorkspaceRecommendations(); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 38d1e971bbe..15986eaead3 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -58,7 +58,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { ShowRuntimeExtensionsAction } from './abstractRuntimeExtensionsEditor.js'; import { ExtensionEnablementWorkspaceTrustTransitionParticipant } from './extensionEnablementWorkspaceTrustTransitionParticipant.js'; import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from './extensionsIcons.js'; -import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; +import { EXTENSION_CATEGORIES, ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { Disposable, DisposableStore, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { mnemonicButtonLabel } from '../../../../base/common/labels.js'; @@ -71,13 +71,14 @@ import { Event } from '../../../../base/common/event.js'; import { UnsupportedExtensionsMigrationContrib } from './unsupportedExtensionsMigrationContribution.js'; import { isLinux, isNative, isWeb } from '../../../../base/common/platform.js'; import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { CONTEXT_KEYBINDINGS_EDITOR } from '../../preferences/common/preferences.js'; import { ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationExtensions } from '../../../common/configuration.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -1924,6 +1925,41 @@ class ExtensionStorageCleaner implements IWorkbenchContribution { } } +class TrustedPublishersInitializer implements IWorkbenchContribution { + constructor( + @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, + @IProductService productService: IProductService, + @IStorageService storageService: IStorageService, + ) { + const trustedPublishersInitStatusKey = 'trusted-publishers-migration'; + if (!storageService.get(trustedPublishersInitStatusKey, StorageScope.APPLICATION)) { + for (const profile of userDataProfilesService.profiles) { + extensionManagementService.getInstalled(ExtensionType.User, profile.extensionsResource) + .then(async extensions => { + const trustedPublishers = new Set(); + for (const extension of extensions) { + if (!extension.publisherId) { + continue; + } + const publisher = extension.manifest.publisher.toLowerCase(); + if (productService.trustedExtensionPublishers?.includes(publisher) + || (extension.publisherDisplayName && productService.trustedExtensionPublishers?.includes(extension.publisherDisplayName.toLowerCase()))) { + continue; + } + trustedPublishers.add(publisher); + } + if (trustedPublishers.size) { + extensionManagementService.trustPublishers(...trustedPublishers); + } + storageService.store(trustedPublishersInitStatusKey, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE); + }); + } + } + } +} + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Eventually); @@ -1935,6 +1971,7 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, Life workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrustTransitionParticipant, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UnsupportedExtensionsMigrationContrib, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(TrustedPublishersInitializer, LifecyclePhase.Eventually); if (isWeb) { workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually); } diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 052e14050eb..83bf9531cc5 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -417,7 +417,7 @@ export class InstallAction extends ExtensionAction { this.hideOnDisabled = false; this.options = { isMachineScoped: false, ...options }; this.update(); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this)); } @@ -1024,7 +1024,7 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { this.update(); } })); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(e => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(e => this.update())); this.update(); } @@ -1458,7 +1458,7 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(TogglePreReleaseExtensionAction.ID, TogglePreReleaseExtensionAction.LABEL, TogglePreReleaseExtensionAction.DisabledClass); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this.update(); } @@ -1534,7 +1534,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(InstallAnotherVersionAction.ID, InstallAnotherVersionAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this.extension = extension; this.update(); } @@ -2521,7 +2521,7 @@ export class ExtensionStatusAction extends ExtensionAction { this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this._register(this.extensionFeaturesManagementService.onDidChangeAccessData(() => this.update())); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this.update(); } diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index 868e006ddab..f7871ef4e2b 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -24,7 +24,6 @@ export const syncIgnoredIcon = registerIcon('extensions-sync-ignored', Codicon.s export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, localize('remoteIcon', 'Icon to indicate that an extension is remote in the extensions view and editor.')); export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.')); export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.')); -export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.')); export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.versions, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.')); export const sponsorIcon = registerIcon('extensions-sponsor', Codicon.heartFilled, localize('sponsorIcon', 'Icon used for sponsoring extensions in the extensions view and editor.')); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 2afa3e73bbf..8e2e5ab9691 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -15,7 +15,7 @@ import { Event } from '../../../../base/common/event.js'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService, IExtensionsViewState } from '../common/extensions.js'; import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction } from './extensionsActions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; -import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from './extensionsWidgets.js'; +import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, VerifiedPublisherWidget } from './extensionsWidgets.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -24,8 +24,8 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { WORKBENCH_BACKGROUND } from '../../../common/theme.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; -import { verifiedPublisherIcon as verifiedPublisherThemeIcon } from './extensionsIcons.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; const EXTENSION_LIST_ELEMENT_HEIGHT = 72; @@ -114,7 +114,7 @@ export class Renderer implements IPagedRenderer { focusOnlyEnabledItems: true }); actionbar.setFocusable(false); - actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); + const actionBarListener = actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); const extensionStatusIconAction = this.instantiationService.createInstance(ExtensionStatusAction); const actions = [ @@ -150,7 +150,7 @@ export class Renderer implements IPagedRenderer { const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); actionbar.push(actions, { icon: true, label: true }); - const disposable = combinedDisposable(...actions, ...widgets, actionbar, extensionContainers); + const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers); return { root, element, icon, name, installCount, ratings, description, publisherDisplayName, disposables: [disposable], actionbar, @@ -250,6 +250,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const verifiedPublisherIconColor = theme.getColor(extensionVerifiedPublisherIconColor); if (verifiedPublisherIconColor) { const disabledVerifiedPublisherIconColor = verifiedPublisherIconColor.transparent(.5).makeOpaque(WORKBENCH_BACKGROUND(theme)); - collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled:not(.selected) .author .verified-publisher ${ThemeIcon.asCSSSelector(verifiedPublisherThemeIcon)} { color: ${disabledVerifiedPublisherIconColor}; }`); + collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled:not(.selected) .author .verified-publisher ${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${disabledVerifiedPublisherIconColor}; }`); } }); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 729d5d96019..17bba8cc4fa 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from './extensionsIcons.js'; +import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js'; import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -38,7 +38,6 @@ import { onUnexpectedError } from '../../../../base/common/errors.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -46,6 +45,7 @@ import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -247,7 +247,6 @@ export class SponsorWidget extends ExtensionWidget { private container: HTMLElement, @IHoverService private readonly hoverService: IHoverService, @IOpenerService private readonly openerService: IOpenerService, - @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); this.render(); @@ -267,15 +266,6 @@ export class SponsorWidget extends ExtensionWidget { const label = $('span', undefined, localize('sponsor', "Sponsor")); append(sponsor, sponsorIconElement, label); this.disposables.add(onClick(sponsor, () => { - type SponsorExtensionClassification = { - owner: 'sandy081'; - comment: 'Reporting when sponosor extension action is executed'; - 'extensionId': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension to be sponsored' }; - }; - type SponsorExtensionEvent = { - 'extensionId': string; - }; - this.telemetryService.publicLog2('extensionsAction.sponsorExtension', { extensionId: this.extension!.identifier.id }); this.openerService.open(this.extension!.publisherSponsorLink!); })); } @@ -870,7 +860,6 @@ export class ExtensionRecommendationWidget extends ExtensionWidget { } export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false); -export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', textLinkForeground, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), false); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false); export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 56e0f1638ca..6a278cac432 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -847,11 +847,6 @@ class Extensions extends Disposable { if (!this.galleryService.isEnabled()) { return; } - type GalleryServiceMatchInstalledExtensionClassification = { - owner: 'sandy081'; - comment: 'Report when a request is made to match installed extension with gallery'; - }; - this.telemetryService.publicLog2<{}, GalleryServiceMatchInstalledExtensionClassification>('galleryService:matchInstalledExtension'); const galleryExtensions = await this.galleryService.getExtensions(toMatch.map(e => ({ ...e.identifier, preRelease: e.local?.preRelease })), { compatible: true, targetPlatform: await this.server.extensionManagementService.getTargetPlatform() }, CancellationToken.None); for (const extension of extensions) { const compatible = galleryExtensions.find(e => areSameExtensions(e.identifier, extension.identifier)); @@ -1106,7 +1101,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); - this._register(this.allowedExtensionsService.onDidChangeAllowedExtensions(() => { + this._register(this.allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => { if (this.isAutoCheckUpdatesEnabled()) { this.checkForUpdates(); } @@ -1958,7 +1953,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.progressService.withProgress({ location: ProgressLocation.Notification }, async progress => { - progress.report({ message: nls.localize('downloading...', "Downlading VSIX...") }); + progress.report({ message: nls.localize('downloading...', "Downloading VSIX...") }); const name = `${galleryExtension.identifier.id}-${galleryExtension.version}${targetPlatform !== TargetPlatform.UNDEFINED && targetPlatform !== TargetPlatform.UNIVERSAL && targetPlatform !== TargetPlatform.UNKNOWN ? `-${targetPlatform}` : ''}.vsix`; await this.galleryService.download(galleryExtension, this.uriIdentityService.extUri.joinPath(result[0], name), InstallOperation.None); this.notificationService.info(nls.localize('download.completed', "Successfully downloaded the VSIX")); @@ -2925,29 +2920,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async doSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise { - const changed = await this.extensionEnablementService.setEnablement(extensions.map(e => e.local!), enablementState); - for (let i = 0; i < changed.length; i++) { - if (changed[i]) { - /* __GDPR__ - "extension:enable" : { - "owner": "sandy081", - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - /* __GDPR__ - "extension:disable" : { - "owner": "sandy081", - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - this.telemetryService.publicLog(enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace ? 'extension:enable' : 'extension:disable', extensions[i].telemetryData); - } - } - return changed; + return await this.extensionEnablementService.setEnablement(extensions.map(e => e.local!), enablementState); } // Current service reports progress when installing/uninstalling extensions diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index bff7df509c7..a380a714200 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -228,6 +228,7 @@ suite('ExtensionRecommendationsService Test', () => { onDidUninstallExtension: Event.None, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index dca3201e44d..bce84045b3e 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -101,6 +101,7 @@ function setupTest(disposables: Pick) { onDidUninstallExtension: didUninstallEvent.event as any, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, @@ -2642,6 +2643,7 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP onDidUninstallExtension: Event.None, onDidChangeProfile: Event.None, onDidUpdateExtensionMetadata: Event.None, + onProfileAwareDidInstallExtensions: Event.None, getInstalled: () => Promise.resolve(installed), canInstall: async (extension: IGalleryExtension) => { return true; }, installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 19268a3cecd..3752be94aaa 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -101,6 +101,7 @@ suite('ExtensionsViews Tests', () => { onDidUninstallExtension: Event.None, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async canInstall() { return true; }, diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index c49ca4dfa18..9c8f2b5c983 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -102,6 +102,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUninstallExtension: didUninstallEvent.event as any, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, @@ -1706,6 +1707,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUninstallExtension: Event.None, onDidChangeProfile: Event.None, onDidUpdateExtensionMetadata: Event.None, + onProfileAwareDidInstallExtensions: Event.None, getInstalled: () => Promise.resolve(installed), installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), updateMetadata: async (local: Mutable, metadata: Partial, profileLocation: URI) => { diff --git a/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index d21998d3bb6..dbc04e88561 100644 --- a/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -56,6 +56,7 @@ import { mainWindow } from '../../../../../base/browser/window.js'; import { EditorGroupView } from '../../../../browser/parts/editor/editorGroupView.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; const $ = dom.$; @@ -943,3 +944,24 @@ registerAction2(class extends Action2 { await commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID); } }); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'openEditors.configure', + title: nls.localize('configureOpenEditorsView', 'Configure \'{0}\'', OpenEditorsView.NAME.value), + f1: false, + icon: Codicon.gear, + menu: { + id: MenuId.ViewTitle, + group: '9_configure', + when: ContextKeyExpr.equals('view', OpenEditorsView.ID), + order: 10 + } + }); + } + async run(accessor: ServicesAccessor): Promise { + const preferencesService = accessor.get(IPreferencesService); + preferencesService.openSettings({ jsonEditor: false, query: '@feature:explorer openEditors' }); + } +}); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 3572a0801b3..b5da56628a4 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -13,9 +13,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; -import { InlineChatSavingServiceImpl } from './inlineChatSavingServiceImpl.js'; import { InlineChatAccessibleView } from './inlineChatAccessibleView.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -25,12 +23,15 @@ import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHintAction, ShowInlineChatHintAction } from './inlineChatCurrentLine.js'; +import { InlineChatController2, StartSessionAction2, StopSessionAction2 } from './inlineChatController2.js'; +registerEditorContribution(InlineChatController2.ID, InlineChatController2, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors +registerAction2(StartSessionAction2); +registerAction2(StopSessionAction2); // --- browser registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts index 338697c4b7b..bce9724997c 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts @@ -6,13 +6,13 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { getChatAccessibilityHelpProvider } from '../../chat/browser/actions/chatAccessibilityHelp.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from '../common/inlineChat.js'; -export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation { +export class InlineChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 106; readonly name = 'inlineChat'; readonly type = AccessibleViewType.Help; diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index b1d46e25c5a..3f733e33748 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -9,12 +9,12 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { renderMarkdownAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; -export class InlineChatAccessibleView implements IAccessibleViewImplentation { +export class InlineChatAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'inlineChat'; readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 30dd09c6e48..561bef81e76 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -264,7 +264,7 @@ export class AcceptChanges extends AbstractInlineChatAction { shortTitle: localize('apply2', 'Accept'), icon: Codicon.check, f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview))), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE), keybinding: [{ weight: KeybindingWeight.WorkbenchContrib + 10, primary: KeyMod.CtrlCmd | KeyCode.Enter, @@ -300,16 +300,6 @@ export class DiscardHunkAction extends AbstractInlineChatAction { icon: Codicon.chromeClose, precondition: CTX_INLINE_CHAT_VISIBLE, menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 2, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits), - CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live) - ), - }, { id: MENU_INLINE_CHAT_ZONE, group: 'navigation', order: 2 @@ -392,10 +382,7 @@ export class CloseAction extends AbstractInlineChatAction { order: 1, when: ContextKeyExpr.and( CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - ContextKeyExpr.or( - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), - CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview) - ) + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages) ), }] }); @@ -506,7 +493,7 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { constructor() { super({ id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_CHANGE_HAS_DIFF), title: localize2('showChanges', 'Toggle Changes'), icon: Codicon.diffSingle, toggled: { @@ -515,7 +502,6 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { menu: [{ id: MENU_INLINE_CHAT_WIDGET_STATUS, group: 'zzz', - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), order: 1, }, { id: MENU_INLINE_CHAT_ZONE, diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 9b4f17b4173..9c6b69b3e4b 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -12,6 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MovingAverage } from '../../../../base/common/numbers.js'; +import { autorun } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; @@ -40,15 +41,15 @@ import { showChatView } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatError } from './inlineChatSessionServiceImpl.js'; -import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; +import { HunkAction, IEditObserver, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; export const enum State { @@ -131,19 +132,19 @@ export class InlineChatController implements IEditorContribution { private readonly _sessionStore = this._store.add(new DisposableStore()); private readonly _stashedSession = this._store.add(new MutableDisposable()); private _session?: Session; - private _strategy?: EditModeStrategy; + private _strategy?: LiveStrategy; constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { @@ -185,7 +186,7 @@ export class InlineChatController implements IEditorContribution { } } - const zone = _instaService.createInstance(InlineChatZoneWidget, location, this._editor); + const zone = _instaService.createInstance(InlineChatZoneWidget, location, undefined, this._editor); this._store.add(zone); this._store.add(zone.widget.chatWidget.onDidClear(async () => { const r = this.joinCurrentRun(); @@ -255,10 +256,6 @@ export class InlineChatController implements IEditorContribution { return INLINE_CHAT_ID; } - private _getMode(): EditMode { - return this._configurationService.getValue(InlineChatConfigKeys.Mode); - } - getWidgetPosition(): Position | undefined { return this._ui.value.position; } @@ -338,7 +335,7 @@ export class InlineChatController implements IEditorContribution { try { session = await this._inlineChatSessionService.createSession( this._editor, - { editMode: this._getMode(), wholeRange: options.initialRange }, + { wholeRange: options.initialRange }, createSessionCts.token ); } catch (error) { @@ -371,15 +368,7 @@ export class InlineChatController implements IEditorContribution { await session.chatModel.waitForInitialization(); // create a new strategy - switch (session.editMode) { - case EditMode.Preview: - this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._editor, this._ui.value); - break; - case EditMode.Live: - default: - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless); - break; - } + this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless); this._session = session; return State.INIT_UI; @@ -432,7 +421,7 @@ export class InlineChatController implements IEditorContribution { this._ctxUserDidEdit.set(altVersionNow !== this._editor.getModel()?.getAlternativeVersionId()); } - if (this._session?.hunkData.ignoreTextModelNChanges || this._strategy?.hasFocus()) { + if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) { return; } @@ -658,7 +647,6 @@ export class InlineChatController implements IEditorContribution { const newSession = await this._inlineChatSessionService.createSession( newEditor, { - editMode: this._getMode(), session: this._session, }, CancellationToken.None); // TODO@ulugbekna: add proper cancellation? @@ -974,7 +962,6 @@ export class InlineChatController implements IEditorContribution { stop: () => this._session!.hunkData.ignoreTextModelNChanges = false, }; - this._inlineChatSavingService.markChanged(this._session); if (opts) { await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { @@ -1116,13 +1103,8 @@ export class InlineChatController implements IEditorContribution { finishExistingSession(): void { if (this._session) { - if (this._session.editMode === EditMode.Preview) { - this._log('finishing existing session, using CANCEL', this._session.editMode); - this.cancelSession(); - } else { - this._log('finishing existing session, using APPLY', this._session.editMode); - this.acceptSession(); - } + this._log('finishing existing session, using APPLY'); + this.acceptSession(); } } @@ -1142,9 +1124,6 @@ export class InlineChatController implements IEditorContribution { unstashLastSession(): Session | undefined { const result = this._stashedSession.value?.unstash(); - if (result) { - this._inlineChatSavingService.markChanged(result); - } return result; } @@ -1152,44 +1131,52 @@ export class InlineChatController implements IEditorContribution { return this._currentRun; } - async reviewEdits(anchor: IRange, stream: AsyncIterable, token: CancellationToken) { + async reviewEdits(stream: AsyncIterable, token: CancellationToken) { if (!this._editor.hasModel()) { return false; } - const session = await this._inlineChatSessionService.createSession(this._editor, { editMode: EditMode.Live, wholeRange: anchor, headless: true }, token); - if (!session) { - return false; - } + const uri = this._editor.getModel().uri; + const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); - const request = session.chatModel.addRequest({ text: 'DUMMY', parts: [] }, { variables: [] }, 0); - const run = this.run({ - existingSession: session, - headless: true - }); + const editSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); - await Event.toPromise(Event.filter(this._onDidEnterState.event, candidate => candidate === State.SHOW_REQUEST)); + // + const store = new DisposableStore(); + store.add(chatModel); + store.add(editSession); + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); for await (const chunk of stream) { - session.chatModel.acceptResponseProgress(request, { kind: 'textEdit', uri: this._editor.getModel()!.uri, edits: chunk }); + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); } + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - if (token.isCancellationRequested) { - session.chatModel.cancelRequest(request); - } else { - session.chatModel.completeResponse(request); + if (!token.isCancellationRequested) { + chatRequest.response.complete(); } - await Event.toPromise(Event.filter(this._onDidEnterState.event, candidate => candidate === State.WAIT_FOR_INPUT)); + const whenDecided = new Promise(resolve => { + store.add(autorun(r => { + if (!editSession.entries.read(r).some(e => e.state.read(r) === WorkingSetEntryState.Modified)) { + resolve(undefined); + } + })); + }); - if (session.hunkData.pending === 0) { - // no real changes, just cancel - this.cancelSession(); - } + await raceCancellation(whenDecided, token); + + store.dispose(); - const dispo = token.onCancellationRequested(() => this.cancelSession()); - await raceCancellation(run, token); - dispo.dispose(); return true; } } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts new file mode 100644 index 00000000000..688280bb744 --- /dev/null +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -0,0 +1,375 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun, autorunWithStore, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { assertType } from '../../../../base/common/types.js'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; +import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; +import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { IAction2Options, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ctxIsGlobalEditingSession } from '../../chat/browser/chatEditorController.js'; +import { ChatEditorOverlayController } from '../../chat/browser/chatEditorOverlay.js'; +import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; +import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; +import { WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; +import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; +import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js'; +import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; + + +export const CTX_HAS_SESSION = new RawContextKey('inlineChatHasSession', undefined, localize('chat.hasInlineChatSession', "The current editor has an active inline chat session")); + + +export class InlineChatController2 implements IEditorContribution { + + static readonly ID = 'editor.contrib.inlineChatController2'; + + static get(editor: ICodeEditor): InlineChatController2 | undefined { + return editor.getContribution(InlineChatController2.ID) ?? undefined; + } + + private readonly _store = new DisposableStore(); + + + private readonly _showWidgetOverrideObs = observableValue(this, false); + private readonly _isActiveController = observableValue(this, false); + + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, + @IInlineChatSessionService inlineChatSessions: IInlineChatSessionService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + + const ctxHasSession = CTX_HAS_SESSION.bindTo(contextKeyService); + const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + + const location: IChatWidgetLocationOptions = { + location: ChatAgentLocation.Editor, + resolveData: () => { + assertType(this._editor.hasModel()); + + return { + type: ChatAgentLocation.Editor, + selection: this._editor.getSelection(), + document: this._editor.getModel().uri, + wholeRange: this._editor.getSelection(), + }; + } + }; + + // inline chat in notebooks + // check if this editor is part of a notebook editor + // and iff so, use the notebook location but keep the resolveData + // talk about editor data + for (const notebookEditor of this._notebookEditorService.listNotebookEditors()) { + for (const [, codeEditor] of notebookEditor.codeEditors) { + if (codeEditor === this._editor) { + location.location = ChatAgentLocation.Notebook; + break; + } + } + } + + const zone = this._instaService.createInstance(InlineChatZoneWidget, + location, + { + enableWorkingSet: 'implicit', + // filter: item => isRequestVM(item), + rendererOptions: { + renderCodeBlockPills: true, + renderTextEditsAsSummary: uri => isEqual(uri, _editor.getModel()?.uri) + } + }, + this._editor + ); + + zone.domNode.classList.add('inline-chat-2'); + + const overlay = ChatEditorOverlayController.get(_editor)!; + + const editorObs = observableCodeEditor(_editor); + + const sessionsSignal = observableSignalFromEvent(this, inlineChatSessions.onDidChangeSessions); + + const sessionObs = derived(r => { + sessionsSignal.read(r); + const model = editorObs.model.read(r); + const value = model && inlineChatSessions.getSession2(model.uri); + return value ?? undefined; + }); + + + this._store.add(autorunWithStore((r, store) => { + const session = sessionObs.read(r); + + if (!session) { + ctxHasSession.set(undefined); + this._isActiveController.set(false, undefined); + } else { + const checkRequests = () => ctxHasSession.set(session.chatModel.getRequests().length === 0 ? 'empty' : 'active'); + store.add(session.chatModel.onDidChange(checkRequests)); + checkRequests(); + } + })); + + const visibleSessionObs = observableValue(this, undefined); + + this._store.add(autorunWithStore((r, store) => { + + const session = sessionObs.read(r); + const isActive = this._isActiveController.read(r); + + if (!session || !isActive) { + visibleSessionObs.set(undefined, undefined); + return; + } + + const { chatModel } = session; + const showShowUntil = this._showWidgetOverrideObs.read(r); + const hasNoRequests = chatModel.getRequests().length === 0; + + store.add(chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + transaction(tx => { + this._showWidgetOverrideObs.set(false, tx); + visibleSessionObs.set(undefined, tx); + }); + } + })); + + if (showShowUntil || hasNoRequests) { + visibleSessionObs.set(session, undefined); + } else { + visibleSessionObs.set(undefined, undefined); + } + })); + + this._store.add(autorun(r => { + + const session = visibleSessionObs.read(r); + + if (!session) { + zone.hide(); + _editor.focus(); + ctxInlineChatVisible.reset(); + } else { + ctxInlineChatVisible.set(true); + zone.widget.setChatModel(session.chatModel); + if (!zone.position) { + zone.show(session.initialPosition); + } + zone.reveal(zone.position!); + zone.widget.focus(); + session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); + } + })); + + this._store.add(autorun(r => { + + const session = sessionObs.read(r); + const model = editorObs.model.read(r); + if (!session || !model) { + overlay.hide(); + return; + } + + const lastResponse = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().at(-1)?.response); + const response = lastResponse.read(r); + + const isInProgress = response + ? observableFromEvent(this, response.onDidChange, () => !response.isComplete) + : constObservable(false); + + if (isInProgress.read(r)) { + overlay.showRequest(session.editingSession); + } else if (session.editingSession.getEntry(session.uri)?.state.get() !== WorkingSetEntryState.Modified) { + overlay.hide(); + } + })); + } + + dispose(): void { + this._store.dispose(); + } + + toggleWidgetUntilNextRequest() { + const value = this._showWidgetOverrideObs.get(); + this._showWidgetOverrideObs.set(!value, undefined); + } + + markActiveController() { + this._isActiveController.set(true, undefined); + } +} + +export class StartSessionAction2 extends EditorAction2 { + + constructor() { + super({ + id: 'inlineChat2.start', + title: localize2('start', "Inline Chat"), + precondition: ContextKeyExpr.and( + CTX_INLINE_CHAT_HAS_AGENT2, + CTX_INLINE_CHAT_POSSIBLE, + CTX_HAS_SESSION.negate(), + EditorContextKeys.writable, + EditorContextKeys.editorSimpleInput.negate() + ), + f1: true, + category: AbstractInlineChatAction.category, + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + menu: { + id: MenuId.ChatCommandCenter, + group: 'd_inlineChat', + order: 10, + when: CTX_INLINE_CHAT_HAS_AGENT2 + } + }); + } + + override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { + const inlineChatSessions = accessor.get(IInlineChatSessionService); + if (!editor.hasModel()) { + return; + } + const textModel = editor.getModel(); + await inlineChatSessions.createSession2(editor, textModel.uri, CancellationToken.None); + InlineChatController2.get(editor)?.markActiveController(); + } +} + +abstract class AbstractInlineChatAction extends EditorAction2 { + + static readonly category = localize2('cat', "Inline Chat"); + + constructor(desc: IAction2Options) { + super({ + ...desc, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, desc.precondition) + }); + } + + override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { + const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + + let ctrl = InlineChatController2.get(editor); + if (!ctrl) { + const { activeTextEditorControl } = editorService; + if (isCodeEditor(activeTextEditorControl)) { + editor = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + editor = activeTextEditorControl.getModifiedEditor(); + } + ctrl = InlineChatController2.get(editor); + } + + if (!ctrl) { + logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); + return; + } + + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + if (!ctrl) { + for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { + if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { + if (diffEditor instanceof EmbeddedDiffEditorWidget) { + this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); + } + } + } + return; + } + this.runInlineChatCommand(accessor, ctrl, editor, ..._args); + } + + abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void; +} + +export class StopSessionAction2 extends AbstractInlineChatAction { + constructor() { + super({ + id: 'inlineChat2.stop', + title: localize2('stop', "Stop"), + f1: true, + precondition: ContextKeyExpr.and(CTX_HAS_SESSION.isEqualTo('empty'), CTX_INLINE_CHAT_VISIBLE), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] + }, + }); + } + + runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void { + const inlineChatSessions = accessor.get(IInlineChatSessionService); + if (!editor.hasModel()) { + return; + } + const textModel = editor.getModel(); + inlineChatSessions.getSession2(textModel.uri)?.dispose(); + } +} + +class RevealWidget extends AbstractInlineChatAction { + constructor() { + super({ + id: 'inlineChat2.reveal', + title: localize2('reveal', "Toggle Inline Chat"), + f1: true, + icon: Codicon.copilot, + precondition: ContextKeyExpr.and( + CTX_HAS_SESSION, + ), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + menu: { + id: MenuId.ChatEditingEditorContent, + when: ContextKeyExpr.and( + CTX_HAS_SESSION, + ctxIsGlobalEditingSession.negate(), + ), + group: 'navigate', + order: 4, + } + }); + } + + runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController2, _editor: ICodeEditor): void { + ctrl.toggleWidgetUntilNextRequest(); + ctrl.markActiveController(); + } +} + +registerAction2(RevealWidget); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts deleted file mode 100644 index ebe92f44f05..00000000000 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts +++ /dev/null @@ -1,16 +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 { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Session } from './inlineChatSession.js'; - - -export const IInlineChatSavingService = createDecorator('IInlineChatSavingService '); - -export interface IInlineChatSavingService { - _serviceBrand: undefined; - - markChanged(session: Session): void; - -} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts deleted file mode 100644 index 53ae97f2806..00000000000 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts +++ /dev/null @@ -1,197 +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 { Queue } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { DisposableStore, MutableDisposable, combinedDisposable, dispose } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../common/editor.js'; -import { Session } from './inlineChatSession.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; -import { InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; -import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { CellUri } from '../../notebook/common/notebookCommon.js'; -import { IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Event } from '../../../../base/common/event.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { CancellationError } from '../../../../base/common/errors.js'; - -interface SessionData { - readonly resourceUri: URI; - readonly dispose: () => void; - readonly session: Session; - readonly groupCandidate: IEditorGroup; -} - -// TODO@jrieken this duplicates a config key -const key = 'chat.editing.alwaysSaveWithGeneratedChanges'; - -export class InlineChatSavingServiceImpl implements IInlineChatSavingService { - - declare readonly _serviceBrand: undefined; - - private readonly _store = new DisposableStore(); - private readonly _saveParticipant = this._store.add(new MutableDisposable()); - private readonly _sessionData = new Map(); - - constructor( - @IFilesConfigurationService private readonly _fileConfigService: IFilesConfigurationService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInlineChatSessionService _inlineChatSessionService: IInlineChatSessionService, - @IConfigurationService private readonly _configService: IConfigurationService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, - @IDialogService private readonly _dialogService: IDialogService, - @ILabelService private readonly _labelService: ILabelService, - ) { - this._store.add(Event.any(_inlineChatSessionService.onDidEndSession, _inlineChatSessionService.onDidStashSession)(e => { - this._sessionData.get(e.session)?.dispose(); - })); - - this._store.add(_configService.onDidChangeConfiguration(e => { - if (!e.affectsConfiguration(key) && !e.affectsConfiguration(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave)) { - return; - } - if (this._isDisabled()) { - dispose(this._sessionData.values()); - this._sessionData.clear(); - } - })); - } - - dispose(): void { - this._store.dispose(); - dispose(this._sessionData.values()); - } - - markChanged(session: Session): void { - - if (this._isDisabled()) { - return; - } - - if (!this._sessionData.has(session)) { - - let uri = session.targetUri; - - // notebooks: use the notebook-uri because saving happens on the notebook-level - if (uri.scheme === Schemas.vscodeNotebookCell) { - const data = CellUri.parse(uri); - if (!data) { - return; - } - uri = data?.notebook; - } - - if (this._sessionData.size === 0) { - this._installSaveParticpant(); - } - - const saveConfigOverride = this._fileConfigService.disableAutoSave(uri); - this._sessionData.set(session, { - resourceUri: uri, - groupCandidate: this._editorGroupService.activeGroup, - session, - dispose: () => { - saveConfigOverride.dispose(); - this._sessionData.delete(session); - if (this._sessionData.size === 0) { - this._saveParticipant.clear(); - } - } - }); - } - } - - private _installSaveParticpant(): void { - - const queue = new Queue(); - - const d1 = this._textFileService.files.addSaveParticipant({ - participate: (model, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? model.textEditorModel?.uri, ctx.reason, progress, token)); - } - }); - const d2 = this._workingCopyFileService.addSaveParticipant({ - participate: (workingCopy, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? workingCopy.resource, ctx.reason, progress, token)); - } - }); - this._saveParticipant.value = combinedDisposable(d1, d2, queue); - } - - private async _participate(uri: URI | undefined, reason: SaveReason, progress: IProgress, token: CancellationToken): Promise { - - - if (reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - if (this._isDisabled()) { - // disabled - return; - } - - const sessions = new Map(); - for (const [session, data] of this._sessionData) { - if (uri?.toString() === data.resourceUri.toString()) { - sessions.set(session, data); - } - } - - if (sessions.size === 0) { - return; - } - - let message: string; - - if (sessions.size === 1) { - const session = Iterable.first(sessions.values())!.session; - const agentName = session.agent.fullName; - const filelabel = this._labelService.getUriBasenameLabel(session.textModelN.uri); - - message = localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel); - } else { - const labels = Array.from(Iterable.map(sessions.values(), i => this._labelService.getUriBasenameLabel(i.session.textModelN.uri))); - message = localize('message.2', "Do you want to save the changes inline chat made in {0}?", labels.join(', ')); - } - - const result = await this._dialogService.confirm({ - message, - detail: localize('detail', "AI-generated changes may be incorrect and should be reviewed before saving."), - primaryButton: localize('save', "Save"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - if (!result.confirmed) { - // cancel the save - throw new CancellationError(); - } - - if (result.checkboxChecked) { - // remember choice - this._configService.updateValue(key, true); - } - } - - private _isDisabled() { - return this._configService.getValue(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave) === true || this._configService.getValue(key); - } -} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 83d2d451dec..c153bb89559 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -6,7 +6,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { EditMode, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; @@ -36,7 +36,6 @@ export type TelemetryData = { finishedByEdit: boolean; startTime: string; endTime: string; - editMode: string; acceptedHunks: number; discardedHunks: number; responseTypes: string; @@ -53,7 +52,6 @@ export type TelemetryDataClassification = { finishedByEdit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits cause the session to terminate' }; startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' }; endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' }; - editMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What edit mode was choosen: live, livePreview, preview' }; acceptedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of accepted hunks' }; discardedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of discarded hunks' }; responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; @@ -125,7 +123,6 @@ export class Session { private readonly _versionByRequest = new Map(); constructor( - readonly editMode: EditMode, readonly headless: boolean, /** * The URI of the document which is being EditorEdit @@ -154,7 +151,6 @@ export class Session { finishedByEdit: false, rounds: '', undos: '', - editMode, unstashed: 0, acceptedHunks: 0, discardedHunks: 0, diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 233e79d76e3..a6e06ad4b36 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -7,10 +7,12 @@ import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { Position } from '../../../../editor/common/core/position.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IValidEditOperation } from '../../../../editor/common/model.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { EditMode } from '../common/inlineChat.js'; +import { IChatEditingSession } from '../../chat/common/chatEditingService.js'; +import { IChatModel } from '../../chat/common/chatModel.js'; import { Session, StashedSession } from './inlineChatSession.js'; export interface ISessionKeyComputer { @@ -28,6 +30,14 @@ export interface IInlineChatSessionEndEvent extends IInlineChatSessionEvent { readonly endedByExternalCause: boolean; } +export interface IInlineChatSession2 { + readonly initialPosition: Position; + readonly uri: URI; + readonly chatModel: IChatModel; + readonly editingSession: IChatEditingSession; + dispose(): void; +} + export interface IInlineChatSessionService { _serviceBrand: undefined; @@ -36,7 +46,7 @@ export interface IInlineChatSessionService { onDidStashSession: Event; onDidEndSession: Event; - createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise; + createSession(editor: IActiveCodeEditor, options: { wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise; moveSession(session: Session, newEditor: ICodeEditor): void; @@ -51,4 +61,9 @@ export interface IInlineChatSessionService { registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable; dispose(): void; + + + createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; + getSession2(uri: URI): IInlineChatSession2 | undefined; + onDidChangeSessions: Event; } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 758764720e8..b502148e9a4 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -22,14 +22,18 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js'; import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; import { IChatService } from '../../chat/common/chatService.js'; -import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_POSSIBLE, EditMode } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; -import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; +import { IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; +import { assertType } from '../../../../base/common/types.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { ResourceMap } from '../../../../base/common/map.js'; type SessionData = { @@ -79,7 +83,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @ITextFileService private readonly _textFileService: ITextFileService, @ILanguageService private readonly _languageService: ILanguageService, @IChatService private readonly _chatService: IChatService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, ) { } dispose() { @@ -88,7 +93,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._sessions.clear(); } - async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise { + async createSession(editor: IActiveCodeEditor, options: { headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise { const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); @@ -197,7 +202,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } const session = new Session( - options.editMode, options.headless ?? false, targetUri, textModel0, @@ -312,6 +316,63 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._keyComputers.set(scheme, value); return toDisposable(() => this._keyComputers.delete(scheme)); } + + // ---- NEW + + private readonly _sessions2 = new ResourceMap(); + + private readonly _onDidChangeSessions = this._store.add(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + + + async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise { + + assertType(editor.hasModel()); + + if (this._sessions2.has(uri)) { + throw new Error('Session already exists'); + } + + this._onWillStartSession.fire(editor as IActiveCodeEditor); + + const chatModel = this._chatService.startSession(ChatAgentLocation.EditingSession, token); + + const editingSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); + editingSession.addFileToWorkingSet(uri); + + const store = new DisposableStore(); + store.add(toDisposable(() => { + editingSession.reject(); + this._sessions2.delete(uri); + this._onDidChangeSessions.fire(this); + })); + store.add(editingSession); + store.add(chatModel); + + store.add(autorun(r => { + const entry = editingSession.readEntry(uri, r); + const state = entry?.state.read(r); + if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { + // self terminate + store.dispose(); + } + })); + + const result: IInlineChatSession2 = { + uri, + initialPosition: editor.getPosition().delta(-1), + chatModel, + editingSession, + dispose: store.dispose.bind(store) + }; + this._sessions2.set(uri, result); + this._onDidChangeSessions.fire(this); + return result; + } + + getSession2(uri: URI): IInlineChatSession2 | undefined { + return this._sessions2.get(uri); + } } export class InlineChatEnabler { @@ -319,6 +380,7 @@ export class InlineChatEnabler { static Id = 'inlineChat.enabler'; private readonly _ctxHasProvider: IContextKey; + private readonly _ctxHasProvider2: IContextKey; private readonly _ctxPossible: IContextKey; private readonly _store = new DisposableStore(); @@ -329,11 +391,21 @@ export class InlineChatEnabler { @IEditorService editorService: IEditorService, ) { this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); + this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); const updateAgent = () => { - const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); - this._ctxHasProvider.set(hasEditorAgent); + const agent = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); + if (agent?.locations.length === 1) { + this._ctxHasProvider.set(true); + this._ctxHasProvider2.reset(); + } else if (agent?.locations.includes(ChatAgentLocation.EditingSession)) { + this._ctxHasProvider.reset(); + this._ctxHasProvider2.set(true); + } else { + this._ctxHasProvider.reset(); + this._ctxHasProvider2.reset(); + } }; this._store.add(chatAgentService.onDidChangeAgents(updateAgent)); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 4d8b814e0df..1c19f39cd02 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { WindowIntervalTimer } from '../../../../base/browser/dom.js'; -import { coalesceInPlace } from '../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -27,9 +26,8 @@ import { SaveReason } from '../../../common/editor.js'; import { countWords } from '../../chat/common/chatWordCounter.js'; import { HunkInformation, Session, HunkState } from './inlineChatSession.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../common/inlineChat.js'; +import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../common/inlineChat.js'; import { assertType } from '../../../../base/common/types.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { performAsyncTextEdit, asProgressiveEdit } from './utils.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -57,183 +55,7 @@ export const enum HunkAction { ToggleDiff } -export abstract class EditModeStrategy { - - protected static _decoBlock = ModelDecorationOptions.register({ - description: 'inline-chat', - showIfCollapsed: false, - isWholeLine: true, - }); - - protected readonly _store = new DisposableStore(); - protected readonly _onDidAccept = this._store.add(new Emitter()); - protected readonly _onDidDiscard = this._store.add(new Emitter()); - - - readonly onDidAccept: Event = this._onDidAccept.event; - readonly onDidDiscard: Event = this._onDidDiscard.event; - - constructor( - protected readonly _session: Session, - protected readonly _editor: ICodeEditor, - protected readonly _zone: InlineChatZoneWidget, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInstantiationService protected readonly _instaService: IInstantiationService, - ) { } - - dispose(): void { - this._store.dispose(); - } - - performHunkAction(_hunk: HunkInformation | undefined, action: HunkAction) { - if (action === HunkAction.Accept) { - this._onDidAccept.fire(); - } else if (action === HunkAction.Discard) { - this._onDidDiscard.fire(); - } - } - - protected async _doApplyChanges(ignoreLocal: boolean): Promise { - - const untitledModels: IUntitledTextEditorModel[] = []; - - const editor = this._instaService.createInstance(DefaultChatTextEditor); - - - for (const request of this._session.chatModel.getRequests()) { - - if (!request.response?.response) { - continue; - } - - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup') { - continue; - } - if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { - continue; - } - - await editor.apply(request.response, item, undefined); - - if (item.uri.scheme === Schemas.untitled) { - const untitled = this._textFileService.untitled.get(item.uri); - if (untitled) { - untitledModels.push(untitled); - } - } - } - } - - for (const untitledModel of untitledModels) { - if (!untitledModel.isDisposed()) { - await untitledModel.resolve(); - await untitledModel.save({ reason: SaveReason.EXPLICIT }); - } - } - } - - abstract apply(): Promise; - - cancel() { - return this._session.hunkData.discardAll(); - } - - - - abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions, undoStopBefore: boolean): Promise; - - abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; - - abstract renderChanges(): Promise; - - abstract hasFocus(): boolean; - - getWholeRangeDecoration(): IModelDeltaDecoration[] { - const ranges = [this._session.wholeRange.value]; - const newDecorations = ranges.map(range => range.isEmpty() ? undefined : ({ range, options: EditModeStrategy._decoBlock })); - coalesceInPlace(newDecorations); - return newDecorations; - } -} - -export class PreviewStrategy extends EditModeStrategy { - - private readonly _ctxDocumentChanged: IContextKey; - - constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, - @IModelService modelService: IModelService, - @IContextKeyService contextKeyService: IContextKeyService, - @ITextFileService textFileService: ITextFileService, - @IInstantiationService instaService: IInstantiationService - ) { - super(session, editor, zone, textFileService, instaService); - - this._ctxDocumentChanged = CTX_INLINE_CHAT_DOCUMENT_CHANGED.bindTo(contextKeyService); - - const baseModel = modelService.getModel(session.targetUri)!; - Event.debounce(baseModel.onDidChangeContent.bind(baseModel), () => { }, 350)(_ => { - if (!baseModel.isDisposed() && !session.textModel0.isDisposed()) { - this._ctxDocumentChanged.set(session.hasChangedText); - } - }, undefined, this._store); - } - - override dispose(): void { - this._ctxDocumentChanged.reset(); - super.dispose(); - } - - override async apply() { - await super._doApplyChanges(false); - } - - override async makeChanges(): Promise { - } - - override async makeProgressiveChanges(): Promise { - } - - override async renderChanges(): Promise { } - - hasFocus(): boolean { - return this._zone.widget.hasFocus(); - } -} - - -export interface ProgressingEditsOptions { - duration: number; - token: CancellationToken; -} - - - -type HunkDisplayData = { - - decorationIds: string[]; - - diffViewZoneId: string | undefined; - diffViewZone: IViewZone; - - lensActionsViewZoneIds?: string[]; - - distance: number; - position: Position; - acceptHunk: () => void; - discardHunk: () => void; - toggleDiff?: () => any; - remove(): void; - move: (next: boolean) => void; - - hunk: HunkInformation; -}; - - -export class LiveStrategy extends EditModeStrategy { +export class LiveStrategy { private readonly _decoInsertedText = ModelDecorationOptions.register({ description: 'inline-modified-line', @@ -255,17 +77,23 @@ export class LiveStrategy extends EditModeStrategy { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }); + protected readonly _store = new DisposableStore(); + protected readonly _onDidAccept = this._store.add(new Emitter()); + protected readonly _onDidDiscard = this._store.add(new Emitter()); private readonly _ctxCurrentChangeHasDiff: IContextKey; private readonly _ctxCurrentChangeShowsDiff: IContextKey; - private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; private readonly _lensActionsFactory: ConflictActionsFactory; private _editCount: number = 0; + private readonly _hunkData = new Map(); + + readonly onDidAccept: Event = this._onDidAccept.event; + readonly onDidDiscard: Event = this._onDidDiscard.event; constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, + protected readonly _session: Session, + protected readonly _editor: ICodeEditor, + protected readonly _zone: InlineChatZoneWidget, private readonly _showOverlayToolbar: boolean, @IContextKeyService contextKeyService: IContextKeyService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, @@ -273,21 +101,19 @@ export class LiveStrategy extends EditModeStrategy { @IConfigurationService private readonly _configService: IConfigurationService, @IMenuService private readonly _menuService: IMenuService, @IContextKeyService private readonly _contextService: IContextKeyService, - @ITextFileService textFileService: ITextFileService, - @IInstantiationService instaService: IInstantiationService + @ITextFileService private readonly _textFileService: ITextFileService, + @IInstantiationService protected readonly _instaService: IInstantiationService ) { - super(session, editor, zone, textFileService, instaService); this._ctxCurrentChangeHasDiff = CTX_INLINE_CHAT_CHANGE_HAS_DIFF.bindTo(contextKeyService); this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); this._lensActionsFactory = this._store.add(new ConflictActionsFactory(this._editor)); - } - override dispose(): void { + dispose(): void { this._resetDiff(); - super.dispose(); + this._store.dispose(); } private _resetDiff(): void { @@ -297,29 +123,29 @@ export class LiveStrategy extends EditModeStrategy { this._progressiveEditingDecorations.clear(); - for (const data of this._hunkDisplayData.values()) { + for (const data of this._hunkData.values()) { data.remove(); } } - override async apply() { + async apply() { this._resetDiff(); if (this._editCount > 0) { this._editor.pushUndoStop(); } - await super._doApplyChanges(true); + await this._doApplyChanges(true); } - override cancel() { + cancel() { this._resetDiff(); - return super.cancel(); + return this._session.hunkData.discardAll(); } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { + async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore); } - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { + async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { // add decorations once per line that got edited const progress = new Progress(edits => { @@ -373,7 +199,7 @@ export class LiveStrategy extends EditModeStrategy { } } - override performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { + performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { const displayData = this._findDisplayData(hunk); if (!displayData) { @@ -404,14 +230,14 @@ export class LiveStrategy extends EditModeStrategy { let result: HunkDisplayData | undefined; if (hunkInfo) { // use context hunk (from tool/buttonbar) - result = this._hunkDisplayData.get(hunkInfo); + result = this._hunkData.get(hunkInfo); } if (!result && this._zone.position) { // find nearest from zone position const zoneLine = this._zone.position.lineNumber; let distance: number = Number.MAX_SAFE_INTEGER; - for (const candidate of this._hunkDisplayData.values()) { + for (const candidate of this._hunkData.values()) { if (candidate.hunk.getState() !== HunkState.Pending) { continue; } @@ -433,14 +259,12 @@ export class LiveStrategy extends EditModeStrategy { if (!result) { // fallback: first hunk that is pending - result = Iterable.first(Iterable.filter(this._hunkDisplayData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); + result = Iterable.first(Iterable.filter(this._hunkData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); } return result; } - private readonly _hunkDisplayData = new Map(); - - override async renderChanges() { + async renderChanges() { this._progressiveEditingDecorations.clear(); @@ -450,7 +274,7 @@ export class LiveStrategy extends EditModeStrategy { changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - const keysNow = new Set(this._hunkDisplayData.keys()); + const keysNow = new Set(this._hunkData.keys()); widgetData = undefined; for (const hunkData of this._session.hunkData.getInfo()) { @@ -458,7 +282,7 @@ export class LiveStrategy extends EditModeStrategy { keysNow.delete(hunkData); const hunkRanges = hunkData.getRangesN(); - let data = this._hunkDisplayData.get(hunkData); + let data = this._hunkData.get(hunkData); if (!data) { // first time -> create decoration const decorationIds: string[] = []; @@ -570,7 +394,7 @@ export class LiveStrategy extends EditModeStrategy { decorationsAccessor.removeDecoration(decorationId); } if (data.diffViewZoneId) { - viewZoneAccessor.removeZone(data.diffViewZoneId); + viewZoneAccessor.removeZone(data.diffViewZoneId!); } data.decorationIds = []; data.diffViewZoneId = undefined; @@ -583,11 +407,11 @@ export class LiveStrategy extends EditModeStrategy { }; const move = (next: boolean) => { - const keys = Array.from(this._hunkDisplayData.keys()); + const keys = Array.from(this._hunkData.keys()); const idx = keys.indexOf(hunkData); const nextIdx = (idx + (next ? 1 : -1) + keys.length) % keys.length; if (nextIdx !== idx) { - const nextData = this._hunkDisplayData.get(keys[nextIdx])!; + const nextData = this._hunkData.get(keys[nextIdx])!; this._zone.updatePositionAndHeight(nextData?.position); renderHunks(); } @@ -613,7 +437,7 @@ export class LiveStrategy extends EditModeStrategy { move, }; - this._hunkDisplayData.set(hunkData, data); + this._hunkData.set(hunkData, data); } else if (hunkData.getState() !== HunkState.Pending) { data.remove(); @@ -634,9 +458,9 @@ export class LiveStrategy extends EditModeStrategy { } for (const key of keysNow) { - const data = this._hunkDisplayData.get(key); + const data = this._hunkData.get(key); if (data) { - this._hunkDisplayData.delete(key); + this._hunkData.delete(key); data.remove(); } } @@ -652,7 +476,7 @@ export class LiveStrategy extends EditModeStrategy { this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); - } else if (this._hunkDisplayData.size > 0) { + } else if (this._hunkData.size > 0) { // everything accepted or rejected let oneAccepted = false; for (const hunkData of this._session.hunkData.getInfo()) { @@ -674,16 +498,77 @@ export class LiveStrategy extends EditModeStrategy { return renderHunks()?.position; } - hasFocus(): boolean { - return this._zone.widget.hasFocus(); - } - - override getWholeRangeDecoration(): IModelDeltaDecoration[] { + getWholeRangeDecoration(): IModelDeltaDecoration[] { // don't render the blue in live mode return []; } + + private async _doApplyChanges(ignoreLocal: boolean): Promise { + + const untitledModels: IUntitledTextEditorModel[] = []; + + const editor = this._instaService.createInstance(DefaultChatTextEditor); + + + for (const request of this._session.chatModel.getRequests()) { + + if (!request.response?.response) { + continue; + } + + for (const item of request.response.response.value) { + if (item.kind !== 'textEditGroup') { + continue; + } + if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { + continue; + } + + await editor.apply(request.response, item, undefined); + + if (item.uri.scheme === Schemas.untitled) { + const untitled = this._textFileService.untitled.get(item.uri); + if (untitled) { + untitledModels.push(untitled); + } + } + } + } + + for (const untitledModel of untitledModels) { + if (!untitledModel.isDisposed()) { + await untitledModel.resolve(); + await untitledModel.save({ reason: SaveReason.EXPLICIT }); + } + } + } +} + +export interface ProgressingEditsOptions { + duration: number; + token: CancellationToken; } +type HunkDisplayData = { + + decorationIds: string[]; + + diffViewZoneId: string | undefined; + diffViewZone: IViewZone; + + lensActionsViewZoneIds?: string[]; + + distance: number; + position: Position; + acceptHunk: () => void; + discardHunk: () => void; + toggleDiff?: () => any; + remove(): void; + move: (next: boolean) => void; + + hunk: HunkInformation; +}; + function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { editor.changeDecorations(decorationsAccessor => { editor.changeViewZones(viewZoneAccessor => { diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index ba1d884af6c..138b51910c4 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -16,13 +16,13 @@ import { Range } from '../../../../editor/common/core/range.js'; import { ScrollType } from '../../../../editor/common/editorCommon.js'; import { IOptions, ZoneWidget } from '../../../../editor/contrib/zoneWidget/browser/zoneWidget.js'; import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { isResponseVM } from '../../chat/common/chatViewModel.js'; -import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; export class InlineChatZoneWidget extends ZoneWidget { @@ -48,11 +48,11 @@ export class InlineChatZoneWidget extends ZoneWidget { constructor( location: IChatWidgetLocationOptions, + options: IChatWidgetViewOptions | undefined, editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService, ) { super(editor, InlineChatZoneWidget._options); @@ -82,15 +82,15 @@ export class InlineChatZoneWidget extends ZoneWidget { menus: { telemetrySource: 'interactiveEditorWidget-toolbar', }, + ...options, rendererOptions: { renderTextEditsAsSummary: (uri) => { - // render edits as summary only when using Live mode and when - // dealing with the current file in the editor - return isEqual(uri, editor.getModel()?.uri) - && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; + // render when dealing with the current file in the editor + return isEqual(uri, editor.getModel()?.uri); }, renderDetectedCommandsWithRequest: true, - } + ...options?.rendererOptions + }, } }); this._disposables.add(this.widget); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 7ecb62c8cbc..01c83a45b7b 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -76,6 +76,10 @@ padding: 4px 0 0 0; } +.monaco-workbench .inline-chat-2 .inline-chat .chat-widget .interactive-session .interactive-input-part { + padding: 8px 0 0 0; +} + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { margin-bottom: 1px; } diff --git a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index b541cb0e621..21dcf39b8d6 100644 --- a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -13,9 +13,7 @@ import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, // settings export const enum InlineChatConfigKeys { - Mode = 'inlineChat.mode', FinishOnType = 'inlineChat.finishOnType', - AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', HoldToSpeech = 'inlineChat.holdToSpeech', AccessibleDiffView = 'inlineChat.accessibleDiffView', @@ -23,24 +21,9 @@ export const enum InlineChatConfigKeys { LineNLHint = 'inlineChat.lineNaturalLanguageHint' } -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} - Registry.as(Extensions.Configuration).registerConfiguration({ id: 'editor', properties: { - [InlineChatConfigKeys.Mode]: { - description: localize('mode', "Configure if changes crafted with inline chat are applied directly to the document or are previewed first."), - default: EditMode.Live, - type: 'string', - enum: [EditMode.Live, EditMode.Preview], - markdownEnumDescriptions: [ - localize('mode.live', "Changes are applied directly to the document, can be highlighted via inline diffs, and accepted/discarded by hunks. Ending a session will keep the changes."), - localize('mode.preview', "Changes are previewed only and need to be accepted via the apply button. Ending a session will discard the changes."), - ] - }, [InlineChatConfigKeys.FinishOnType]: { description: localize('finishOnType', "Whether to finish an inline chat session when typing outside of changed regions."), default: false, @@ -91,6 +74,7 @@ export const enum InlineChatResponseType { export const CTX_INLINE_CHAT_POSSIBLE = new RawContextKey('inlineChatPossible', false, localize('inlineChatHasPossible', "Whether a provider for inline chat exists and whether an editor for inline chat is open")); export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); +export const CTX_INLINE_CHAT_HAS_AGENT2 = new RawContextKey('inlineChatHasEditsAgent', false, localize('inlineChatHasEditsAgent', "Whether an agent for inliine for interactive editors exists")); export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); export const CTX_INLINE_CHAT_EDITING = new RawContextKey('inlineChatEditing', true, localize('inlineChatEditing', "Whether the user is currently editing or generating code in the inline chat")); @@ -103,10 +87,8 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inli export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); -export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); -export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); @@ -140,7 +122,7 @@ export const inlineChatInputBackground = registerColor('inlineChatInput.backgrou export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', transparent(diffInserted, .5), localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); -export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); +export const minimapInlineChatDiffInserted = registerColor('editorMinimap.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorMinimap.inlineChatInserted', 'Minimap marker color for inline chat inserted content.')); export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', transparent(diffRemoved, .5), localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); diff --git a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 75db4dbb7f1..d8d8ce7467c 100644 --- a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -34,8 +34,7 @@ import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../. import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; import { InlineChatController, State } from '../../browser/inlineChatController.js'; -import { Session } from '../../browser/inlineChatSession.js'; -import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; +import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; @@ -61,7 +60,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { assertType } from '../../../../../base/common/types.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; import { TestWorkerService } from './testWorkerService.js'; @@ -70,7 +68,7 @@ import { IChatEditingService, IChatEditingSession } from '../../../chat/common/c import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js'; -import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { constObservable, IObservable, observableValue } from '../../../../../base/common/observable.js'; import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; @@ -166,11 +164,7 @@ suite('InlineChatController', function () { [ICommandService, new SyncDescriptor(TestCommandService)], [IChatEditingService, new class extends mock() { override currentEditingSessionObs: IObservable = observableValue(this, null); - }], - [IInlineChatSavingService, new class extends mock() { - override markChanged(session: Session): void { - // noop - } + override editingSessionsObs: IObservable = constObservable([]); }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { @@ -207,7 +201,7 @@ suite('InlineChatController', function () { configurationService = instaService.get(IConfigurationService) as TestConfigurationService; configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); + configurationService.setUserConfiguration('editor', {}); contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; @@ -404,7 +398,6 @@ suite('InlineChatController', function () { test.skip('UI is streaming edits minutes after the response is finished #3345', async function () { - configurationService.setUserConfiguration(InlineChatConfigKeys.Mode, EditMode.Live); return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { @@ -845,7 +838,7 @@ suite('InlineChatController', function () { model.setValue(''); - const newSession = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(newSession); await chatService.sendRequest(newSession.chatModel.sessionId, 'Existing', { location: ChatAgentLocation.Editor }); diff --git a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 665206f55b0..cfa95c16467 100644 --- a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -26,11 +26,9 @@ import { IViewDescriptorService } from '../../../../common/views.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IChatAccessibilityService, IChatWidgetService } from '../../../chat/browser/chat.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService.js'; -import { HunkState, Session } from '../../browser/inlineChatSession.js'; +import { HunkState } from '../../browser/inlineChatSession.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; -import { EditMode } from '../../common/inlineChat.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { assertType } from '../../../../../base/common/types.js'; @@ -62,6 +60,8 @@ import { ILanguageModelToolsService } from '../../../chat/common/languageModelTo import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; import { IChatRequestModel } from '../../../chat/common/chatModel.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; +import { IObservable, observableValue, constObservable } from '../../../../../base/common/observable.js'; +import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js'; suite('InlineChatSession', function () { @@ -96,11 +96,6 @@ suite('InlineChatSession', function () { [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], [ILanguageModelToolsService, new MockLanguageModelToolsService()], - [IInlineChatSavingService, new class extends mock() { - override markChanged(session: Session): void { - // noop - } - }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { return { @@ -110,6 +105,10 @@ suite('InlineChatSession', function () { }; } }], + [IChatEditingService, new class extends mock() { + override currentEditingSessionObs: IObservable = observableValue(this, null); + override editingSessionsObs: IObservable = constObservable([]); + }], [IChatAccessibilityService, new class extends mock() { override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { } override acceptRequest(): number { return -1; } @@ -178,7 +177,7 @@ suite('InlineChatSession', function () { test('Create, release', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); inlineChatSessionService.releaseSession(session); }); @@ -187,7 +186,7 @@ suite('InlineChatSession', function () { const decorationCountThen = model.getAllDecorations().length; - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); assert.ok(session.textModelN === model); @@ -213,7 +212,7 @@ suite('InlineChatSession', function () { test('HunkData, accept', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -234,7 +233,7 @@ suite('InlineChatSession', function () { test('HunkData, reject', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -257,7 +256,7 @@ suite('InlineChatSession', function () { model.setValue('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven\ntwelwe\nthirteen\nfourteen\nfifteen\nsixteen\nseventeen\neighteen\nnineteen\n'); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); assert.ok(session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); @@ -306,7 +305,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); @@ -323,7 +322,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three', 'four', 'five']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); @@ -348,7 +347,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); @@ -364,7 +363,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); @@ -387,7 +386,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three', 'four', 'five']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); @@ -414,7 +413,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three', 'four', 'five']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); @@ -444,7 +443,7 @@ suite('InlineChatSession', function () { test('HunkData, accept, discardAll', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -466,7 +465,7 @@ suite('InlineChatSession', function () { test('HunkData, discardAll return undo edits', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -509,7 +508,7 @@ suite('InlineChatSession', function () { }`; model.setValue(origValue); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); const fakeRequest = new class extends mock() { @@ -543,7 +542,7 @@ suite('InlineChatSession', function () { if (n === 2) return 1; return fib(n - 1) + fib(n - 2); }`); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.replace(new Range(5, 1, 6, Number.MAX_SAFE_INTEGER), ` diff --git a/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts b/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts index b62de00c9dc..f563593e70a 100644 --- a/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts +++ b/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts @@ -9,8 +9,6 @@ import { autorunWithStore } from '../../../../base/common/observable.js'; import Severity from '../../../../base/common/severity.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ILanguageStatusService } from '../../../services/languageStatus/common/languageStatusService.js'; export class InlineCompletionLanguageStatusBarContribution extends Disposable { @@ -18,22 +16,15 @@ export class InlineCompletionLanguageStatusBarContribution extends Disposable { public static Id = 'vs.editor.contrib.inlineCompletionLanguageStatusBarContribution'; - // TODO always enable this! - private readonly _inlineCompletionInlineEdits = observableConfigValue('editor.inlineSuggest.experimentalInlineEditsEnabled', false, this._configurationService); - constructor( private readonly _editor: ICodeEditor, @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, - @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); const c = InlineCompletionsController.get(this._editor); this._register(autorunWithStore((reader, store) => { - // TODO always enable this feature! - if (!this._inlineCompletionInlineEdits.read(reader)) { return; } - const model = c?.model.read(reader); if (!model) { return; } diff --git a/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts b/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts index b04c5c16c5a..142d518e68b 100644 --- a/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts +++ b/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts @@ -102,6 +102,8 @@ class LanguageStatus { private _dedicatedEntries = new Map(); private readonly _renderDisposables = new DisposableStore(); + private readonly _combinedEntryTooltip = document.createElement('div'); + constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, @IStatusbarService private readonly _statusBarService: IStatusbarService, @@ -207,10 +209,9 @@ class LanguageStatus { let isOneBusy = false; const ariaLabels: string[] = []; - const element = document.createElement('div'); for (const status of model.combined) { const isPinned = model.dedicated.includes(status); - element.appendChild(this._renderStatus(status, showSeverity, isPinned, this._renderDisposables)); + this._renderStatus(this._combinedEntryTooltip, status, showSeverity, isPinned, this._renderDisposables); ariaLabels.push(LanguageStatus._accessibilityInformation(status).label); isOneBusy = isOneBusy || (!isPinned && status.busy); // unpinned items contribute to the busy-indicator of the composite status item } @@ -218,7 +219,7 @@ class LanguageStatus { const props: IStatusbarEntry = { name: localize('langStatus.name', "Editor Language Status"), ariaLabel: localize('langStatus.aria', "Editor Language Status: {0}", ariaLabels.join(', next: ')), - tooltip: element, + tooltip: this._combinedEntryTooltip, command: ShowTooltipCommand, text: isOneBusy ? '$(loading~spin)' : text, }; @@ -256,7 +257,7 @@ class LanguageStatus { const hoverTarget = targetWindow.document.querySelector('.monaco-workbench .context-view'); if (dom.isHTMLElement(hoverTarget)) { const observer = new MutationObserver(() => { - if (targetWindow.document.contains(element)) { + if (targetWindow.document.contains(this._combinedEntryTooltip)) { this._interactionCounter.increment(); observer.disconnect(); } @@ -284,11 +285,14 @@ class LanguageStatus { this._dedicatedEntries = newDedicatedEntries; } - private _renderStatus(status: ILanguageStatus, showSeverity: boolean, isPinned: boolean, store: DisposableStore): HTMLElement { + private _renderStatus(container: HTMLElement, status: ILanguageStatus, showSeverity: boolean, isPinned: boolean, store: DisposableStore): HTMLElement { const parent = document.createElement('div'); parent.classList.add('hover-language-status'); + container.appendChild(parent); + store.add(toDisposable(() => parent.remove())); + const severity = document.createElement('div'); severity.classList.add('severity', `sev${status.severity}`); severity.classList.toggle('show', showSeverity); diff --git a/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts b/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts index 1302428ebdf..4d6ba9f5e01 100644 --- a/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts +++ b/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts @@ -38,8 +38,6 @@ export interface IDefaultLogLevelsService { getDefaultLogLevel(extensionId?: string): Promise; setDefaultLogLevel(logLevel: LogLevel, extensionId?: string): Promise; - - migrateLogLevels(): void; } class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsService { @@ -148,17 +146,6 @@ class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsSer return !isUndefined(result.default) || result.extensions?.length ? result : undefined; } - async migrateLogLevels(): Promise { - const logLevels = await this._readLogLevelsFromArgv(); - const regex = /^([^.]+\..+):(.+)$/; - if (logLevels.some(extensionLogLevel => regex.test(extensionLogLevel))) { - const argvLogLevel = await this._parseLogLevelsFromArgv(); - if (argvLogLevel) { - await this._writeLogLevelsToArgv(argvLogLevel); - } - } - } - private async _readLogLevelsFromArgv(): Promise { try { const content = await this.fileService.readFile(this.environmentService.argvResource); diff --git a/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 4901650bec9..05b5f7a0329 100644 --- a/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -9,18 +9,13 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { SetLogLevelAction } from './logsActions.js'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../common/contributions.js'; -import { IFileService, whenProviderRegistered } from '../../../../platform/files/common/files.js'; -import { IOutputChannelRegistry, IOutputService, Extensions } from '../../../services/output/common/output.js'; -import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { CONTEXT_LOG_LEVEL, ILogService, ILoggerResource, ILoggerService, LogLevel, LogLevelToString, isLogLevel } from '../../../../platform/log/common/log.js'; +import { IOutputChannelRegistry, IOutputService, Extensions, isMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { CONTEXT_LOG_LEVEL, ILoggerResource, ILoggerService, LogLevel, LogLevelToString, isLogLevel } from '../../../../platform/log/common/log.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { windowLogId, showWindowLogActionId } from '../../../services/log/common/logConstants.js'; -import { createCancelablePromise, timeout } from '../../../../base/common/async.js'; -import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { CounterSet } from '../../../../base/common/map.js'; @@ -58,13 +53,10 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { private readonly contextKeys = new CounterSet(); private readonly outputChannelRegistry = Registry.as(Extensions.OutputChannels); - private readonly loggerDisposables = this._register(new DisposableMap()); constructor( - @ILogService private readonly logService: ILogService, @ILoggerService private readonly loggerService: ILoggerService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IFileService private readonly fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); @@ -142,56 +134,48 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } private registerLogChannel(logger: ILoggerResource): void { + if (logger.group) { + this.registerCompoundLogChannel(logger.group.id, logger.group.name, logger); + return; + } + const channel = this.outputChannelRegistry.getChannel(logger.id); - if (channel && this.uriIdentityService.extUri.isEqual(channel.file, logger.resource)) { + if (channel && isSingleSourceOutputChannelDescriptor(channel) && this.uriIdentityService.extUri.isEqual(channel.source.resource, logger.resource)) { return; } - const disposables = new DisposableStore(); - const promise = createCancelablePromise(async token => { - await whenProviderRegistered(logger.resource, this.fileService); - try { - await this.whenFileExists(logger.resource, 1, token); - const existingChannel = this.outputChannelRegistry.getChannel(logger.id); - const remoteLogger = existingChannel?.file?.scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.file) : undefined; - if (remoteLogger) { - this.deregisterLogChannel(remoteLogger); - } - const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; - const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; - const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; - this.outputChannelRegistry.registerChannel({ id, label, file: logger.resource, log: true, extensionId: logger.extensionId }); - disposables.add(toDisposable(() => this.outputChannelRegistry.removeChannel(id))); - if (remoteLogger) { - this.registerLogChannel(remoteLogger); - } - } catch (error) { - if (!isCancellationError(error)) { - this.logService.error('Error while registering log channel', logger.resource.toString(), getErrorMessage(error)); - } - } - }); - disposables.add(toDisposable(() => promise.cancel())); - this.loggerDisposables.set(logger.resource.toString(), disposables); - } - private deregisterLogChannel(logger: ILoggerResource): void { - this.loggerDisposables.deleteAndDispose(logger.resource.toString()); + const existingChannel = this.outputChannelRegistry.getChannel(logger.id); + const remoteLogger = existingChannel && isSingleSourceOutputChannelDescriptor(existingChannel) && existingChannel.source.resource.scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.source.resource) : undefined; + if (remoteLogger) { + this.deregisterLogChannel(remoteLogger); + } + const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; + const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; + const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; + this.outputChannelRegistry.registerChannel({ id, label, source: { resource: logger.resource }, log: true, extensionId: logger.extensionId }); } - private async whenFileExists(file: URI, trial: number, token: CancellationToken): Promise { - const exists = await this.fileService.exists(file); - if (exists) { - return; - } - if (token.isCancellationRequested) { - throw new CancellationError(); + private registerCompoundLogChannel(id: string, name: string, logger: ILoggerResource): void { + const channel = this.outputChannelRegistry.getChannel(id); + const source = { resource: logger.resource, name: logger.name ?? logger.id }; + if (channel) { + if (isMultiSourceOutputChannelDescriptor(channel) && !channel.source.some(({ resource }) => this.uriIdentityService.extUri.isEqual(resource, logger.resource))) { + this.outputChannelRegistry.updateChannelSources(id, [...channel.source, source]); + } + } else { + this.outputChannelRegistry.registerChannel({ id, label: name, log: true, source: [source] }); } - if (trial > 10) { - throw new Error(`Timed out while waiting for file to be created`); + } + + private deregisterLogChannel(logger: ILoggerResource): void { + if (logger.group) { + const channel = this.outputChannelRegistry.getChannel(logger.group.id); + if (channel && isMultiSourceOutputChannelDescriptor(channel)) { + this.outputChannelRegistry.updateChannelSources(logger.group.id, channel.source.filter(({ resource }) => !this.uriIdentityService.extUri.isEqual(resource, logger.resource))); + } + } else { + this.outputChannelRegistry.removeChannel(logger.id); } - this.logService.debug(`[Registering Log Channel] File does not exist. Waiting for 1s to retry.`, file.toString()); - await timeout(1000, token); - await this.whenFileExists(file, trial + 1, token); } private registerShowWindowLogAction(): void { @@ -212,13 +196,4 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } } -class LogLevelMigration implements IWorkbenchContribution { - constructor( - @IDefaultLogLevelsService defaultLogLevelsService: IDefaultLogLevelsService - ) { - defaultLogLevelsService.migrateLogLevels(); - } -} - Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Restored); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogLevelMigration, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/logs/common/logsActions.ts b/code/src/vs/workbench/contrib/logs/common/logsActions.ts index c28c6ca9140..23bf63df891 100644 --- a/code/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/code/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -12,15 +12,14 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { dirname, basename, isEqual } from '../../../../base/common/resources.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IOutputChannelDescriptor, IOutputService } from '../../../services/output/common/output.js'; -import { extensionTelemetryLogChannelId, telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; +import { IOutputChannelDescriptor, IOutputService, isMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; type LogLevelQuickPickItem = IQuickPickItem & { level: LogLevel }; -type LogChannelQuickPickItem = IQuickPickItem & { id: string; resource: URI; extensionId?: string }; +type LogChannelQuickPickItem = IQuickPickItem & { id: string; channel: IOutputChannelDescriptor }; export class SetLogLevelAction extends Action { @@ -52,11 +51,20 @@ export class SetLogLevelAction extends Action { const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { - if (!SetLogLevelAction.isLevelSettable(channel) || !channel.file) { + if (!this.outputService.canSetLogLevel(channel)) { continue; } - const channelLogLevel = this.loggerService.getLogLevel(channel.file) ?? logLevel; - const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.file, label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; + const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : []; + if (!sources.length) { + continue; + } + const channelLogLevel = sources.reduce((prev, curr) => Math.min(prev, this.loggerService.getLogLevel(curr.resource) ?? logLevel), logLevel); + const item: LogChannelQuickPickItem = { + id: channel.id, + label: channel.label, + description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, + channel + }; if (channel.extensionId) { extensionLogs.push(item); } else { @@ -96,15 +104,10 @@ export class SetLogLevelAction extends Action { }); } - static isLevelSettable(channel: IOutputChannelDescriptor): boolean { - return channel.log && channel.file !== undefined && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; - } - private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); - const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; - const currentLogLevel = this.loggerService.getLogLevel(logChannel.resource) ?? defaultLogLevel; - const entries = this.getLogLevelEntries(defaultLogLevel, currentLogLevel, !!logChannel.extensionId); + const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.channel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; + const entries = this.getLogLevelEntries(defaultLogLevel, this.outputService.getLogLevel(logChannel.channel) ?? defaultLogLevel, !!logChannel.channel.extensionId); return new Promise((resolve, reject) => { const disposables = new DisposableStore(); @@ -115,7 +118,7 @@ export class SetLogLevelAction extends Action { let selectedItem: LogLevelQuickPickItem | undefined; disposables.add(quickPick.onDidTriggerItemButton(e => { quickPick.hide(); - this.defaultLogLevelsService.setDefaultLogLevel((e.item).level, logChannel.extensionId); + this.defaultLogLevelsService.setDefaultLogLevel((e.item).level, logChannel.channel.extensionId); })); disposables.add(quickPick.onDidAccept(e => { selectedItem = quickPick.selectedItems[0] as LogLevelQuickPickItem; @@ -123,7 +126,7 @@ export class SetLogLevelAction extends Action { })); disposables.add(quickPick.onDidHide(() => { if (selectedItem) { - this.loggerService.setLogLevel(logChannel.resource, selectedItem.level); + this.outputService.setLogLevel(logChannel.channel, selectedItem.level); } disposables.dispose(); resolve(); diff --git a/code/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts b/code/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts deleted file mode 100644 index 3470efba634..00000000000 --- a/code/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts +++ /dev/null @@ -1,51 +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 { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import * as languages from '../../../../editor/common/languages.js'; - -CommandsRegistry.registerCommand( - '_executeMappedEditsProvider', - async ( - accessor: ServicesAccessor, - documentUri: URI, - codeBlocks: string[], - context: languages.MappedEditsContext - ): Promise => { - - const modelService = accessor.get(ITextModelService); - const langFeaturesService = accessor.get(ILanguageFeaturesService); - - const document = await modelService.createModelReference(documentUri); - - let result: languages.WorkspaceEdit | null = null; - - try { - const providers = langFeaturesService.mappedEditsProvider.ordered(document.object.textEditorModel); - - if (providers.length > 0) { - const mostRelevantProvider = providers[0]; - - const cancellationTokenSource = new CancellationTokenSource(); - - result = await mostRelevantProvider.provideMappedEdits( - document.object.textEditorModel, - codeBlocks, - context, - cancellationTokenSource.token - ); - } - } finally { - document.dispose(); - } - - return result; - } -); diff --git a/code/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/code/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index cdd3aac6b29..ec198f8ad63 100644 --- a/code/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/code/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -139,7 +139,7 @@ export class OpenScmGroupAction extends Action2 { constructor() { super({ id: '_workbench.openScmMultiDiffEditor', - title: localize2('viewChanges', 'View Changes'), + title: localize2('openChanges', 'Open Changes'), f1: false }); } diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index 7ac61fbcb30..f58810283e3 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -18,8 +18,7 @@ import { autorun, autorunWithStore, IObservable, ISettableObservable, observable import { isEqual } from '../../../../../../base/common/resources.js'; import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; import { INotebookDeletedCellDecorator } from './notebookCellDecorators.js'; -import { AcceptAction, RejectAction } from '../../../../chat/browser/chatEditorActions.js'; -import { navigationBearingFakeActionId } from '../../../../chat/browser/chatEditorOverlay.js'; +import { AcceptAction, navigationBearingFakeActionId, RejectAction } from '../../../../chat/browser/chatEditorActions.js'; export class NotebookChatActionsOverlayController extends Disposable { constructor( diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 8509900d4b0..74a3bc881b5 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -566,7 +566,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, - history: [], + history: new Set([]), inputBoxStyles: defaultInputBoxStyles, toggleStyles: defaultToggleStyles }, contextKeyService, false)); diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts new file mode 100644 index 00000000000..8c53b1cf111 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -0,0 +1,357 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; +import { onUnexpectedExternalError } from '../../../../../../base/common/errors.js'; +import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; +import { format, noBreakWhitespace } from '../../../../../../base/common/strings.js'; +import { Constants } from '../../../../../../base/common/uint.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { InlineValueContext, InlineValueText, InlineValueVariableLookup } from '../../../../../../editor/common/languages.js'; +import { IModelDeltaDecoration, InjectedTextCursorStops } from '../../../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { localize } from '../../../../../../nls.js'; +import { registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IDebugService, State } from '../../../../debug/common/debug.js'; +import { NotebookSetting } from '../../../common/notebookCommon.js'; +import { ICellExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from '../../../common/notebookExecutionStateService.js'; +import { INotebookKernelMatchResult, INotebookKernelService, VariablesResult } from '../../../common/notebookKernelService.js'; +import { INotebookActionContext, NotebookAction } from '../../controller/coreActions.js'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; +import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; + +// value from debug, may need to keep an eye on and shorter to account for cells having a narrower viewport width +const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator. If exceeded ... is added + +const variableRegex = new RegExp( + '(?:[a-zA-Z_][a-zA-Z0-9_]*\\.)*' + //match any number of variable names separated by '.' + '[a-zA-Z_][a-zA-Z0-9_]*', //math variable name + 'g', +); + +class InlineSegment { + constructor(public column: number, public text: string) { + } +} + +export class NotebookInlineVariablesController extends Disposable implements INotebookEditorContribution { + + static readonly id: string = 'notebook.inlineVariablesController'; + + private cellDecorationIds = new Map(); + private cellContentListeners = new ResourceMap(); + + private currentCancellationTokenSources = new ResourceMap(); + + constructor( + private readonly notebookEditor: INotebookEditor, + @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IDebugService private readonly debugService: IDebugService, + ) { + super(); + + this._register(this.notebookExecutionStateService.onDidChangeExecution(async e => { + if (!this.configurationService.getValue(NotebookSetting.notebookInlineValues)) { + return; + } + + if (e.type === NotebookExecutionType.cell) { + await this.updateInlineVariables(e); + } + })); + } + + private async updateInlineVariables(event: ICellExecutionStateChangedEvent): Promise { + if (event.changed) { // undefined -> execution was completed, so return on all else. no code should execute until we know it's an execution completion + return; + } + + const cell = this.notebookEditor.getCellByHandle(event.cellHandle); + if (!cell) { + return; + } + + // Cancel any ongoing request in this cell + const existingSource = this.currentCancellationTokenSources.get(cell.uri); + if (existingSource) { + existingSource.cancel(); + } + + // Create a new CancellationTokenSource for the new request per cell + this.currentCancellationTokenSources.set(cell.uri, new CancellationTokenSource()); + const token = this.currentCancellationTokenSources.get(cell.uri)!.token; + + if (this.debugService.state !== State.Inactive) { + this._clearNotebookInlineDecorations(); + return; + } + + if (!this.notebookEditor.textModel?.uri || !isEqual(this.notebookEditor.textModel.uri, event.notebook)) { + return; + } + + const model = await cell.resolveTextModel(); + if (!model) { + return; + } + + this.clearCellInlineDecorations(cell); + + const inlineDecorations: IModelDeltaDecoration[] = []; + + if (this.languageFeaturesService.inlineValuesProvider.has(model)) { + // use extension based provider, borrowed from https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L679 + const lastLine = model.getLineCount(); + const lastColumn = model.getLineMaxColumn(lastLine); + const ctx: InlineValueContext = { + frameId: 0, // ignored, we won't have a stack from since not in a debug session + stoppedLocation: new Range(lastLine, lastColumn, lastLine, lastColumn) // executing cell by cell, so "stopped" location would just be the end of document + }; + + const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse(); + const lineDecorations = new Map(); + + const fullCellRange = new Range(1, 1, lastLine, lastColumn); + + const promises = providers.flatMap(provider => Promise.resolve(provider.provideInlineValues(model, fullCellRange, ctx, token)).then(async (result) => { + if (result) { + + let kernel: INotebookKernelMatchResult; + const kernelVars: VariablesResult[] = []; + if (result.some(iv => iv.type === 'variable')) { // if anyone will need a lookup, get vars now to avoid needing to do it multiple times + if (!this.notebookEditor.hasModel()) { + return; // should not happen, a cell will be executed + } + kernel = this.notebookKernelService.getMatchingKernel(this.notebookEditor.textModel); + const variables = kernel.selected?.provideVariables(event.notebook, undefined, 'named', 0, token); + if (!variables) { + return; + } + for await (const v of variables) { + kernelVars.push(v); + } + } + + for (const iv of result) { + let text: string | undefined = undefined; + switch (iv.type) { + case 'text': + text = (iv as InlineValueText).text; + break; + case 'variable': { + const name = (iv as InlineValueVariableLookup).variableName; + if (!name) { + continue; // skip to next var, no valid name to lookup with + } + const value = kernelVars.find(v => v.name === name)?.value; + if (!value) { + continue; + } + text = format('{0} = {1}', name, value); + break; + } + case 'expression': { + continue; // no active debug session, so evaluate would break + } + } + + if (text) { + const line = iv.range.startLineNumber; + let lineSegments = lineDecorations.get(line); + if (!lineSegments) { + lineSegments = []; + lineDecorations.set(line, lineSegments); + } + if (!lineSegments.some(iv => iv.text === text)) { // de-dupe + lineSegments.push(new InlineSegment(iv.range.startColumn, text)); + } + } + } + } + }, err => { + onUnexpectedExternalError(err); + })); + + await Promise.all(promises); + + // sort line segments and concatenate them into a decoration + lineDecorations.forEach((segments, line) => { + if (segments.length > 0) { + segments = segments.sort((a, b) => a.column - b.column); + const text = segments.map(s => s.text).join(', '); + inlineDecorations.push(...this.createNotebookInlineValueDecoration(line, text)); + + } + }); + + } else { // generic regex matching approach + if (!this.notebookEditor.hasModel()) { + return; // should not happen, a cell will be executed + } + const kernel = this.notebookKernelService.getMatchingKernel(this.notebookEditor.textModel); + const variables = kernel?.selected?.provideVariables(event.notebook, undefined, 'named', 0, CancellationToken.None); + if (!variables) { + return; + } + + const vars: VariablesResult[] = []; + for await (const v of variables) { + vars.push(v); + } + const varNames: string[] = vars.map(v => v.name); + + const document = cell.textModel; + if (!document) { + return; + } + + for (let i = 1; i <= document.getLineCount(); i++) { + const line = document.getLineContent(i); + + if (line.trimStart().startsWith('#')) { + continue; + } + for (let match = variableRegex.exec(line); match; match = variableRegex.exec(line)) { + const name = match[0]; + if (varNames.includes(name)) { + const inlineVal = name + ' = ' + vars.find(v => v.name === name)?.value; + inlineDecorations.push(...this.createNotebookInlineValueDecoration(i, inlineVal)); + } + } + } + } + + if (inlineDecorations.length > 0) { + this.updateCellInlineDecorations(cell, inlineDecorations); + this.initCellContentListener(cell); + } + } + + private updateCellInlineDecorations(cell: ICellViewModel, decorations: IModelDeltaDecoration[]) { + const oldDecorations = this.cellDecorationIds.get(cell) ?? []; + this.cellDecorationIds.set(cell, cell.deltaModelDecorations( + oldDecorations, + decorations + )); + } + + private initCellContentListener(cell: ICellViewModel) { + const cellModel = cell.textModel; + if (!cellModel) { + return; // should not happen + } + + this.cellContentListeners.set(cell.uri, cellModel.onDidChangeContent(() => { + this.clearCellInlineDecorations(cell); + })); + } + + private clearCellInlineDecorations(cell: ICellViewModel) { + const cellDecorations = this.cellDecorationIds.get(cell) ?? []; + if (cellDecorations) { + cell.deltaModelDecorations(cellDecorations, []); + this.cellDecorationIds.delete(cell); + } + + const listener = this.cellContentListeners.get(cell.uri); + if (listener) { + listener.dispose(); + this.cellContentListeners.delete(cell.uri); + } + } + + private _clearNotebookInlineDecorations() { + this.cellDecorationIds.forEach((_, cell) => { + this.clearCellInlineDecorations(cell); + }); + } + + public clearNotebookInlineDecorations() { + this._clearNotebookInlineDecorations(); + } + + // taken from /src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts + private createNotebookInlineValueDecoration(lineNumber: number, contentText: string, column = Constants.MAX_SAFE_SMALL_INTEGER): IModelDeltaDecoration[] { + // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line + if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { + contentText = contentText.substring(0, MAX_INLINE_DECORATOR_LENGTH) + '...'; + } + + return [ + { + range: { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: column, + endColumn: column + }, + options: { + description: 'nb-inline-value-decoration-spacer', + after: { + content: noBreakWhitespace, + cursorStops: InjectedTextCursorStops.None + }, + showIfCollapsed: true, + } + }, + { + range: { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: column, + endColumn: column + }, + options: { + description: 'nb-inline-value-decoration', + after: { + content: this.replaceWsWithNoBreakWs(contentText), + inlineClassName: 'nb-inline-value', + inlineClassNameAffectsLetterSpacing: true, + cursorStops: InjectedTextCursorStops.None + }, + showIfCollapsed: true, + } + }, + ]; + } + + private replaceWsWithNoBreakWs(str: string): string { + return str.replace(/[ \t]/g, noBreakWhitespace); + } + + override dispose(): void { + super.dispose(); + this._clearNotebookInlineDecorations(); + this.currentCancellationTokenSources.forEach(source => source.cancel()); + this.currentCancellationTokenSources.clear(); + } +} + + +registerNotebookContribution(NotebookInlineVariablesController.id, NotebookInlineVariablesController); + +registerAction2(class ClearNotebookInlineValues extends NotebookAction { + constructor() { + super({ + id: 'notebook.clearAllInlineValues', + title: localize('clearAllInlineValues', 'Clear All Inline Values'), + }); + } + + override runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const editor = context.notebookEditor; + const controller = editor.getContribution(NotebookInlineVariablesController.id); + controller.clearNotebookInlineDecorations(); + return Promise.resolve(); + } + +}); diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index e32df03950a..c367f5a8988 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -15,7 +15,7 @@ import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/con import { InputFocusedContextKey } from '../../../../../../platform/contextkey/common/contextkeys.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, EditMode, InlineChatResponseType, MENU_INLINE_CHAT_WIDGET_STATUS } from '../../../../inlineChat/common/inlineChat.js'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, InlineChatResponseType, MENU_INLINE_CHAT_WIDGET_STATUS } from '../../../../inlineChat/common/inlineChat.js'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_HAS_AGENT, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from './notebookChatContext.js'; import { NotebookChatController } from './notebookChatController.js'; import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getContextFromActiveEditor, getEditorFromArgsOrActivePane } from '../coreActions.js'; @@ -684,7 +684,6 @@ export class AcceptChangesAndRun extends AbstractInlineChatAction { precondition: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), CTX_INLINE_CHAT_VISIBLE, - ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview)) ), keybinding: undefined, menu: [{ diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 1dc3bb5b7d7..f613e034b6a 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -40,6 +40,7 @@ import { INotebookKernelService } from '../../common/notebookKernelService.js'; import { ICellRange } from '../../common/notebookRange.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js'; +import { NotebookInlineVariablesController } from '../contrib/notebookVariables/notebookInlineVariables.js'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; @@ -338,6 +339,9 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { if (clearExecutionMetadataEdits.length) { context.notebookEditor.textModel.applyEdits(clearExecutionMetadataEdits, true, undefined, () => undefined, undefined, computeUndoRedo); } + + const controller = editor.getContribution(NotebookInlineVariablesController.id); + controller.clearNotebookInlineDecorations(); } }); diff --git a/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 179fd0d6240..db5cc015738 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -335,7 +335,7 @@ } .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row .codicon:not(.suggest-icon) { - color: inherit; + color: var(--vscode-icon-foreground); } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-overview-ruler-container { @@ -449,6 +449,13 @@ background-color: var(--vscode-notebook-symbolHighlightBackground) !important; } +/** Cell execution inline vars */ +.nb-inline-value { + background-color: var(--vscode-editorInlayHint-background); + color: var(--vscode-editorInlayHint-foreground) !important; + font-size: 90%; +} + /** Notebook Textual Selection Highlight */ .nb-selection-highlight { background-color: var(--vscode-editor-selectionBackground); diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 17568757a13..1a030514703 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -100,6 +100,7 @@ import './contrib/kernelDetection/notebookKernelDetection.js'; import './contrib/cellDiagnostics/cellDiagnostics.js'; import './contrib/multicursor/notebookMulticursor.js'; import './contrib/multicursor/notebookSelectionHighlight.js'; +import './contrib/notebookVariables/notebookInlineVariables.js'; // Diff Editor Contribution import './diff/notebookDiffActions.js'; @@ -1235,6 +1236,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: false }, + [NotebookSetting.notebookInlineValues]: { + markdownDescription: nls.localize('notebook.inlineValues.description', "Enable the showing of inline values within notebook code cells after cell execution. Values will remain until the cell is edited, re-executed, or explicitly cleared via the Clear All Outputs toolbar button or the `Notebook: Clear Inline Values` command. "), + type: 'boolean', + default: false + }, [NotebookSetting.cellFailureDiagnostics]: { markdownDescription: nls.localize('notebook.cellFailureDiagnostics', "Show available diagnostics for cell failures."), type: 'boolean', @@ -1250,5 +1256,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: false }, + [NotebookSetting.markupFontFamily]: { + markdownDescription: nls.localize('notebook.markupFontFamily', "Controls the font family of rendered markup in notebooks. When left blank, this will fall back to the default workbench font family."), + type: 'string', + default: '', + tags: ['notebookLayout'] + }, } }); diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts index 9e2118bcb10..45d8a96ba20 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IS_COMPOSITE_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED } from '../common/notebookContextKeys.js'; import { localize } from '../../../../nls.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -14,7 +14,7 @@ import { IVisibleEditorPane } from '../../../common/editor.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { +export class NotebookAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'notebook'; readonly when = ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, IS_COMPOSITE_NOTEBOOK.negate()); diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts index fc8d932181a..56e6d347e4e 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -13,7 +13,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { InputFocusedContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { getAllOutputsText } from './viewModel/cellOutputTextHelper.js'; -export class NotebookAccessibleView implements IAccessibleViewImplentation { +export class NotebookAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'notebook'; readonly type = AccessibleViewType.View; diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b528b21b2a6..0099df4fb2b 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -103,6 +103,7 @@ import { PreventDefaultContextMenuItemsContextKeyName } from '../../webview/brow import { NotebookAccessibilityProvider } from './notebookAccessibilityProvider.js'; import { NotebookHorizontalTracker } from './viewParts/notebookHorizontalTracker.js'; import { NotebookCellEditorPool } from './view/notebookCellEditorPool.js'; +import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; const $ = DOM.$; @@ -2040,6 +2041,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD CopyPasteController.get(editor)?.clearWidgets(); } }); + + this._renderedEditors.forEach((editor, cell) => { + const controller = InlineCompletionsController.get(editor); + if (controller?.model.get()?.inlineEditState.get()) { + editor.render(true); + } + }); } private editorHasDomFocus(): boolean { diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 634b9cdb5bb..159928cf81b 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -53,6 +53,7 @@ export interface NotebookDisplayOptions { // TODO @Yoyokrazy rename to a more ge 'editor.tabSize': number; 'editor.insertSpaces': boolean; }> | undefined; + markupFontFamily: string; } export interface NotebookLayoutConfiguration { @@ -108,6 +109,7 @@ export interface NotebookOptionsChangeEvent { readonly outputLinkifyFilePaths?: boolean; readonly minimalError?: boolean; readonly readonly?: boolean; + readonly markupFontFamily?: boolean; } const defaultConfigConstants = Object.freeze({ @@ -213,6 +215,7 @@ export class NotebookOptions extends Disposable { const outputLineLimit = this.configurationService.getValue(NotebookSetting.textOutputLineLimit) ?? 30; const linkifyFilePaths = this.configurationService.getValue(NotebookSetting.LinkifyOutputFilePaths) ?? true; const minimalErrors = this.configurationService.getValue(NotebookSetting.minimalErrorRendering); + const markupFontFamily = this.configurationService.getValue(NotebookSetting.markupFontFamily); const editorTopPadding = this._computeEditorTopPadding(); @@ -259,7 +262,8 @@ export class NotebookOptions extends Disposable { outputWordWrap: outputWordWrap, outputLineLimit: outputLineLimit, outputLinkifyFilePaths: linkifyFilePaths, - outputMinimalError: minimalErrors + outputMinimalError: minimalErrors, + markupFontFamily }; this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -426,6 +430,7 @@ export class NotebookOptions extends Disposable { const outputWordWrap = e.affectsConfiguration(NotebookSetting.outputWordWrap); const outputLinkifyFilePaths = e.affectsConfiguration(NotebookSetting.LinkifyOutputFilePaths); const minimalError = e.affectsConfiguration(NotebookSetting.minimalErrorRendering); + const markupFontFamily = e.affectsConfiguration(NotebookSetting.markupFontFamily); if ( !cellStatusBarVisibility @@ -454,7 +459,8 @@ export class NotebookOptions extends Disposable { && !outputScrolling && !outputWordWrap && !outputLinkifyFilePaths - && !minimalError) { + && !minimalError + && !markupFontFamily) { return; } @@ -569,6 +575,10 @@ export class NotebookOptions extends Disposable { configuration.outputMinimalError = this.configurationService.getValue(NotebookSetting.minimalErrorRendering); } + if (markupFontFamily) { + configuration.markupFontFamily = this.configurationService.getValue(NotebookSetting.markupFontFamily); + } + this._layoutConfiguration = Object.freeze(configuration); // trigger event @@ -599,7 +609,8 @@ export class NotebookOptions extends Disposable { outputScrolling, outputWordWrap, outputLinkifyFilePaths, - minimalError + minimalError, + markupFontFamily }); } @@ -814,7 +825,8 @@ export class NotebookOptions extends Disposable { outputWordWrap: this._layoutConfiguration.outputWordWrap, outputLineLimit: this._layoutConfiguration.outputLineLimit, outputLinkifyFilePaths: this._layoutConfiguration.outputLinkifyFilePaths, - minimalError: this._layoutConfiguration.outputMinimalError + minimalError: this._layoutConfiguration.outputMinimalError, + markupFontFamily: this._layoutConfiguration.markupFontFamily }; } @@ -838,7 +850,8 @@ export class NotebookOptions extends Disposable { outputWordWrap: this._layoutConfiguration.outputWordWrap, outputLineLimit: this._layoutConfiguration.outputLineLimit, outputLinkifyFilePaths: false, - minimalError: false + minimalError: false, + markupFontFamily: this._layoutConfiguration.markupFontFamily }; } diff --git a/code/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts b/code/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts index 98184cdfe3a..7c55c37b901 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts @@ -5,7 +5,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -16,7 +16,7 @@ import { getAllOutputsText } from './viewModel/cellOutputTextHelper.js'; /** * The REPL input is already accessible, so we can show a view for the most recent execution output. */ -export class ReplEditorAccessibleView implements IAccessibleViewImplentation { +export class ReplEditorAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'replEditorInput'; readonly type = AccessibleViewType.View; diff --git a/code/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts b/code/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts index 37aa5e99bef..93e248e3c74 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts @@ -7,6 +7,7 @@ import * as nls from '../../../../../nls.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { INotebookLoggingService } from '../../common/notebookLoggingService.js'; import { ILogger, ILoggerService } from '../../../../../platform/log/common/log.js'; +import { windowLogGroup } from '../../../../services/log/common/logConstants.js'; const logChannelId = 'notebook.rendering'; @@ -20,7 +21,7 @@ export class NotebookLoggingService extends Disposable implements INotebookLoggi @ILoggerService loggerService: ILoggerService, ) { super(); - this._logger = this._register(loggerService.createLogger(logChannelId, { name: nls.localize('renderChannelName', "Notebook") })); + this._logger = this._register(loggerService.createLogger(logChannelId, { name: nls.localize('renderChannelName', "Notebook"), group: windowLogGroup })); } debug(category: string, output: string): void { diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 8eb293b41ba..966a7e69917 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -121,6 +121,7 @@ interface BacklayerWebviewOptions { readonly outputLineLimit: number; readonly outputLinkifyFilePaths: boolean; readonly minimalError: boolean; + readonly markupFontFamily: string; } @@ -281,6 +282,7 @@ export class BackLayerWebView extends Themable { key: 'notebook.error.rendererFallbacksExhausted', comment: ['$0 is a placeholder for the mime type'] }, "Could not render content for '$0'"), + 'notebook-markup-font-family': this.options.markupFontFamily, }; } @@ -370,6 +372,7 @@ export class BackLayerWebView extends Themable { font-size: var(--notebook-markup-font-size); line-height: var(--notebook-markdown-line-height); color: var(--theme-ui-foreground); + font-family: var(--notebook-markup-font-family); } #container div.preview.draggable { diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index da6d359d55e..ff6f74f9096 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -188,7 +188,8 @@ async function webviewPreloads(ctx: PreloadContext) { }; const isEditableElement = (element: Element) => { - return element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea' || 'editContext' in element; + return element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea' + || ('editContext' in element && !!element.editContext); }; // check if an input element is focused within the output element diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d656f74241f..6fd9ec8fa9a 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -1029,10 +1029,12 @@ export const NotebookSetting = { cellChat: 'notebook.experimental.cellChat', cellGenerate: 'notebook.experimental.generate', notebookVariablesView: 'notebook.variablesView', + notebookInlineValues: 'notebook.inlineValues', InteractiveWindowPromptToSave: 'interactiveWindow.promptToSaveOnClose', cellFailureDiagnostics: 'notebook.cellFailureDiagnostics', outputBackupSizeLimit: 'notebook.backup.sizeLimit', multiCursor: 'notebook.multiCursor.enabled', + markupFontFamily: 'notebook.markupFontFamily', } as const; export const enum CellStatusbarAlignment { diff --git a/code/src/vs/workbench/contrib/output/browser/output.contribution.ts b/code/src/vs/workbench/contrib/output/browser/output.contribution.ts index 7288357d606..b1cbad16c0d 100644 --- a/code/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/code/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -22,13 +22,11 @@ import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContaine import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IQuickPickItem, IQuickInputService, IQuickPickSeparator, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { AUX_WINDOW_GROUP, AUX_WINDOW_GROUP_TYPE, IEditorService } from '../../../services/editor/common/editorService.js'; -import { assertIsDefined } from '../../../../base/common/types.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ILoggerService, LogLevel, LogLevelToLocalizedString, LogLevelToString } from '../../../../platform/log/common/log.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; @@ -37,6 +35,14 @@ import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.j import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; import { IsWindowsContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { FocusedViewContext } from '../../../common/contextkeys.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; +import { ViewAction } from '../../../browser/parts/views/viewPane.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { basename } from '../../../../base/common/resources.js'; + +const IMPORTED_LOG_ID_PREFIX = 'importedLog.'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -91,7 +97,6 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { constructor( @IOutputService private readonly outputService: IOutputService, @IEditorService private readonly editorService: IEditorService, - @IFilesConfigurationService private readonly fileConfigurationService: IFilesConfigurationService, ) { super(); this.registerActions(); @@ -99,14 +104,21 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { private registerActions(): void { this.registerSwitchOutputAction(); + this.registerAddCompoundLogAction(); + this.registerRemoveLogAction(); this.registerShowOutputChannelsAction(); this.registerClearOutputAction(); this.registerToggleAutoScrollAction(); this.registerOpenActiveOutputFileAction(); this.registerOpenActiveOutputFileInAuxWindowAction(); + this.registerSaveActiveOutputAsAction(); this.registerShowLogsAction(); this.registerOpenLogFileAction(); this.registerConfigureActiveOutputLogLevelAction(); + this.registerLogLevelFilterActions(); + this.registerClearFilterActions(); + this.registerExportLogsAction(); + this.registerImportLogAction(); } private registerSwitchOutputAction(): void { @@ -137,7 +149,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => { for (const channel of channels) { const title = channel.label; - const group = channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; + const group = channel.user ? '2_user_outputchannels' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; registeredChannels.set(channel.id, registerAction2(class extends Action2 { constructor() { super({ @@ -165,8 +177,86 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } })); this._register(outputChannelRegistry.onDidRemoveChannel(e => { - registeredChannels.get(e)?.dispose(); - registeredChannels.delete(e); + registeredChannels.get(e.id)?.dispose(); + registeredChannels.delete(e.id); + })); + } + + private registerAddCompoundLogAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.output.addCompoundLog', + title: nls.localize2('addCompoundLog', "Add Compound Log..."), + category: Categories.Developer, + f1: true, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: '2_add', + }], + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + + const extensionLogs: IOutputChannelDescriptor[] = [], logs: IOutputChannelDescriptor[] = []; + for (const channel of outputService.getChannelDescriptors()) { + if (channel.log && !channel.user) { + if (channel.extensionId) { + extensionLogs.push(channel); + } else { + logs.push(channel); + } + } + } + const entries: Array = []; + for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + if (extensionLogs.length && logs.length) { + entries.push({ type: 'separator', label: nls.localize('extensionLogs', "Extension Logs") }); + } + for (const log of extensionLogs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); + if (result?.length) { + outputService.showChannel(outputService.registerCompoundLogChannel(result)); + } + } + })); + } + + private registerRemoveLogAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.output.remove', + title: nls.localize2('removeLog', "Remove Output..."), + category: Categories.Developer, + f1: true + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + const notificationService = accessor.get(INotificationService); + const entries: Array = outputService.getChannelDescriptors().filter(channel => channel.user); + if (entries.length === 0) { + notificationService.info(nls.localize('nocustumoutput', "No custom outputs to remove.")); + return; + } + const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); + if (!result?.length) { + return; + } + const outputChannelRegistry = Registry.as(Extensions.OutputChannels); + for (const channel of result) { + outputChannelRegistry.removeChannel(channel.id); + } + } })); } @@ -285,11 +375,10 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { isHiddenByDefault: true }], icon: Codicon.goToFile, - precondition: CONTEXT_ACTIVE_FILE_OUTPUT }); } async run(): Promise { - that.openActiveOutoutFile(); + that.openActiveOutput(); } })); } @@ -309,21 +398,46 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { isHiddenByDefault: true }], icon: Codicon.emptyWindow, - precondition: CONTEXT_ACTIVE_FILE_OUTPUT }); } async run(): Promise { - that.openActiveOutoutFile(AUX_WINDOW_GROUP); + that.openActiveOutput(AUX_WINDOW_GROUP); + } + })); + } + + private registerSaveActiveOutputAsAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.saveActiveLogOutputAs`, + title: nls.localize2('saveActiveOutputAs', "Save Output As..."), + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: '1_export', + order: 1 + }], + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const channel = outputService.getActiveChannel(); + if (channel) { + const descriptor = outputService.getChannelDescriptors().find(c => c.id === channel.id); + if (descriptor) { + await outputService.saveOutputAs(descriptor); + } + } } })); } - private async openActiveOutoutFile(group?: AUX_WINDOW_GROUP_TYPE): Promise { - const fileOutputChannelDescriptor = this.getFileOutputChannelDescriptor(); - if (fileOutputChannelDescriptor) { - await this.fileConfigurationService.updateReadonly(fileOutputChannelDescriptor.file, true); + private async openActiveOutput(group?: AUX_WINDOW_GROUP_TYPE): Promise { + const channel = this.outputService.getActiveChannel(); + if (channel) { await this.editorService.openEditor({ - resource: fileOutputChannelDescriptor.file, + resource: channel.uri, options: { pinned: true, }, @@ -331,19 +445,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } } - private getFileOutputChannelDescriptor(): IFileOutputChannelDescriptor | null { - const channel = this.outputService.getActiveChannel(); - if (channel) { - const descriptor = this.outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0]; - if (descriptor?.file) { - return descriptor; - } - } - return null; - } - private registerConfigureActiveOutputLogLevelAction(): void { - const that = this; const logLevelMenu = new MenuId('workbench.output.menu.logLevel'); this._register(MenuRegistry.appendMenuItem(MenuId.ViewTitle, { submenu: logLevelMenu, @@ -370,11 +472,12 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor): Promise { - const channel = that.outputService.getActiveChannel(); + const outputService = accessor.get(IOutputService); + const channel = outputService.getActiveChannel(); if (channel) { - const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.file) { - return accessor.get(ILoggerService).setLogLevel(channelDescriptor.file, logLevel); + const channelDescriptor = outputService.getChannelDescriptor(channel.id); + if (channelDescriptor) { + outputService.setLogLevel(channelDescriptor, logLevel); } } } @@ -402,12 +505,15 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor): Promise { - const channel = that.outputService.getActiveChannel(); + const outputService = accessor.get(IOutputService); + const loggerService = accessor.get(ILoggerService); + const defaultLogLevelsService = accessor.get(IDefaultLogLevelsService); + const channel = outputService.getActiveChannel(); if (channel) { - const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.file) { - const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.file); - return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); + const channelDescriptor = outputService.getChannelDescriptor(channel.id); + if (channelDescriptor && isSingleSourceOutputChannelDescriptor(channelDescriptor)) { + const logLevel = loggerService.getLogLevel(channelDescriptor.source.resource); + return await defaultLogLevelsService.setDefaultLogLevel(logLevel, channelDescriptor.extensionId); } } } @@ -458,14 +564,11 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } private registerOpenLogFileAction(): void { - interface IOutputChannelQuickPickItem extends IQuickPickItem { - channel: IOutputChannelDescriptor; - } this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openLogFile', - title: nls.localize2('openLogFile', "Open Log File..."), + title: nls.localize2('openLogFile', "Open Log..."), category: Categories.Developer, menu: { id: MenuId.CommandPalette, @@ -486,15 +589,13 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); - const fileConfigurationService = accessor.get(IFilesConfigurationService); - - let entry: IOutputChannelQuickPickItem | undefined; + let entry: IQuickPickItem | undefined; const argName = args && typeof args === 'string' ? args : undefined; - const extensionChannels: IOutputChannelQuickPickItem[] = []; - const coreChannels: IOutputChannelQuickPickItem[] = []; + const extensionChannels: IQuickPickItem[] = []; + const coreChannels: IQuickPickItem[] = []; for (const c of outputService.getChannelDescriptors()) { - if (c.file && c.log) { - const e = { id: c.id, label: c.label, channel: c }; + if (c.log) { + const e = { id: c.id, label: c.label }; if (c.extensionId) { extensionChannels.push(e); } else { @@ -511,22 +612,199 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { entries.push({ type: 'separator' }); entries.push(...coreChannels.sort((a, b) => a.label.localeCompare(b.label))); } - entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log File") }); + entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log File") }); } - if (entry) { - const resource = assertIsDefined(entry.channel.file); - await fileConfigurationService.updateReadonly(resource, true); - await editorService.openEditor({ - resource, - options: { - pinned: true, - } + if (entry?.id) { + const channel = outputService.getChannel(entry.id); + if (channel) { + await editorService.openEditor({ + resource: channel.uri, + options: { + pinned: true, + } + }); + } + } + } + })); + } + + private registerLogLevelFilterActions(): void { + let order = 0; + const registerLogLevel = (logLevel: LogLevel, toggled: ContextKeyExpression) => { + this._register(registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${LogLevelToString(logLevel)}`, + title: LogLevelToLocalizedString(logLevel).value, + metadata: { + description: localize2('toggleTraceDescription', "Show or hide {0} messages in the output", LogLevelToString(logLevel)) + }, + toggled, + menu: { + id: viewFilterSubmenu, + group: '2_log_filter', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_LOG_FILE_OUTPUT), + order: order++ + }, + viewId: OUTPUT_VIEW_ID }); } + async runInView(serviceAccessor: ServicesAccessor, view: OutputViewPane): Promise { + this.toggleLogLevelFilter(serviceAccessor.get(IOutputService), logLevel); + } + private toggleLogLevelFilter(outputService: IOutputService, logLevel: LogLevel): void { + switch (logLevel) { + case LogLevel.Trace: + outputService.filters.trace = !outputService.filters.trace; + break; + case LogLevel.Debug: + outputService.filters.debug = !outputService.filters.debug; + break; + case LogLevel.Info: + outputService.filters.info = !outputService.filters.info; + break; + case LogLevel.Warning: + outputService.filters.warning = !outputService.filters.warning; + break; + case LogLevel.Error: + outputService.filters.error = !outputService.filters.error; + break; + } + } + })); + }; + + registerLogLevel(LogLevel.Trace, SHOW_TRACE_FILTER_CONTEXT); + registerLogLevel(LogLevel.Debug, SHOW_DEBUG_FILTER_CONTEXT); + registerLogLevel(LogLevel.Info, SHOW_INFO_FILTER_CONTEXT); + registerLogLevel(LogLevel.Warning, SHOW_WARNING_FILTER_CONTEXT); + registerLogLevel(LogLevel.Error, SHOW_ERROR_FILTER_CONTEXT); + } + + private registerClearFilterActions(): void { + this._register(registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.clearFilterText`, + title: localize('clearFiltersText', "Clear filters text"), + keybinding: { + when: OUTPUT_FILTER_FOCUS_CONTEXT, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape + }, + viewId: OUTPUT_VIEW_ID + }); + } + async runInView(serviceAccessor: ServicesAccessor, outputView: OutputViewPane): Promise { + outputView.clearFilterText(); } })); } + private registerExportLogsAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.exportLogs`, + title: nls.localize2('exportLogs', "Export Logs..."), + f1: true, + category: Categories.Developer, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: '1_export', + order: 2, + }], + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + const extensionLogs: IOutputChannelDescriptor[] = [], logs: IOutputChannelDescriptor[] = [], userLogs: IOutputChannelDescriptor[] = []; + for (const channel of outputService.getChannelDescriptors()) { + if (channel.log) { + if (channel.extensionId) { + extensionLogs.push(channel); + } else if (channel.user) { + userLogs.push(channel); + } else { + logs.push(channel); + } + } + } + const entries: Array = []; + for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + if (extensionLogs.length && logs.length) { + entries.push({ type: 'separator', label: nls.localize('extensionLogs', "Extension Logs") }); + } + for (const log of extensionLogs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + if (userLogs.length && (extensionLogs.length || logs.length)) { + entries.push({ type: 'separator', label: nls.localize('userLogs', "User Logs") }); + } + for (const log of userLogs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); + if (result?.length) { + await outputService.saveOutputAs(...result); + } + } + })); + } + + private registerImportLogAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.importLog`, + title: nls.localize2('importLog', "Import Log..."), + f1: true, + category: Categories.Developer, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: '2_add', + order: 2, + }], + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const fileDialogService = accessor.get(IFileDialogService); + const result = await fileDialogService.showOpenDialog({ + title: nls.localize('importLogFile', "Import Log File"), + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: true, + filters: [{ + name: nls.localize('logFiles', "Log Files"), + extensions: ['log'] + }] + }); + + if (result?.length) { + const channelName = basename(result[0]); + const channelId = `${IMPORTED_LOG_ID_PREFIX}${Date.now()}`; + // Register and show the channel + Registry.as(Extensions.OutputChannels).registerChannel({ + id: channelId, + label: channelName, + log: true, + user: true, + source: result.length === 1 + ? { resource: result[0] } + : result.map(resource => ({ resource, name: basename(resource).split('.')[0] })) + }); + outputService.showChannel(channelId); + } + } + })); + } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored); diff --git a/code/src/vs/workbench/contrib/output/browser/outputServices.ts b/code/src/vs/workbench/contrib/output/browser/outputServices.ts index 25950e9f7e9..a140d38c0e6 100644 --- a/code/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/code/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -6,24 +6,30 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { URI } from '../../../../base/common/uri.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from '../../../services/output/common/output.js'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_CATEGORY_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor, ILogEntry } from '../../../services/output/common/output.js'; import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { ILogService, ILoggerService, LogLevelToString } from '../../../../platform/log/common/log.js'; +import { ILogService, ILoggerService, LogLevel, LogLevelToString } from '../../../../platform/log/common/log.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { IOutputChannelModel } from '../common/outputChannelModel.js'; +import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel, MultiFileOutputChannelModel } from '../common/outputChannelModel.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { OutputViewPane } from './outputView.js'; -import { IOutputChannelModelService } from '../common/outputChannelModelService.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { SetLogLevelAction } from '../../logs/common/logsActions.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { localize } from '../../../../nls.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { extensionTelemetryLogChannelId, telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; +import { toLocalISOString } from '../../../../base/common/date.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -37,14 +43,31 @@ class OutputChannel extends Disposable implements IOutputChannel { constructor( readonly outputChannelDescriptor: IOutputChannelDescriptor, - @IOutputChannelModelService outputChannelModelService: IOutputChannelModelService, - @ILanguageService languageService: ILanguageService, + private readonly outputLocation: URI, + private readonly outputDirPromise: Promise, + @ILanguageService private readonly languageService: ILanguageService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.id = outputChannelDescriptor.id; this.label = outputChannelDescriptor.label; this.uri = URI.from({ scheme: Schemas.outputChannel, path: this.id }); - this.model = this._register(outputChannelModelService.createOutputChannelModel(this.id, this.uri, outputChannelDescriptor.languageId ? languageService.createById(outputChannelDescriptor.languageId) : languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME), outputChannelDescriptor.file)); + this.model = this._register(this.createOutputChannelModel(this.uri, outputChannelDescriptor)); + } + + private createOutputChannelModel(uri: URI, outputChannelDescriptor: IOutputChannelDescriptor): IOutputChannelModel { + const language = outputChannelDescriptor.languageId ? this.languageService.createById(outputChannelDescriptor.languageId) : this.languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME); + if (isMultiSourceOutputChannelDescriptor(outputChannelDescriptor)) { + return this.instantiationService.createInstance(MultiFileOutputChannelModel, uri, language, [...outputChannelDescriptor.source]); + } + if (isSingleSourceOutputChannelDescriptor(outputChannelDescriptor)) { + return this.instantiationService.createInstance(FileOutputChannelModel, uri, language, outputChannelDescriptor.source); + } + return this.instantiationService.createInstance(DelegatedOutputChannelModel, this.id, uri, language, this.outputLocation, this.outputDirPromise); + } + + getLogEntries(): ReadonlyArray { + return this.model.getLogEntries(); } append(output: string): void { @@ -64,11 +87,136 @@ class OutputChannel extends Disposable implements IOutputChannel { } } +interface IOutputFilterOptions { + filterHistory: string[]; + trace: boolean; + debug: boolean; + info: boolean; + warning: boolean; + error: boolean; + sources: string; +} + +class OutputViewFilters extends Disposable implements IOutputViewFilters { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor( + options: IOutputFilterOptions, + private readonly contextKeyService: IContextKeyService + ) { + super(); + + this._trace.set(options.trace); + this._debug.set(options.debug); + this._info.set(options.info); + this._warning.set(options.warning); + this._error.set(options.error); + this._categories.set(options.sources); + + this.filterHistory = options.filterHistory; + } + + filterHistory: string[]; + + private _filterText = ''; + get text(): string { + return this._filterText; + } + set text(filterText: string) { + if (this._filterText !== filterText) { + this._filterText = filterText; + this._onDidChange.fire(); + } + } + + private readonly _trace = SHOW_TRACE_FILTER_CONTEXT.bindTo(this.contextKeyService); + get trace(): boolean { + return !!this._trace.get(); + } + set trace(trace: boolean) { + if (this._trace.get() !== trace) { + this._trace.set(trace); + this._onDidChange.fire(); + } + } + + private readonly _debug = SHOW_DEBUG_FILTER_CONTEXT.bindTo(this.contextKeyService); + get debug(): boolean { + return !!this._debug.get(); + } + set debug(debug: boolean) { + if (this._debug.get() !== debug) { + this._debug.set(debug); + this._onDidChange.fire(); + } + } + + private readonly _info = SHOW_INFO_FILTER_CONTEXT.bindTo(this.contextKeyService); + get info(): boolean { + return !!this._info.get(); + } + set info(info: boolean) { + if (this._info.get() !== info) { + this._info.set(info); + this._onDidChange.fire(); + } + } + + private readonly _warning = SHOW_WARNING_FILTER_CONTEXT.bindTo(this.contextKeyService); + get warning(): boolean { + return !!this._warning.get(); + } + set warning(warning: boolean) { + if (this._warning.get() !== warning) { + this._warning.set(warning); + this._onDidChange.fire(); + } + } + + private readonly _error = SHOW_ERROR_FILTER_CONTEXT.bindTo(this.contextKeyService); + get error(): boolean { + return !!this._error.get(); + } + set error(error: boolean) { + if (this._error.get() !== error) { + this._error.set(error); + this._onDidChange.fire(); + } + } + + private readonly _categories = HIDE_CATEGORY_FILTER_CONTEXT.bindTo(this.contextKeyService); + get categories(): string { + return this._categories.get() || ','; + } + set categories(categories: string) { + this._categories.set(categories); + this._onDidChange.fire(); + } + + toggleCategory(category: string): void { + const categories = this.categories; + if (this.hasCategory(category)) { + this.categories = categories.replace(`,${category},`, ','); + } else { + this.categories = `${categories}${category},`; + } + } + + hasCategory(category: string): boolean { + if (category === ',') { + return false; + } + return this.categories.includes(`,${category},`); + } +} + export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider { declare readonly _serviceBrand: undefined; - private channels: Map = new Map(); + private readonly channels = this._register(new DisposableMap()); private activeChannelIdInStorage: string; private activeChannel?: OutputChannel; @@ -77,20 +225,28 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelContext: IContextKey; private readonly activeFileOutputChannelContext: IContextKey; + private readonly activeLogOutputChannelContext: IContextKey; private readonly activeOutputChannelLevelSettableContext: IContextKey; private readonly activeOutputChannelLevelContext: IContextKey; private readonly activeOutputChannelLevelIsDefaultContext: IContextKey; + private readonly outputLocation: URI; + + readonly filters: OutputViewFilters; + constructor( @IStorageService private readonly storageService: IStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextModelService textModelResolverService: ITextModelService, + @ITextModelService private readonly textModelService: ITextModelService, @ILogService private readonly logService: ILogService, @ILoggerService private readonly loggerService: ILoggerService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IViewsService private readonly viewsService: IViewsService, @IContextKeyService contextKeyService: IContextKeyService, - @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService + @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IFileService private readonly fileService: IFileService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { super(); this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, ''); @@ -99,12 +255,15 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this._register(this.onActiveOutputChannel(channel => this.activeOutputChannelContext.set(channel))); this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService); + this.activeLogOutputChannelContext = CONTEXT_ACTIVE_LOG_FILE_OUTPUT.bindTo(contextKeyService); this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService); this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService); this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); + this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); + // Register as text model content provider for output - this._register(textModelResolverService.registerTextModelContentProvider(Schemas.outputChannel, this)); + this._register(textModelService.registerTextModelContentProvider(Schemas.outputChannel, this)); this._register(instantiationService.createInstance(OutputLinkProvider)); // Create output channels for already registered channels @@ -112,7 +271,9 @@ export class OutputService extends Disposable implements IOutputService, ITextMo for (const channelIdentifier of registry.getChannels()) { this.onDidRegisterChannel(channelIdentifier.id); } - this._register(registry.onDidRegisterChannel(this.onDidRegisterChannel, this)); + this._register(registry.onDidRegisterChannel(id => this.onDidRegisterChannel(id))); + this._register(registry.onDidUpdateChannelSources(channel => this.onDidUpdateChannelSources(channel))); + this._register(registry.onDidRemoveChannel(channel => this.onDidRemoveChannel(channel))); // Set active channel to first channel if not set if (!this.activeChannel) { @@ -126,7 +287,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } })); - this._register(this.loggerService.onDidChangeLogLevel(_level => { + this._register(this.loggerService.onDidChangeLogLevel(() => { + this.resetLogLevelFilters(); this.setLevelContext(); this.setLevelIsDefaultContext(); })); @@ -135,6 +297,16 @@ export class OutputService extends Disposable implements IOutputService, ITextMo })); this._register(this.lifecycleService.onDidShutdown(() => this.dispose())); + + this.filters = this._register(new OutputViewFilters({ + filterHistory: [], + trace: true, + debug: true, + info: true, + warning: true, + error: true, + sources: '', + }, contextKeyService)); } provideTextContent(resource: URI): Promise | null { @@ -173,6 +345,108 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.activeChannel; } + canSetLogLevel(channel: IOutputChannelDescriptor): boolean { + return channel.log && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; + } + + getLogLevel(channel: IOutputChannelDescriptor): LogLevel | undefined { + if (!channel.log) { + return undefined; + } + const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : []; + if (sources.length === 0) { + return undefined; + } + + const logLevel = this.loggerService.getLogLevel(); + return sources.reduce((prev, curr) => Math.min(prev, this.loggerService.getLogLevel(curr.resource) ?? logLevel), LogLevel.Error); + } + + setLogLevel(channel: IOutputChannelDescriptor, logLevel: LogLevel): void { + if (!channel.log) { + return; + } + const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : []; + if (sources.length === 0) { + return; + } + for (const source of sources) { + this.loggerService.setLogLevel(source.resource, logLevel); + } + } + + registerCompoundLogChannel(descriptors: IOutputChannelDescriptor[]): string { + const outputChannelRegistry = Registry.as(Extensions.OutputChannels); + descriptors.sort((a, b) => a.label.localeCompare(b.label)); + const id = descriptors.map(r => r.id.toLowerCase()).join('-'); + if (!outputChannelRegistry.getChannel(id)) { + outputChannelRegistry.registerChannel({ + id, + label: descriptors.map(r => r.label).join(', '), + log: descriptors.some(r => r.log), + user: true, + source: descriptors.map(descriptor => { + if (isSingleSourceOutputChannelDescriptor(descriptor)) { + return [{ resource: descriptor.source.resource, name: descriptor.source.name ?? descriptor.label }]; + } + if (isMultiSourceOutputChannelDescriptor(descriptor)) { + return descriptor.source; + } + const channel = this.getChannel(descriptor.id); + if (channel) { + return channel.model.source; + } + return []; + }).flat(), + }); + } + return id; + } + + async saveOutputAs(...channels: IOutputChannelDescriptor[]): Promise { + let channel: IOutputChannel | undefined; + if (channels.length > 1) { + const compoundChannelId = this.registerCompoundLogChannel(channels); + channel = this.getChannel(compoundChannelId); + } else { + channel = this.getChannel(channels[0].id); + } + + if (!channel) { + return; + } + + try { + const name = channels.length > 1 ? 'output' : channels[0].label; + const uri = await this.fileDialogService.showSaveDialog({ + title: localize('saveLog.dialogTitle', "Save Output As"), + availableFileSystems: [Schemas.file], + defaultUri: joinPath(await this.fileDialogService.defaultFilePath(), `${name}.log`), + filters: [{ + name, + extensions: ['log'] + }] + }); + + if (!uri) { + return; + } + + const modelRef = await this.textModelService.createModelReference(channel.uri); + try { + await this.fileService.writeFile(uri, VSBuffer.fromString(modelRef.object.textEditorModel.getValue())); + } finally { + modelRef.dispose(); + } + return; + } + finally { + if (channels.length > 1) { + Registry.as(Extensions.OutputChannels).removeChannel(channel.id); + } + } + } + private async onDidRegisterChannel(channelId: string): Promise { const channel = this.createChannel(channelId); this.channels.set(channelId, channel); @@ -184,6 +458,23 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } } + private onDidUpdateChannelSources(channel: IMultiSourceOutputChannelDescriptor): void { + const outputChannel = this.channels.get(channel.id); + if (outputChannel) { + outputChannel.model.updateChannelSources(channel.source); + } + } + + private onDidRemoveChannel(channel: IOutputChannelDescriptor): void { + if (this.activeChannel?.id === channel.id) { + const channels = this.getChannelDescriptors(); + if (channels[0]) { + this.showChannel(channels[0].id); + } + } + this.channels.deleteAndDispose(channel.id); + } + private createChannel(id: string): OutputChannel { const channel = this.instantiateChannel(id); this._register(Event.once(channel.model.onDispose)(() => { @@ -202,26 +493,42 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return channel; } + private outputFolderCreationPromise: Promise | null = null; private instantiateChannel(id: string): OutputChannel { const channelData = Registry.as(Extensions.OutputChannels).getChannel(id); if (!channelData) { this.logService.error(`Channel '${id}' is not registered yet`); throw new Error(`Channel '${id}' is not registered yet`); } - return this.instantiationService.createInstance(OutputChannel, channelData); + if (!this.outputFolderCreationPromise) { + this.outputFolderCreationPromise = this.fileService.createFolder(this.outputLocation).then(() => undefined); + } + return this.instantiationService.createInstance(OutputChannel, channelData, this.outputLocation, this.outputFolderCreationPromise); + } + + private resetLogLevelFilters(): void { + const descriptor = this.activeChannel?.outputChannelDescriptor; + const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; + if (channelLogLevel !== undefined) { + this.filters.error = channelLogLevel <= LogLevel.Error; + this.filters.warning = channelLogLevel <= LogLevel.Warning; + this.filters.info = channelLogLevel <= LogLevel.Info; + this.filters.debug = channelLogLevel <= LogLevel.Debug; + this.filters.trace = channelLogLevel <= LogLevel.Trace; + } } private setLevelContext(): void { const descriptor = this.activeChannel?.outputChannelDescriptor; - const channelLogLevel = descriptor?.log ? this.loggerService.getLogLevel(descriptor.file) : undefined; + const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); } private async setLevelIsDefaultContext(): Promise { const descriptor = this.activeChannel?.outputChannelDescriptor; - if (descriptor?.log) { - const channelLogLevel = this.loggerService.getLogLevel(descriptor.file); - const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); + const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; + if (channelLogLevel !== undefined) { + const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor?.extensionId); this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); } else { this.activeOutputChannelLevelIsDefaultContext.set(false); @@ -231,8 +538,9 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private setActiveChannel(channel: OutputChannel | undefined): void { this.activeChannel = channel; const descriptor = channel?.outputChannelDescriptor; - this.activeFileOutputChannelContext.set(!!descriptor?.file); - this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); + this.activeFileOutputChannelContext.set(!!descriptor && isSingleSourceOutputChannelDescriptor(descriptor)); + this.activeLogOutputChannelContext.set(!!descriptor?.log); + this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && this.canSetLogLevel(descriptor)); this.setLevelIsDefaultContext(); this.setLevelContext(); diff --git a/code/src/vs/workbench/contrib/output/browser/outputView.ts b/code/src/vs/workbench/contrib/output/browser/outputView.ts index 4d280d00857..d3d24dd49f3 100644 --- a/code/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/code/src/vs/workbench/contrib/output/browser/outputView.ts @@ -7,20 +7,20 @@ import * as nls from '../../../../nls.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { IEditorOptions as ICodeEditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, ILogEntry, HIDE_CATEGORY_FILTER_CONTEXT } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; -import { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js'; +import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IViewDescriptorService } from '../../../common/views.js'; @@ -35,8 +35,22 @@ import { ServiceCollection } from '../../../../platform/instantiation/common/ser import { IEditorConfiguration } from '../../../browser/parts/editor/textEditor.js'; import { computeEditorAriaLabel } from '../../../browser/editor.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; - -export class OutputViewPane extends ViewPane { +import { localize } from '../../../../nls.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { LogLevel } from '../../../../platform/log/common/log.js'; +import { IEditorContributionDescription, EditorExtensionsRegistry, EditorContributionInstantiation, EditorContributionCtor } from '../../../../editor/browser/editorExtensions.js'; +import { ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; +import { IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { FindDecorations } from '../../../../editor/contrib/find/browser/findDecorations.js'; +import { Memento, MementoObject } from '../../../common/memento.js'; +import { Markers } from '../../markers/common/markers.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; +import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; + +export class OutputViewPane extends FilterViewPane { private readonly editor: OutputEditor; private channelId: string | undefined; @@ -46,6 +60,9 @@ export class OutputViewPane extends ViewPane { get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); } set scrollLock(scrollLock: boolean) { this.scrollLockContextKey.set(scrollLock); } + private readonly memento: Memento; + private readonly panelState: MementoObject; + constructor( options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -58,8 +75,32 @@ export class OutputViewPane extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @IHoverService hoverService: IHoverService, + @IOutputService private readonly outputService: IOutputService, + @IStorageService storageService: IStorageService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); + const memento = new Memento(Markers.MARKERS_VIEW_STORAGE_ID, storageService); + const viewState = memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + super({ + ...options, + filterOptions: { + placeholder: localize('outputView.filter.placeholder', "Filter"), + focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key, + text: viewState['filter'] || '', + history: [] + } + }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); + this.memento = memento; + this.panelState = viewState; + + const filters = outputService.filters; + filters.text = this.panelState['filter'] || ''; + filters.trace = this.panelState['showTrace'] ?? true; + filters.debug = this.panelState['showDebug'] ?? true; + filters.info = this.panelState['showInfo'] ?? true; + filters.warning = this.panelState['showWarning'] ?? true; + filters.error = this.panelState['showError'] ?? true; + filters.categories = this.panelState['categories'] ?? ''; + this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService); const editorInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); @@ -69,6 +110,10 @@ export class OutputViewPane extends ViewPane { this.updateActions(); })); this._register(this.onDidChangeBodyVisibility(() => this.onDidChangeVisibility(this.isBodyVisible()))); + this._register(this.filterWidget.onDidChangeFilterText(text => outputService.filters.text = text)); + + this.checkMoreFilters(); + this._register(outputService.filters.onDidChange(() => this.checkMoreFilters())); } showChannel(channel: IOutputChannel, preserveFocus: boolean): void { @@ -85,6 +130,10 @@ export class OutputViewPane extends ViewPane { this.editorPromise?.then(() => this.editor.focus()); } + public clearFilterText(): void { + this.filterWidget.setFilterText(''); + } + protected override renderBody(container: HTMLElement): void { super.renderBody(container); this.editor.create(container); @@ -114,8 +163,7 @@ export class OutputViewPane extends ViewPane { })); } - protected override layoutBody(height: number, width: number): void { - super.layoutBody(height, width); + protected layoutBodyContent(height: number, width: number): void { this.editor.layout(new Dimension(width, height)); } @@ -128,6 +176,7 @@ export class OutputViewPane extends ViewPane { private setInput(channel: IOutputChannel): void { this.channelId = channel.id; + this.checkMoreFilters(); const input = this.createInput(channel); if (!this.editor.input || !input.matches(this.editor.input)) { @@ -138,6 +187,11 @@ export class OutputViewPane extends ViewPane { } + private checkMoreFilters(): void { + const filters = this.outputService.filters; + this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.categories.includes(`,${this.channelId}:`))); + } + private clearInput(): void { this.channelId = undefined; this.editor.clearInput(); @@ -148,9 +202,23 @@ export class OutputViewPane extends ViewPane { return this.instantiationService.createInstance(TextResourceEditorInput, channel.uri, nls.localize('output model title', "{0} - Output", channel.label), nls.localize('channel', "Output channel for '{0}'", channel.label), undefined, undefined); } + override saveState(): void { + const filters = this.outputService.filters; + this.panelState['filter'] = filters.text; + this.panelState['showTrace'] = filters.trace; + this.panelState['showDebug'] = filters.debug; + this.panelState['showInfo'] = filters.info; + this.panelState['showWarning'] = filters.warning; + this.panelState['showError'] = filters.error; + this.panelState['categories'] = filters.categories; + + this.memento.saveMemento(); + super.saveState(); + } + } -class OutputEditor extends AbstractTextResourceEditor { +export class OutputEditor extends AbstractTextResourceEditor { private readonly resourceContext: ResourceContextKey; constructor( @@ -260,4 +328,195 @@ class OutputEditor extends AbstractTextResourceEditor { CONTEXT_IN_OUTPUT.bindTo(scopedContextKeyService).set(true); } } + + private _getContributions(): IEditorContributionDescription[] { + return [ + ...EditorExtensionsRegistry.getEditorContributions(), + { + id: FilterController.ID, + ctor: FilterController as EditorContributionCtor, + instantiation: EditorContributionInstantiation.Eager + } + ]; + } + + protected override getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { contributions: this._getContributions() }; + } + +} + +export class FilterController extends Disposable implements IEditorContribution { + + public static readonly ID = 'output.editor.contrib.filterController'; + + private readonly modelDisposables: DisposableStore = this._register(new DisposableStore()); + private hiddenAreas: Range[] = []; + private readonly categories = new Map(); + private readonly decorationsCollection: IEditorDecorationsCollection; + + constructor( + private readonly editor: ICodeEditor, + @IOutputService private readonly outputService: IOutputService, + ) { + super(); + this.decorationsCollection = editor.createDecorationsCollection(); + this._register(editor.onDidChangeModel(() => this.onDidChangeModel())); + this._register(this.outputService.filters.onDidChange(() => editor.hasModel() && this.filter(editor.getModel()))); + } + + private onDidChangeModel(): void { + this.modelDisposables.clear(); + this.hiddenAreas = []; + this.categories.clear(); + + if (!this.editor.hasModel()) { + return; + } + + const model = this.editor.getModel(); + this.filter(model); + + const computeEndLineNumber = () => { + const endLineNumber = model.getLineCount(); + return endLineNumber > 1 && model.getLineMaxColumn(endLineNumber) === 1 ? endLineNumber - 1 : endLineNumber; + }; + + let endLineNumber = computeEndLineNumber(); + + this.modelDisposables.add(model.onDidChangeContent(e => { + if (e.changes.every(e => e.range.startLineNumber > endLineNumber)) { + this.filterIncremental(model, endLineNumber + 1); + } else { + this.filter(model); + } + endLineNumber = computeEndLineNumber(); + })); + } + + private filter(model: ITextModel): void { + this.hiddenAreas = []; + this.decorationsCollection.clear(); + this.filterIncremental(model, 1); + } + + private filterIncremental(model: ITextModel, fromLineNumber: number): void { + const { findMatches, hiddenAreas, categories: sources } = this.compute(model, fromLineNumber); + this.hiddenAreas.push(...hiddenAreas); + this.editor.setHiddenAreas(this.hiddenAreas, this); + if (findMatches.length) { + this.decorationsCollection.append(findMatches); + } + if (sources.size) { + const that = this; + for (const [categoryFilter, categoryName] of sources) { + if (this.categories.has(categoryFilter)) { + continue; + } + this.categories.set(categoryFilter, categoryName); + this.modelDisposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${categoryFilter}`, + title: categoryName, + toggled: ContextKeyExpr.regex(HIDE_CATEGORY_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(categoryFilter)},.*`)).negate(), + menu: { + id: viewFilterSubmenu, + group: '1_category_filter', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID)), + } + }); + } + async run(): Promise { + that.outputService.filters.toggleCategory(categoryFilter); + } + })); + } + } + } + + private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map } { + const filters = this.outputService.filters; + const activeChannel = this.outputService.getActiveChannel(); + const findMatches: IModelDeltaDecoration[] = []; + const hiddenAreas: Range[] = []; + const categories = new Map(); + + const logEntries = activeChannel?.getLogEntries(); + if (activeChannel && logEntries?.length) { + const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error; + + const fromLogLevelEntryIndex = logEntries.findIndex(entry => fromLineNumber >= entry.range.startLineNumber && fromLineNumber <= entry.range.endLineNumber); + if (fromLogLevelEntryIndex === -1) { + return { findMatches, hiddenAreas, categories }; + } + + for (let i = fromLogLevelEntryIndex; i < logEntries.length; i++) { + const entry = logEntries[i]; + if (entry.category) { + categories.set(`${activeChannel.id}:${entry.category}`, entry.category); + } + if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) { + hiddenAreas.push(entry.range); + continue; + } + if (!this.shouldShowCategory(activeChannel.id, entry, filters)) { + hiddenAreas.push(entry.range); + continue; + } + if (filters.text) { + const matches = model.findMatches(filters.text, entry.range, false, false, null, false); + if (matches.length) { + for (const match of matches) { + findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + } + } else { + hiddenAreas.push(entry.range); + } + } + } + return { findMatches, hiddenAreas, categories }; + } + + if (!filters.text) { + return { findMatches, hiddenAreas, categories }; + } + + const lineCount = model.getLineCount(); + for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) { + const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); + const matches = model.findMatches(filters.text, lineRange, false, false, null, false); + if (matches.length) { + for (const match of matches) { + findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + } + } else { + hiddenAreas.push(lineRange); + } + } + return { findMatches, hiddenAreas, categories }; + } + + private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean { + switch (entry.logLevel) { + case LogLevel.Trace: + return filters.trace; + case LogLevel.Debug: + return filters.debug; + case LogLevel.Info: + return filters.info; + case LogLevel.Warning: + return filters.warning; + case LogLevel.Error: + return filters.error; + } + return true; + } + + private shouldShowCategory(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean { + if (!entry.category) { + return true; + } + return !filters.hasCategory(`${activeChannelId}:${entry.category}`); + } } diff --git a/code/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/code/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 8d813401113..9b1fd05fb69 100644 --- a/code/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/code/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -10,56 +10,168 @@ import { IEditorWorkerService } from '../../../../editor/common/services/editorW import { Emitter, Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { Promises, ThrottledDelayer } from '../../../../base/common/async.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; -import { Disposable, toDisposable, IDisposable, dispose, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../base/common/types.js'; import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; import { Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; +import { ILogger, ILoggerService, ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { OutputChannelUpdateMode } from '../../../services/output/common/output.js'; +import { ILogEntry, IOutputContentSource, LOG_MIME, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; +import { TextModel } from '../../../../editor/common/model/textModel.js'; +import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js'; + +const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\])?/; + +export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { + const lineContent = model.getLineContent(lineNumber); + const match = LOG_ENTRY_REGEX.exec(lineContent); + if (match) { + const timestamp = new Date(match[1]).getTime(); + const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length); + const logLevel = parseLogLevel(match[3]); + const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length); + const category = match[5]; + const startLine = lineNumber; + let endLine = lineNumber; + + const lineCount = model.getLineCount(); + while (endLine < lineCount) { + const nextLineContent = model.getLineContent(endLine + 1); + const isLastLine = endLine + 1 === lineCount && nextLineContent === ''; // Last line will be always empty + if (LOG_ENTRY_REGEX.test(nextLineContent) || isLastLine) { + break; + } + endLine++; + } + const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + return { range, timestamp, timestampRange, logLevel, logLevelRange, category }; + } + return null; +} + +function* logEntryIterator(model: ITextModel, process: (logEntry: ILogEntry) => T): IterableIterator { + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { + const logEntry = parseLogEntryAt(model, lineNumber); + if (logEntry) { + yield process(logEntry); + lineNumber = logEntry.range.endLineNumber; + } + } +} + +function changeStartLineNumber(logEntry: ILogEntry, lineNumber: number): ILogEntry { + return { + ...logEntry, + range: new Range(lineNumber, logEntry.range.startColumn, lineNumber + logEntry.range.endLineNumber - logEntry.range.startLineNumber, logEntry.range.endColumn), + timestampRange: new Range(lineNumber, logEntry.timestampRange.startColumn, lineNumber, logEntry.timestampRange.endColumn), + logLevelRange: new Range(lineNumber, logEntry.logLevelRange.startColumn, lineNumber, logEntry.logLevelRange.endColumn), + }; +} + +function parseLogLevel(level: string): LogLevel { + switch (level.toLowerCase()) { + case 'trace': + return LogLevel.Trace; + case 'debug': + return LogLevel.Debug; + case 'info': + return LogLevel.Info; + case 'warning': + return LogLevel.Warning; + case 'error': + return LogLevel.Error; + default: + throw new Error(`Unknown log level: ${level}`); + } +} export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; + readonly source: IOutputContentSource | ReadonlyArray; + getLogEntries(): ReadonlyArray; append(output: string): void; update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void; + updateChannelSources(sources: ReadonlyArray): void; loadModel(): Promise; clear(): void; replace(value: string): void; } -class OutputFileListener extends Disposable { +interface IContentProvider { + readonly onDidAppend: Event; + readonly onDidReset: Event; + reset(): void; + watch(): void; + unwatch(): void; + getContent(): Promise<{ readonly content: string; readonly consume: () => void }>; + getLogEntries(): ReadonlyArray; +} + +class FileContentProvider extends Disposable implements IContentProvider { - private readonly _onDidContentChange = new Emitter(); - readonly onDidContentChange: Event = this._onDidContentChange.event; + private readonly _onDidAppend = new Emitter(); + readonly onDidAppend = this._onDidAppend.event; + + private readonly _onDidReset = new Emitter(); + readonly onDidReset = this._onDidReset.event; private watching: boolean = false; private syncDelayer: ThrottledDelayer; - private etag: string | undefined; + private etag: string | undefined = ''; + + private logEntries: ILogEntry[] = []; + private startOffset: number = 0; + private endOffset: number = 0; + + readonly resource: URI; + readonly name: string; constructor( - private readonly file: URI, - private readonly fileService: IFileService, - private readonly logService: ILogService + { name, resource }: IOutputContentSource, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, ) { super(); + + this.name = name ?? ''; + this.resource = resource; this.syncDelayer = new ThrottledDelayer(500); + this._register(toDisposable(() => this.unwatch())); + } + + reset(offset?: number): void { + this.endOffset = this.startOffset = offset ?? this.startOffset; + this.logEntries = []; } - watch(eTag: string | undefined): void { + resetToEnd(): void { + this.startOffset = this.endOffset; + this.logEntries = []; + } + + watch(): void { if (!this.watching) { - this.etag = eTag; + this.logService.trace('Started polling', this.resource.toString()); this.poll(); - this.logService.trace('Started polling', this.file.toString()); this.watching = true; } } + unwatch(): void { + if (this.watching) { + this.syncDelayer.cancel(); + this.watching = false; + this.logService.trace('Stopped polling', this.resource.toString()); + } + } + private poll(): void { const loop = () => this.doWatch().then(() => this.poll()); this.syncDelayer.trigger(loop).catch(error => { @@ -70,92 +182,332 @@ class OutputFileListener extends Disposable { } private async doWatch(): Promise { - const stat = await this.fileService.stat(this.file); - if (stat.etag !== this.etag) { - this.etag = stat.etag; - this._onDidContentChange.fire(stat.size); + try { + if (!this.fileService.hasProvider(this.resource)) { + return; + } + const stat = await this.fileService.stat(this.resource); + if (stat.etag !== this.etag) { + this.etag = stat.etag; + if (isNumber(stat.size) && this.endOffset > stat.size) { + this.reset(0); + this._onDidReset.fire(); + } else { + this._onDidAppend.fire(); + } + } + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + throw error; + } + } + } + + getLogEntries(): ReadonlyArray { + return this.logEntries; + } + + async getContent(donotConsumeLogEntries?: boolean): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> { + try { + if (!this.fileService.hasProvider(this.resource)) { + return { + name: this.name, + content: '', + consume: () => { /* No Op */ } + }; + } + const fileContent = await this.fileService.readFile(this.resource, { position: this.endOffset }); + const content = fileContent.value.toString(); + const logEntries = donotConsumeLogEntries ? [] : this.parseLogEntries(content, this.logEntries[this.logEntries.length - 1]); + let consumed = false; + return { + name: this.name, + content, + consume: () => { + if (!consumed) { + consumed = true; + this.endOffset += fileContent.value.byteLength; + this.etag = fileContent.etag; + this.logEntries.push(...logEntries); + } + } + }; + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + throw error; + } + return { + name: this.name, + content: '', + consume: () => { /* No Op */ } + }; + } + } + + private parseLogEntries(content: string, lastLogEntry: ILogEntry | undefined): ILogEntry[] { + const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); + if (!parseLogEntryAt(model, 1)) { + return []; + } + try { + const logEntries: ILogEntry[] = []; + let logEntryStartLineNumber = lastLogEntry ? lastLogEntry.range.endLineNumber + 1 : 1; + for (const entry of logEntryIterator(model, (e) => changeStartLineNumber(e, logEntryStartLineNumber))) { + logEntries.push(entry); + logEntryStartLineNumber = entry.range.endLineNumber + 1; + } + return logEntries; + } finally { + model.dispose(); + } + } +} + +class MultiFileContentProvider extends Disposable implements IContentProvider { + + private readonly _onDidAppend = this._register(new Emitter()); + readonly onDidAppend = this._onDidAppend.event; + readonly onDidReset = Event.None; + + private logEntries: ILogEntry[] = []; + private readonly fileContentProviderItems: [FileContentProvider, DisposableStore][] = []; + + private watching: boolean = false; + + constructor( + filesInfos: IOutputContentSource[], + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + ) { + super(); + for (const file of filesInfos) { + this.fileContentProviderItems.push(this.createFileContentProvider(file)); + } + this._register(toDisposable(() => { + for (const [, disposables] of this.fileContentProviderItems) { + disposables.dispose(); + } + })); + } + + private createFileContentProvider(file: IOutputContentSource): [FileContentProvider, DisposableStore] { + const disposables = new DisposableStore(); + const fileOutput = disposables.add(new FileContentProvider(file, this.fileService, this.instantiationService, this.logService)); + disposables.add(fileOutput.onDidAppend(() => this._onDidAppend.fire())); + return [fileOutput, disposables]; + } + + watch(): void { + if (!this.watching) { + this.watching = true; + for (const [output] of this.fileContentProviderItems) { + output.watch(); + } } } unwatch(): void { if (this.watching) { - this.syncDelayer.cancel(); this.watching = false; - this.logService.trace('Stopped polling', this.file.toString()); + for (const [output] of this.fileContentProviderItems) { + output.unwatch(); + } } } - override dispose(): void { - this.unwatch(); - super.dispose(); + updateFiles(files: IOutputContentSource[]): void { + const wasWatching = this.watching; + if (wasWatching) { + this.unwatch(); + } + + const result = sortedDiff(this.fileContentProviderItems.map(([output]) => output), files, (a, b) => resources.extUri.compare(a.resource, b.resource)); + for (const { start, deleteCount, toInsert } of result) { + const outputs = toInsert.map(file => this.createFileContentProvider(file)); + const outputsToRemove = this.fileContentProviderItems.splice(start, deleteCount, ...outputs); + for (const [, disposables] of outputsToRemove) { + disposables.dispose(); + } + } + + if (wasWatching) { + this.watch(); + } + } + + reset(): void { + for (const [output] of this.fileContentProviderItems) { + output.reset(); + } + this.logEntries = []; + } + + resetToEnd(): void { + for (const [output] of this.fileContentProviderItems) { + output.resetToEnd(); + } + this.logEntries = []; + } + + getLogEntries(): ReadonlyArray { + return this.logEntries; + } + + async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { + const outputs = await Promise.all(this.fileContentProviderItems.map(([output]) => output.getContent(true))); + const { content, logEntries } = this.combineLogEntries(outputs, this.logEntries[this.logEntries.length - 1]); + let consumed = false; + return { + content, + consume: () => { + if (!consumed) { + consumed = true; + outputs.forEach(({ consume }) => consume()); + this.logEntries.push(...logEntries); + } + } + }; + } + + private combineLogEntries(outputs: { content: string; name: string }[], lastEntry: ILogEntry | undefined): { logEntries: ILogEntry[]; content: string } { + + outputs = outputs.filter(output => !!output.content); + + if (outputs.length === 0) { + return { logEntries: [], content: '' }; + } + + const logEntries: ILogEntry[] = []; + const contents: string[] = []; + const process = (model: ITextModel, logEntry: ILogEntry, name: string): [ILogEntry, string] => { + const lineContent = model.getValueInRange(logEntry.range); + const content = name ? `${lineContent.substring(0, logEntry.logLevelRange.endColumn)} [${name}]${lineContent.substring(logEntry.logLevelRange.endColumn)}` : lineContent; + return [{ + ...logEntry, + category: name, + range: new Range(logEntry.range.startLineNumber, logEntry.logLevelRange.startColumn, logEntry.range.endLineNumber, name ? logEntry.range.endColumn + name.length + 3 : logEntry.range.endColumn), + }, content]; + }; + + const model = this.instantiationService.createInstance(TextModel, outputs[0].content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); + try { + for (const [logEntry, content] of logEntryIterator(model, (e) => process(model, e, outputs[0].name))) { + logEntries.push(logEntry); + contents.push(content); + } + } finally { + model.dispose(); + } + + for (let index = 1; index < outputs.length; index++) { + const { content, name } = outputs[index]; + const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); + try { + const iterator = logEntryIterator(model, (e) => process(model, e, name)); + let next = iterator.next(); + while (!next.done) { + const [logEntry, content] = next.value; + const logEntriesToAdd = [logEntry]; + const contentsToAdd = [content]; + + let insertionIndex; + + // If the timestamp is greater than or equal to the last timestamp, + // we can just append all the entries at the end + if (logEntry.timestamp >= logEntries[logEntries.length - 1].timestamp) { + insertionIndex = logEntries.length; + for (next = iterator.next(); !next.done; next = iterator.next()) { + logEntriesToAdd.push(next.value[0]); + contentsToAdd.push(next.value[1]); + } + } + else { + if (logEntry.timestamp <= logEntries[0].timestamp) { + // If the timestamp is less than or equal to the first timestamp + // then insert at the beginning + insertionIndex = 0; + } else { + // Otherwise, find the insertion index + const idx = binarySearch(logEntries, logEntry, (a, b) => a.timestamp - b.timestamp); + insertionIndex = idx < 0 ? ~idx : idx; + } + + // Collect all entries that have a timestamp less than or equal to the timestamp at the insertion index + for (next = iterator.next(); !next.done && next.value[0].timestamp <= logEntries[insertionIndex].timestamp; next = iterator.next()) { + logEntriesToAdd.push(next.value[0]); + contentsToAdd.push(next.value[1]); + } + } + + contents.splice(insertionIndex, 0, ...contentsToAdd); + logEntries.splice(insertionIndex, 0, ...logEntriesToAdd); + } + } finally { + model.dispose(); + } + } + + let content = ''; + const updatedLogEntries: ILogEntry[] = []; + let logEntryStartLineNumber = lastEntry ? lastEntry.range.endLineNumber + 1 : 1; + for (let i = 0; i < logEntries.length; i++) { + content += contents[i] + '\n'; + const updatedLogEntry = changeStartLineNumber(logEntries[i], logEntryStartLineNumber); + updatedLogEntries.push(updatedLogEntry); + logEntryStartLineNumber = updatedLogEntry.range.endLineNumber + 1; + } + + return { logEntries: updatedLogEntries, content }; } + } -export class FileOutputChannelModel extends Disposable implements IOutputChannelModel { +export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel { private readonly _onDispose = this._register(new Emitter()); readonly onDispose: Event = this._onDispose.event; - private readonly fileHandler: OutputFileListener; - private etag: string | undefined = ''; + protected loadModelPromise: Promise | null = null; - private loadModelPromise: Promise | null = null; - private model: ITextModel | null = null; + private readonly modelDisposable = this._register(new MutableDisposable()); + protected model: ITextModel | null = null; private modelUpdateInProgress: boolean = false; private readonly modelUpdateCancellationSource = this._register(new MutableDisposable()); private readonly appendThrottler = this._register(new ThrottledDelayer(300)); private replacePromise: Promise | undefined; - private startOffset: number = 0; - private endOffset: number = 0; + abstract readonly source: IOutputContentSource | ReadonlyArray; constructor( private readonly modelUri: URI, private readonly language: ILanguageSelection, - private readonly file: URI, - @IFileService private readonly fileService: IFileService, - @IModelService private readonly modelService: IModelService, - @ILogService logService: ILogService, + private readonly outputContentProvider: IContentProvider, + @IModelService protected readonly modelService: IModelService, @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, ) { super(); - - this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService, logService)); - this._register(this.fileHandler.onDidContentChange(size => this.onDidContentChange(size))); - this._register(toDisposable(() => this.fileHandler.unwatch())); - } - - append(message: string): void { - throw new Error('Not supported'); - } - - replace(message: string): void { - throw new Error('Not supported'); - } - - clear(): void { - this.update(OutputChannelUpdateMode.Clear, this.endOffset, true); } - update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { - const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); - loadModelPromise.then(() => this.doUpdate(mode, till, immediate)); - } - - loadModel(): Promise { + async loadModel(): Promise { this.loadModelPromise = Promises.withAsyncBody(async (c, e) => { try { - let content = ''; - if (await this.fileService.exists(this.file)) { - const fileContent = await this.fileService.readFile(this.file, { position: this.startOffset }); - this.endOffset = this.startOffset + fileContent.value.byteLength; - this.etag = fileContent.etag; - content = fileContent.value.toString(); - } else { - this.startOffset = 0; - this.endOffset = 0; - } - c(this.createModel(content)); + this.modelDisposable.value = new DisposableStore(); + this.model = this.modelService.createModel('', this.language, this.modelUri); + const { content, consume } = await this.outputContentProvider.getContent(); + consume(); + this.doAppendContent(this.model, content); + this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true))); + this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false))); + this.outputContentProvider.watch(); + this.modelDisposable.value.add(toDisposable(() => this.outputContentProvider.unwatch())); + this.modelDisposable.value.add(this.model.onWillDispose(() => { + this.outputContentProvider.reset(); + this.modelDisposable.value = undefined; + this.cancelModelUpdate(); + this.model = null; + })); + c(this.model); } catch (error) { e(error); } @@ -163,25 +515,19 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel return this.loadModelPromise; } - private createModel(content: string): ITextModel { - if (this.model) { - this.model.setValue(content); - } else { - this.model = this.modelService.createModel(content, this.language, this.modelUri); - this.fileHandler.watch(this.etag); - const disposable = this.model.onWillDispose(() => { - this.cancelModelUpdate(); - this.fileHandler.unwatch(); - this.model = null; - dispose(disposable); - }); + getLogEntries(): readonly ILogEntry[] { + return this.outputContentProvider.getLogEntries(); + } + + private onDidContentChange(reset: boolean, appendImmediately: boolean): void { + if (reset && !this.modelUpdateInProgress) { + this.doUpdate(OutputChannelUpdateMode.Clear, true); } - return this.model; + this.doUpdate(OutputChannelUpdateMode.Append, appendImmediately); } - private doUpdate(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { + protected doUpdate(mode: OutputChannelUpdateMode, immediate: boolean): void { if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { - this.startOffset = this.endOffset = isNumber(till) ? till : this.endOffset; this.cancelModelUpdate(); } if (!this.model) { @@ -208,7 +554,8 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel } private clearContent(model: ITextModel): void { - this.doUpdateModel(model, [EditOperation.delete(model.getFullModelRange())], VSBuffer.fromString('')); + model.applyEdits([EditOperation.delete(model.getFullModelRange())]); + this.modelUpdateInProgress = false; } private appendContent(model: ITextModel, immediate: boolean, token: CancellationToken): void { @@ -228,17 +575,16 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel } /* Get content to append */ - const contentToAppend = await this.getContentToUpdate(); + const { content, consume } = await this.outputContentProvider.getContent(); /* Abort if operation is cancelled */ if (token.isCancellationRequested) { return; } /* Appned Content */ - const lastLine = model.getLineCount(); - const lastLineMaxColumn = model.getLineMaxColumn(lastLine); - const edits = [EditOperation.insert(new Position(lastLine, lastLineMaxColumn), contentToAppend.toString())]; - this.doUpdateModel(model, edits, contentToAppend); + consume(); + this.doAppendContent(model, content); + this.modelUpdateInProgress = false; }, immediate ? 0 : undefined).catch(error => { if (!isCancellationError(error)) { throw error; @@ -246,23 +592,33 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel }); } + private doAppendContent(model: ITextModel, content: string): void { + const lastLine = model.getLineCount(); + const lastLineMaxColumn = model.getLineMaxColumn(lastLine); + model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), content)]); + } + private async replaceContent(model: ITextModel, token: CancellationToken): Promise { /* Get content to replace */ - const contentToReplace = await this.getContentToUpdate(); + const { content, consume } = await this.outputContentProvider.getContent(); /* Abort if operation is cancelled */ if (token.isCancellationRequested) { return; } /* Compute Edits */ - const edits = await this.getReplaceEdits(model, contentToReplace.toString()); + const edits = await this.getReplaceEdits(model, content.toString()); /* Abort if operation is cancelled */ if (token.isCancellationRequested) { return; } - /* Apply Edits */ - this.doUpdateModel(model, edits, contentToReplace); + consume(); + if (edits.length) { + /* Apply Edits */ + model.applyEdits(edits); + } + this.modelUpdateInProgress = false; } private async getReplaceEdits(model: ITextModel, contentToReplace: string): Promise { @@ -278,14 +634,6 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel return []; } - private doUpdateModel(model: ITextModel, edits: ISingleEditOperation[], content: VSBuffer): void { - if (edits.length) { - model.applyEdits(edits); - } - this.endOffset = this.endOffset + content.byteLength; - this.modelUpdateInProgress = false; - } - protected cancelModelUpdate(): void { this.modelUpdateCancellationSource.value?.cancel(); this.modelUpdateCancellationSource.value = undefined; @@ -294,32 +642,101 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel this.modelUpdateInProgress = false; } - private async getContentToUpdate(): Promise { - const content = await this.fileService.readFile(this.file, { position: this.endOffset }); - this.etag = content.etag; - return content.value; + protected isVisible(): boolean { + return !!this.model; + } + + override dispose(): void { + this._onDispose.fire(); + super.dispose(); + } + + append(message: string): void { throw new Error('Not supported'); } + replace(message: string): void { throw new Error('Not supported'); } + + abstract clear(): void; + abstract update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void; + abstract updateChannelSources(files: IOutputContentSource[]): void; +} + +export class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { + + private readonly fileOutput: FileContentProvider; + + constructor( + modelUri: URI, + language: ILanguageSelection, + readonly source: IOutputContentSource, + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + @IInstantiationService instantiationService: IInstantiationService, + @ILogService logService: ILogService, + @IEditorWorkerService editorWorkerService: IEditorWorkerService, + ) { + const fileOutput = new FileContentProvider(source, fileService, instantiationService, logService); + super(modelUri, language, fileOutput, modelService, editorWorkerService); + this.fileOutput = this._register(fileOutput); + } + + override clear(): void { + this.update(OutputChannelUpdateMode.Clear, undefined, true); } - private onDidContentChange(size: number | undefined): void { - if (this.model) { - if (!this.modelUpdateInProgress) { - if (isNumber(size) && this.endOffset > size) { - // Reset - Content is removed - this.update(OutputChannelUpdateMode.Clear, 0, true); + override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { + const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); + loadModelPromise.then(() => { + if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { + if (isNumber(till)) { + this.fileOutput.reset(till); + } else { + this.fileOutput.resetToEnd(); } } - this.update(OutputChannelUpdateMode.Append, undefined, false /* Not needed to update immediately. Wait to collect more changes and update. */); - } + this.doUpdate(mode, immediate); + }); } - protected isVisible(): boolean { - return !!this.model; + override updateChannelSources(files: IOutputContentSource[]): void { throw new Error('Not supported'); } +} + +export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { + + private readonly multifileOutput: MultiFileContentProvider; + + constructor( + modelUri: URI, + language: ILanguageSelection, + readonly source: IOutputContentSource[], + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + @ILogService logService: ILogService, + @IEditorWorkerService editorWorkerService: IEditorWorkerService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + const multifileOutput = new MultiFileContentProvider(source, instantiationService, fileService, logService); + super(modelUri, language, multifileOutput, modelService, editorWorkerService); + this.multifileOutput = this._register(multifileOutput); } - override dispose(): void { - this._onDispose.fire(); - super.dispose(); + override updateChannelSources(files: IOutputContentSource[]): void { + this.multifileOutput.unwatch(); + this.multifileOutput.updateFiles(files); + this.multifileOutput.reset(); + this.doUpdate(OutputChannelUpdateMode.Replace, true); + if (this.isVisible()) { + this.multifileOutput.watch(); + } + } + + override clear(): void { + const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); + loadModelPromise.then(() => { + this.multifileOutput.resetToEnd(); + this.doUpdate(OutputChannelUpdateMode.Clear, true); + }); } + + override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { throw new Error('Not supported'); } } class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutputChannelModel { @@ -335,10 +752,11 @@ class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutpu @IFileService fileService: IFileService, @IModelService modelService: IModelService, @ILoggerService loggerService: ILoggerService, + @IInstantiationService instantiationService: IInstantiationService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService ) { - super(modelUri, language, file, fileService, modelService, logService, editorWorkerService); + super(modelUri, language, { resource: file, name: '' }, fileService, modelService, instantiationService, logService, editorWorkerService); // Donot rotate to check for the file reset this.logger = loggerService.createLogger(file, { logLevel: 'always', donotRotate: true, donotUseFormatters: true, hidden: true }); @@ -372,21 +790,25 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh readonly onDispose: Event = this._onDispose.event; private readonly outputChannelModel: Promise; + readonly source: IOutputContentSource; constructor( id: string, modelUri: URI, language: ILanguageSelection, - outputDir: Promise, + outputDir: URI, + outputDirCreationPromise: Promise, @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, ) { super(); - this.outputChannelModel = this.createOutputChannelModel(id, modelUri, language, outputDir); + this.outputChannelModel = this.createOutputChannelModel(id, modelUri, language, outputDir, outputDirCreationPromise); + const resource = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); + this.source = { resource }; } - private async createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, outputDirPromise: Promise): Promise { - const outputDir = await outputDirPromise; + private async createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, outputDir: URI, outputDirPromise: Promise): Promise { + await outputDirPromise; const file = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); await this.fileService.createFile(file); const outputChannelModel = this._register(this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, language, file)); @@ -394,6 +816,10 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh return outputChannelModel; } + getLogEntries(): readonly ILogEntry[] { + return []; + } + append(output: string): void { this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output)); } @@ -413,4 +839,8 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh replace(value: string): void { this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value)); } + + updateChannelSources(files: IOutputContentSource[]): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.updateChannelSources(files)); + } } diff --git a/code/src/vs/workbench/contrib/output/common/outputChannelModelService.ts b/code/src/vs/workbench/contrib/output/common/outputChannelModelService.ts deleted file mode 100644 index 3f500babda6..00000000000 --- a/code/src/vs/workbench/contrib/output/common/outputChannelModelService.ts +++ /dev/null @@ -1,53 +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 { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { toLocalISOString } from '../../../../base/common/date.js'; -import { joinPath } from '../../../../base/common/resources.js'; -import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel } from './outputChannelModel.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; - -export const IOutputChannelModelService = createDecorator('outputChannelModelService'); - -export interface IOutputChannelModelService { - readonly _serviceBrand: undefined; - - createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, file?: URI): IOutputChannelModel; - -} - -export class OutputChannelModelService { - - declare readonly _serviceBrand: undefined; - - private readonly outputLocation: URI; - - constructor( - @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService - ) { - this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); - } - - createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, file?: URI): IOutputChannelModel { - return file ? this.instantiationService.createInstance(FileOutputChannelModel, modelUri, language, file) : this.instantiationService.createInstance(DelegatedOutputChannelModel, id, modelUri, language, this.outputDir); - } - - private _outputDir: Promise | null = null; - private get outputDir(): Promise { - if (!this._outputDir) { - this._outputDir = this.fileService.createFolder(this.outputLocation).then(() => this.outputLocation); - } - return this._outputDir; - } - -} - -registerSingleton(IOutputChannelModelService, OutputChannelModelService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts b/code/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts deleted file mode 100644 index 7305828c10f..00000000000 --- a/code/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.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. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../base/common/codicons.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../nls.js'; -import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IsMacNativeContext } from '../../../../platform/contextkey/common/contextkeys.js'; -import { INativeHostService } from '../../../../platform/native/common/native.js'; -import { OUTPUT_VIEW_ID, CONTEXT_ACTIVE_FILE_OUTPUT, IOutputService } from '../../../services/output/common/output.js'; - - -registerAction2(class OpenInConsoleAction extends Action2 { - constructor() { - super({ - id: `workbench.action.openActiveLogOutputFileNative`, - title: localize2('openActiveOutputFileNative', "Open Output in Console"), - menu: [{ - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), IsMacNativeContext), - group: 'navigation', - order: 6, - isHiddenByDefault: true - }], - icon: Codicon.goToFile, - precondition: ContextKeyExpr.and(CONTEXT_ACTIVE_FILE_OUTPUT, IsMacNativeContext) - }); - } - - async run(accessor: ServicesAccessor): Promise { - const outputService = accessor.get(IOutputService); - const hostService = accessor.get(INativeHostService); - const channel = outputService.getActiveChannel(); - if (!channel) { - return; - } - const descriptor = outputService.getChannelDescriptors().find(c => c.id === channel.id); - if (descriptor?.file && descriptor.file.scheme === Schemas.file) { - hostService.openExternal(descriptor.file.toString(true), 'open'); - } - } -}); diff --git a/code/src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts b/code/src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts new file mode 100644 index 00000000000..94e98d07969 --- /dev/null +++ b/code/src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { parseLogEntryAt } from '../../common/outputChannelModel.js'; +import { TextModel } from '../../../../../editor/common/model/textModel.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { LogLevel } from '../../../../../platform/log/common/log.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; + +suite('Logs Parsing', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = disposables.add(workbenchInstantiationService({}, disposables)); + }); + + test('should parse log entry with all components', () => { + const text = '2023-10-15 14:30:45.123 [info] [Git] Initializing repository'; + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Info); + assert.strictEqual(entry?.category, 'Git'); + assert.strictEqual(model.getValueInRange(entry?.range), text); + }); + + test('should parse multi-line log entry', () => { + const text = [ + '2023-10-15 14:30:45.123 [error] [Extension] Failed with error:', + 'Error: Could not load extension', + ' at Object.load (/path/to/file:10:5)' + ].join('\n'); + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Error); + assert.strictEqual(entry?.category, 'Extension'); + assert.strictEqual(model.getValueInRange(entry?.range), text); + }); + + test('should parse log entry without category', () => { + const text = '2023-10-15 14:30:45.123 [warning] System is running low on memory'; + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Warning); + assert.strictEqual(entry?.category, undefined); + assert.strictEqual(model.getValueInRange(entry?.range), text); + }); + + test('should return null for invalid log entry', () => { + const model = createModel('Not a valid log entry'); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry, null); + }); + + test('should parse all supported log levels', () => { + const levels = { + info: LogLevel.Info, + trace: LogLevel.Trace, + debug: LogLevel.Debug, + warning: LogLevel.Warning, + error: LogLevel.Error + }; + + for (const [levelText, expectedLevel] of Object.entries(levels)) { + const model = createModel(`2023-10-15 14:30:45.123 [${levelText}] Test message`); + const entry = parseLogEntryAt(model, 1); + assert.strictEqual(entry?.logLevel, expectedLevel, `Failed for log level: ${levelText}`); + } + }); + + test('should parse timestamp correctly', () => { + const timestamps = [ + '2023-01-01 00:00:00.000', + '2023-12-31 23:59:59.999', + '2023-06-15 12:30:45.500' + ]; + + for (const timestamp of timestamps) { + const model = createModel(`${timestamp} [info] Test message`); + const entry = parseLogEntryAt(model, 1); + assert.strictEqual(entry?.timestamp, new Date(timestamp).getTime(), `Failed for timestamp: ${timestamp}`); + } + }); + + test('should handle last line of file', () => { + const model = createModel([ + '2023-10-15 14:30:45.123 [info] First message', + '2023-10-15 14:30:45.124 [info] Last message', + '' + ].join('\n')); + + let actual = parseLogEntryAt(model, 1); + assert.strictEqual(actual?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(actual?.logLevel, LogLevel.Info); + assert.strictEqual(actual?.category, undefined); + assert.strictEqual(model.getValueInRange(actual?.range), '2023-10-15 14:30:45.123 [info] First message'); + + actual = parseLogEntryAt(model, 2); + assert.strictEqual(actual?.timestamp, new Date('2023-10-15 14:30:45.124').getTime()); + assert.strictEqual(actual?.logLevel, LogLevel.Info); + assert.strictEqual(actual?.category, undefined); + assert.strictEqual(model.getValueInRange(actual?.range), '2023-10-15 14:30:45.124 [info] Last message'); + + actual = parseLogEntryAt(model, 3); + assert.strictEqual(actual, null); + }); + + test('should parse multi-line log entry with empty lines', () => { + const text = [ + '2025-01-27 09:53:00.450 [info] Found with version <20.18.1>', + 'Now using node v20.18.1 (npm v10.8.2)', + '', + '> husky - npm run -s precommit', + '> husky - node v20.18.1', + '', + 'Reading git index versions...' + ].join('\n'); + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2025-01-27 09:53:00.450').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Info); + assert.strictEqual(entry?.category, undefined); + assert.strictEqual(model.getValueInRange(entry?.range), text); + + }); + + function createModel(content: string): TextModel { + return disposables.add(instantiationService.createInstance(TextModel, content, 'log', TextModel.DEFAULT_CREATION_OPTIONS, null)); + } +}); diff --git a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 3c6e11132b3..f5f2925dd5c 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -365,7 +365,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP ariaLabelledBy: 'keybindings-editor-aria-label-element', recordEnter: true, quoteRecordedKeys: true, - history: this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] || [], + history: new Set(this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] ?? []), inputBoxStyles: getInputBoxStyle({ inputBorder: settingsTextInputBorder }) diff --git a/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 89ba7579904..088783e156c 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -72,6 +72,7 @@ display: inline-block; line-height: 24px; min-height: 24px; + flex: none; } /* Use monospace to display glob patterns in include/exclude widget */ diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 4af21a7e964..46fa025a87e 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -19,6 +19,7 @@ import { MenuId, MenuRegistry, isIMenuItem } from '../../../../platform/actions/ import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { isLocalizedString } from '../../../../platform/action/common/action.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { KeybindingsRegistry } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; export class ConfigureLanguageBasedSettingsAction extends Action { @@ -119,6 +120,27 @@ CommandsRegistry.registerCommand('_getAllCommands', function (accessor, filterBy }); } } + for (const command of KeybindingsRegistry.getDefaultKeybindings()) { + if (filterByPrecondition && !contextKeyService.contextMatchesRules(command.when ?? undefined)) { + continue; + } + + const keybinding = keybindingService.lookupKeybinding(command.command ?? ''); + if (!keybinding) { + continue; + } + + if (actions.some(a => a.command === command.command)) { + continue; + } + actions.push({ + command: command.command ?? '', + label: command.command ?? '', + keybinding: keybinding?.getLabel() ?? 'Not set', + precondition: command.when?.serialize() + }); + } + return actions; }); //#endregion diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 4c3a8c71dd6..ce8b0477006 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -40,7 +40,7 @@ import { IWorkspaceTrustManagementService } from '../../../../platform/workspace import { RangeHighlightDecorations } from '../../../browser/codeeditor.js'; import { settingsEditIcon } from './preferencesIcons.js'; import { EditPreferenceWidget } from './preferencesWidgets.js'; -import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorModel, ISettingsGroup } from '../../../services/preferences/common/preferences.js'; import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from '../../../services/preferences/common/preferencesModels.js'; @@ -623,7 +623,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc message: nls.localize('defaultProfileSettingWhileNonDefaultActive', "This setting cannot be applied while a non-default profile is active. It will be applied when the default profile is active.") }); } else if (isEqual(this.userDataProfileService.currentProfile.settingsResource, this.settingsEditorModel.uri)) { - if (configuration.scope === ConfigurationScope.APPLICATION) { + if (configuration.scope && APPLICATION_SCOPES.includes(configuration.scope)) { // If we're in a profile setting file, and the setting is application-scoped, fade it out. markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); } else if (this.configurationService.isSettingAppliedForAllProfiles(setting.key)) { @@ -637,7 +637,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } } } - if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { + if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.APPLICATION_MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { markerData.push({ severity: MarkerSeverity.Hint, tags: [MarkerTag.Unnecessary], @@ -654,7 +654,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } private handleWorkspaceConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { - if (configuration.scope === ConfigurationScope.APPLICATION) { + if (configuration.scope && APPLICATION_SCOPES.includes(configuration.scope)) { markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); } @@ -671,7 +671,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } private handleWorkspaceFolderConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { - if (configuration.scope === ConfigurationScope.APPLICATION) { + if (configuration.scope && APPLICATION_SCOPES.includes(configuration.scope)) { markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); } diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index e81c8009f79..375b0121941 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -7,7 +7,7 @@ import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFi import { IRange } from '../../../../editor/common/core/range.js'; import { distinct } from '../../../../base/common/arrays.js'; import * as strings from '../../../../base/common/strings.js'; -import { IMatch, matchesContiguousSubString, matchesWords } from '../../../../base/common/filters.js'; +import { IMatch, matchesContiguousSubString, matchesSubString, matchesWords } from '../../../../base/common/filters.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration } from '../common/preferences.js'; @@ -99,8 +99,8 @@ export class LocalSearchProvider implements ISearchProvider { let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable const settingMatcher = (setting: ISetting) => { - const { matches, matchType } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); - const score = this._filter === setting.key ? + const { matches, matchType, keyMatchScore } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); + const score = strings.equalsIgnoreCase(this._filter, setting.key) ? LocalSearchProvider.EXACT_MATCH_SCORE : orderedScore--; @@ -108,6 +108,7 @@ export class LocalSearchProvider implements ISearchProvider { { matches, matchType, + keyMatchScore, score } : null; @@ -137,7 +138,14 @@ export class LocalSearchProvider implements ISearchProvider { export class SettingMatches { readonly matches: IRange[]; + /** Whether to use the new key matching search algorithm that calculates more weights for each result */ + useNewKeyMatchingSearch: boolean = false; matchType: SettingMatchType = SettingMatchType.None; + /** + * A match score for key matches to allow comparing key matches against each other. + * Otherwise, all key matches are treated the same, and sorting is done by ToC order. + */ + keyMatchScore: number = 0; constructor( searchString: string, @@ -147,6 +155,7 @@ export class SettingMatches { valuesMatcher: (filter: string, setting: ISetting) => IRange[], @IConfigurationService private readonly configurationService: IConfigurationService ) { + this.useNewKeyMatchingSearch = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true; this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); } @@ -170,29 +179,49 @@ export class SettingMatches { const keyMatchingWords: Map = new Map(); const valueMatchingWords: Map = new Map(); - const words = new Set(searchString.split(' ')); - // Key search const settingKeyAsWords: string = this._keyToLabel(setting.key); - for (const word of words) { + const queryWords = new Set(searchString.split(' ')); + for (const word of queryWords) { // Check if the key contains the word. - const keyMatches = matchesWords(word, settingKeyAsWords, true); + // Force contiguous matching iff we're using the new algorithm. + const keyMatches = matchesWords(word, settingKeyAsWords, this.useNewKeyMatchingSearch); if (keyMatches?.length) { keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - // For now, only allow a match if all words match in the key. - if (keyMatchingWords.size === words.size) { - this.matchType |= SettingMatchType.KeyMatch; + if (this.useNewKeyMatchingSearch) { + if (keyMatchingWords.size === queryWords.size) { + // All words in the query matched with something in the setting key. + this.matchType |= SettingMatchType.KeyMatch; + // Score based on how many words matched out of the entire key, penalizing longer setting names. + const settingKeyAsWordsCount = settingKeyAsWords.split(' ').length; + this.keyMatchScore = (keyMatchingWords.size / settingKeyAsWordsCount) + (1 / setting.key.length); + } + const keyMatches = matchesSubString(searchString, settingKeyAsWords); + if (keyMatches?.length) { + // Handles cases such as "editor formonpast" with missing letters. + keyMatchingWords.set(searchString, keyMatches.map(match => this.toKeyRange(setting, match))); + this.matchType |= SettingMatchType.KeyMatch; + this.keyMatchScore = keyMatchingWords.size; + } } else { - keyMatchingWords.clear(); + // Fall back to the old algorithm. + if (keyMatchingWords.size) { + this.matchType |= SettingMatchType.KeyMatch; + this.keyMatchScore = keyMatchingWords.size; + } } - - // Also check if the user tried searching by id. const keyIdMatches = matchesContiguousSubString(searchString, setting.key); if (keyIdMatches?.length) { + // Handles cases such as "editor.formatonpaste" where the user tries searching for the ID. keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.KeyMatch; + if (this.useNewKeyMatchingSearch) { + this.matchType |= SettingMatchType.KeyMatch; + this.keyMatchScore = Math.max(this.keyMatchScore, searchString.length / setting.key.length); + } else { + this.matchType |= SettingMatchType.KeyIdMatch; + } } // Check if the match was for a language tag group setting such as [markdown]. @@ -204,9 +233,16 @@ export class SettingMatches { return [...keyRanges]; } + // New algorithm only: exit early if the key already matched. + if (this.useNewKeyMatchingSearch && (this.matchType & SettingMatchType.KeyMatch)) { + const keyRanges = keyMatchingWords.size ? + Array.from(keyMatchingWords.values()).flat() : []; + return [...keyRanges]; + } + // Description search - if (this.searchDescription) { - for (const word of words) { + if (this.searchDescription && this.matchType !== SettingMatchType.None) { + for (const word of queryWords) { // Search the description lines. for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { const descriptionMatches = matchesContiguousSubString(word, setting.description[lineIndex]); @@ -215,7 +251,7 @@ export class SettingMatches { } } } - if (descriptionMatchingWords.size === words.size) { + if (descriptionMatchingWords.size === queryWords.size) { this.matchType |= SettingMatchType.DescriptionOrValueMatch; } else { // Clear out the match for now. We want to require all words to match in the description. @@ -232,13 +268,13 @@ export class SettingMatches { continue; } valueMatchingWords.clear(); - for (const word of words) { + for (const word of queryWords) { const valueMatches = matchesContiguousSubString(word, option); if (valueMatches?.length) { valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); } } - if (valueMatchingWords.size === words.size) { + if (valueMatchingWords.size === queryWords.size) { this.matchType |= SettingMatchType.DescriptionOrValueMatch; break; } else { @@ -250,13 +286,13 @@ export class SettingMatches { // Search single string value. const settingValue = this.configurationService.getValue(setting.key); if (typeof settingValue === 'string') { - for (const word of words) { + for (const word of queryWords) { const valueMatches = matchesContiguousSubString(word, settingValue); if (valueMatches?.length) { valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); } } - if (valueMatchingWords.size === words.size) { + if (valueMatchingWords.size === queryWords.size) { this.matchType |= SettingMatchType.DescriptionOrValueMatch; } else { // Clear out the match for now. We want to require all words to match in the value. @@ -406,6 +442,7 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { setting: settingsRecord[pick], matches: [settingsRecord[pick].range], matchType: SettingMatchType.RemoteMatch, + keyMatchScore: 0, score: info.weight }); } @@ -501,6 +538,7 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { setting: this._settingsRecord[pick], matches: [this._settingsRecord[pick].range], matchType: SettingMatchType.RemoteMatch, + keyMatchScore: 0, score: info.score }); } diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 43214c91777..48f25cff467 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -50,7 +50,7 @@ import { Settings2EditorModel, nullRange } from '../../../services/preferences/c import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js'; import { preferencesClearInputIcon, preferencesFilterIcon } from './preferencesIcons.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js'; @@ -182,7 +182,6 @@ export class SettingsEditor2 extends EditorPane { private tocTreeContainer!: HTMLElement; private tocTree!: TOCTree; - private delayedFilterLogging: Delayer; private searchDelayer: Delayer; private searchInProgress: CancellationTokenSource | null = null; @@ -252,7 +251,6 @@ export class SettingsEditor2 extends EditorPane { @IUserDataProfileService userDataProfileService: IUserDataProfileService, ) { super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); - this.delayedFilterLogging = new Delayer(1000); this.searchDelayer = new Delayer(300); this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL }; @@ -541,6 +539,7 @@ export class SettingsEditor2 extends EditorPane { // Wait for editor to be removed from DOM #106303 setTimeout(() => { this.searchWidget.onHide(); + this.settingRenderers.cancelSuggesters(); }, 0); } } @@ -794,7 +793,7 @@ export class SettingsEditor2 extends EditorPane { if (options?.revealSetting) { const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); const configurationScope = configurationProperties[options?.revealSetting.key]?.scope; - if (configurationScope === ConfigurationScope.APPLICATION) { + if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) { return this.preferencesService.openApplicationSettings(openOptions); } } @@ -1159,6 +1158,13 @@ export class SettingsEditor2 extends EditorPane { this.refreshTOCTree(); } this.renderTree(key, isManualReset); + this.pendingSettingUpdate = null; + + // Only log 1% of modification events to reduce the volume of data + if (Math.random() >= 0.01) { + return; + } + const reportModifiedProps = { key, query, @@ -1168,8 +1174,6 @@ export class SettingsEditor2 extends EditorPane { isReset: typeof value === 'undefined', settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget }; - - this.pendingSettingUpdate = null; return this.reportModifiedSetting(reportModifiedProps); }); } @@ -1572,12 +1576,7 @@ export class SettingsEditor2 extends EditorPane { const query = this.searchWidget.getValue().trim(); this.viewState.query = query; - this.delayedFilterLogging.cancel(); await this.triggerSearch(query.replace(/\u203A/g, ' ')); - - if (query && this.searchResultModel) { - this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(this.searchResultModel)); - } } private parseSettingFromJSON(query: string): string | null { @@ -1598,10 +1597,7 @@ export class SettingsEditor2 extends EditorPane { separatorBorder: Color.transparent }); } else { - this.splitView.setViewVisible(0, true); - this.splitView.style({ - separatorBorder: this.theme.getColor(settingsSashBorder)! - }); + this.layoutSplitView(this.dimension); } } @@ -1660,8 +1656,7 @@ export class SettingsEditor2 extends EditorPane { this.refreshTOCTree(); this.renderResultCountMessages(); this.refreshTree(); - // Always show the ToC when leaving search mode - this.splitView.setViewVisible(0, true); + this.layoutSplitView(this.dimension); } } progressRunner.done(); @@ -1679,7 +1674,7 @@ export class SettingsEditor2 extends EditorPane { for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) { for (const sect of g.sections) { for (const setting of sect.settings) { - fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, score: 0 }); + fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, keyMatchScore: 0, score: 0 }); } } } @@ -1688,44 +1683,6 @@ export class SettingsEditor2 extends EditorPane { return filterModel; } - private reportFilteringUsed(searchResultModel: SearchResultModel | null): void { - if (!searchResultModel) { - return; - } - - type SettingsEditorFilterEvent = { - 'counts.nlpResult': number | undefined; - 'counts.filterResult': number | undefined; - 'counts.uniqueResultsCount': number | undefined; - }; - type SettingsEditorFilterClassification = { - 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; - 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'comment': 'The number of matches found by the local search provider, if applicable.' }; - 'counts.uniqueResultsCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'comment': 'The number of unique matches over both search providers, if applicable.' }; - owner: 'rzhao271'; - comment: 'Tracks the performance of the built-in search providers'; - }; - // Count unique results - const counts: { nlpResult?: number; filterResult?: number } = {}; - const rawResults = searchResultModel.getRawResults(); - const filterResult = rawResults[SearchResultIdx.Local]; - if (filterResult) { - counts['filterResult'] = filterResult.filterMatches.length; - } - const nlpResult = rawResults[SearchResultIdx.Remote]; - if (nlpResult) { - counts['nlpResult'] = nlpResult.filterMatches.length; - } - - const uniqueResults = searchResultModel.getUniqueResults(); - const data = { - 'counts.nlpResult': counts['nlpResult'], - 'counts.filterResult': counts['filterResult'], - 'counts.uniqueResultsCount': uniqueResults?.filterMatches.length - }; - this.telemetryService.publicLog2('settingsEditor.filter', data); - } - private async triggerFilterPreferences(query: string): Promise { if (this.searchInProgress) { this.searchInProgress.cancel(); @@ -1923,10 +1880,6 @@ class SyncControls extends Disposable { DOM.hide(this.turnOnSyncButton.element); this._register(this.turnOnSyncButton.onDidClick(async () => { - telemetryService.publicLog2<{}, { - owner: 'sandy081'; - comment: 'This event tracks whenever settings sync is turned on from settings editor.'; - }>('sync/turnOnSyncFromSettings'); await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn'); })); diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 76b3878ed85..e2fe1833fe3 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -141,7 +141,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const disposables = new DisposableStore(); const workspaceTrustElement = $('span.setting-indicator.setting-item-workspace-trust'); const workspaceTrustLabel = disposables.add(new SimpleIconLabel(workspaceTrustElement)); - workspaceTrustLabel.text = '$(warning) ' + localize('workspaceUntrustedLabel', "Setting value not applied"); + workspaceTrustLabel.text = '$(shield) ' + localize('workspaceUntrustedLabel', "Requires workspace trust"); const content = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); const showHover = (focus: boolean) => { @@ -370,8 +370,8 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.element.style.display = 'inline'; this.scopeOverridesIndicator.element.classList.add('setting-indicator'); - this.scopeOverridesIndicator.label.text = '$(warning) ' + localize('policyLabelText', "Setting value not applied"); - const content = localize('policyDescription', "This setting is managed by your organization and its applied value cannot be changed."); + this.scopeOverridesIndicator.label.text = '$(briefcase) ' + localize('policyLabelText', "Managed by organization"); + const content = localize('policyDescription', "This setting is managed by your organization and its actual value cannot be changed."); const showHover = (focus: boolean) => { return this.hoverService.showHover({ ...this.defaultHoverOptions, diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index eb600aa17e9..8bf195e2440 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -64,7 +64,7 @@ import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from './settingsWidgets.js'; import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js'; import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; -import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js'; @@ -2207,7 +2207,7 @@ export class SettingTreeRenderers extends Disposable { private getActionsForSetting(setting: ISetting, settingTarget: SettingsTarget): IAction[] { const actions: IAction[] = []; - if (setting.scope !== ConfigurationScope.APPLICATION && settingTarget === ConfigurationTarget.USER_LOCAL) { + if (!(setting.scope && APPLICATION_SCOPES.includes(setting.scope)) && settingTarget === ConfigurationTarget.USER_LOCAL) { actions.push(this._instantiationService.createInstance(ApplySettingToAllProfilesAction, setting)); } if (this._userDataSyncEnablementService.isEnabled() && !setting.disallowSyncIgnore) { @@ -2560,6 +2560,7 @@ export class SettingsTree extends WorkbenchObjectTree { { horizontalScrolling: false, supportDynamicHeights: true, + scrollToActiveElement: true, identityProvider: { getId(e) { return e.id; diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index fd773b645d5..d8712dc5e91 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -22,6 +22,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import { Registry } from '../../../../platform/registry/common/platform.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { USER_LOCAL_AND_REMOTE_SETTINGS } from '../../../../platform/request/common/request.js'; export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices'; @@ -425,12 +426,12 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } if (configTarget === ConfigurationTarget.USER_REMOTE) { - return REMOTE_MACHINE_SCOPES.includes(this.setting.scope); + return REMOTE_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key); } if (configTarget === ConfigurationTarget.USER_LOCAL) { if (isRemote) { - return LOCAL_MACHINE_SCOPES.includes(this.setting.scope); + return LOCAL_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key); } } @@ -959,6 +960,10 @@ export class SearchResultModel extends SettingsTreeModel { // Sort by match type if the match types are not the same. // The priority of the match type is given by the SettingMatchType enum. return b.matchType - a.matchType; + } else if (a.matchType === SettingMatchType.KeyMatch) { + // The match types are the same and are KeyMatch. + // Sort by the number of words matched in the key. + return b.keyMatchScore - a.keyMatchScore; } else if (a.matchType === SettingMatchType.RemoteMatch) { // The match types are the same and are RemoteMatch. // Sort by score. diff --git a/code/src/vs/workbench/contrib/preferences/common/preferences.ts b/code/src/vs/workbench/contrib/preferences/common/preferences.ts index b681d7d6787..10244f4354b 100644 --- a/code/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/code/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -176,5 +176,5 @@ export function compareTwoNullableNumbers(a: number | undefined, b: number | und } } -export const PREVIEW_INDICATOR_DESCRIPTION = localize('previewIndicatorDescription', "This setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome."); -export const EXPERIMENTAL_INDICATOR_DESCRIPTION = localize('experimentalIndicatorDescription', "This setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal."); +export const PREVIEW_INDICATOR_DESCRIPTION = localize('previewIndicatorDescription', "Preview setting: this setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome."); +export const EXPERIMENTAL_INDICATOR_DESCRIPTION = localize('experimentalIndicatorDescription', "Experimental setting: this setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal."); diff --git a/code/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/code/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index a020d871b14..2e03281aa95 100644 --- a/code/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/code/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -126,5 +126,12 @@ registry.registerConfiguration({ 'default': 'filter', 'scope': ConfigurationScope.WINDOW }, + 'workbench.settings.useWeightedKeySearch': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('useWeightedKeySearch', "Controls whether to use a new weight calculation algorithm to order certain search results in the Settings editor. The only search results that will be affected are those where the search query has been determined to match the setting key, and the weights will be calculated in a way that places settings with more matched words and shorter names to the top of the search results."), + 'scope': ConfigurationScope.WINDOW, + 'tags': ['preview'] + } } }); diff --git a/code/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/code/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts index 2dcd24876fe..f06bc5a4193 100644 --- a/code/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts +++ b/code/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -340,7 +340,8 @@ registerAction2(class extends Action2 { keybinding: [{ when: ContextKeyExpr.and( IS_COMPOSITE_NOTEBOOK, - ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl') + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + NOTEBOOK_CELL_LIST_FOCUSED.negate() ), primary: KeyMod.CtrlCmd | KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT @@ -348,7 +349,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and( IS_COMPOSITE_NOTEBOOK, ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), - ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true) + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true), + NOTEBOOK_CELL_LIST_FOCUSED.negate() ), primary: KeyMod.Shift | KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT @@ -356,7 +358,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and( IS_COMPOSITE_NOTEBOOK, ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), - ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', false) + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', false), + NOTEBOOK_CELL_LIST_FOCUSED.negate() ), primary: KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT diff --git a/code/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts b/code/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts index b88e9f6d355..e8090a53fe5 100644 --- a/code/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { localize } from '../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; @@ -11,7 +11,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED } from '../../notebook/common/notebookContextKeys.js'; -export class ReplEditorInputAccessibilityHelp implements IAccessibleViewImplentation { +export class ReplEditorInputAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'REPL Editor Input'; readonly when = ContextKeyExpr.and(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED.negate()); @@ -33,7 +33,7 @@ function getAccessibilityInputHelpText(): string { ].join('\n'); } -export class ReplEditorHistoryAccessibilityHelp implements IAccessibleViewImplentation { +export class ReplEditorHistoryAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'REPL Editor History'; readonly when = ContextKeyExpr.and(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED); diff --git a/code/src/vs/workbench/contrib/scm/browser/media/scm.css b/code/src/vs/workbench/contrib/scm/browser/media/scm.css index b616cb2a26d..5a6eebc287e 100644 --- a/code/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/code/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -496,11 +496,11 @@ } .monaco-hover.history-item-hover p:last-child { - margin-bottom: 0; + margin-bottom: 2px !important; } .monaco-hover.history-item-hover p:last-child span:not(.codicon) { - margin-bottom: 2px !important; + padding: 2px 0; } .monaco-hover.history-item-hover hr { diff --git a/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index c8a3d8b07dd..ce0f83a212f 100644 --- a/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -28,6 +28,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; export const IQuickDiffModelService = createDecorator('IQuickDiffModelService'); @@ -155,7 +156,18 @@ export class QuickDiffModel extends Disposable { })); this._register(this.quickDiffService.onDidChangeQuickDiffProviders(() => this.triggerDiff())); - this._register(this._chatEditingService.onDidChangeEditingSession(() => this.triggerDiff())); + + this._register(autorunWithStore((r, store) => { + for (const session of this._chatEditingService.editingSessionsObs.read(r)) { + store.add(autorun(r => { + for (const entry of session.entries.read(r)) { + entry.state.read(r); // signal + } + this.triggerDiff(); + })); + } + })); + this.triggerDiff(); } @@ -344,9 +356,10 @@ export class QuickDiffModel extends Disposable { } const uri = this._model.resource; - const session = this._chatEditingService.currentEditingSession; - if (session && session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified) { - // disable dirty diff when doing chat edits + // disable dirty diff when doing chat edits + const isBeingModifiedByChatEdits = this._chatEditingService.editingSessionsObs.get() + .some(session => session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified); + if (isBeingModifiedByChatEdits) { return Promise.resolve([]); } diff --git a/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 7f8e9c71a09..c9811c5d89f 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -42,6 +42,8 @@ import { QuickDiffModelService, IQuickDiffModelService } from './quickDiffModel. import { QuickDiffEditorController } from './quickDiffWidget.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { RemoteNameContext } from '../../../common/contextkeys.js'; +import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { SCMAccessibilityHelp } from './scmAccessibilityHelp.js'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -605,3 +607,5 @@ registerSingleton(ISCMService, SCMService, InstantiationType.Delayed); registerSingleton(ISCMViewService, SCMViewService, InstantiationType.Delayed); registerSingleton(IQuickDiffService, QuickDiffService, InstantiationType.Delayed); registerSingleton(IQuickDiffModelService, QuickDiffModelService, InstantiationType.Delayed); + +AccessibleViewRegistry.register(new SCMAccessibilityHelp()); diff --git a/code/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts b/code/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts new file mode 100644 index 00000000000..3126416fcfd --- /dev/null +++ b/code/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { AccessibleViewType, AccessibleContentProvider, IAccessibleViewContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { FocusedViewContext, SidebarFocusContext } from '../../../common/contextkeys.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; +import { HISTORY_VIEW_PANE_ID, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, VIEW_PANE_ID } from '../common/scm.js'; + +export class SCMAccessibilityHelp implements IAccessibleViewImplementation { + readonly name = 'scm'; + readonly type = AccessibleViewType.Help; + readonly priority = 100; + readonly when = ContextKeyExpr.or( + ContextKeyExpr.and(ContextKeyExpr.equals('activeViewlet', 'workbench.view.scm'), SidebarFocusContext), + ContextKeyExpr.equals(FocusedViewContext.key, REPOSITORIES_VIEW_PANE_ID), + ContextKeyExpr.equals(FocusedViewContext.key, VIEW_PANE_ID), + ContextKeyExpr.equals(FocusedViewContext.key, HISTORY_VIEW_PANE_ID) + ); + + getProvider(accessor: ServicesAccessor): AccessibleContentProvider { + const commandService = accessor.get(ICommandService); + const scmViewService = accessor.get(ISCMViewService); + const viewsService = accessor.get(IViewsService); + + return new SCMAccessibilityHelpContentProvider(commandService, scmViewService, viewsService); + } +} + +class SCMAccessibilityHelpContentProvider extends Disposable implements IAccessibleViewContentProvider { + readonly id = AccessibleViewProviderId.SourceControl; + readonly verbositySettingKey = AccessibilityVerbositySettingId.SourceControl; + readonly options = { type: AccessibleViewType.Help }; + + private _focusedView: string | undefined; + + constructor( + @ICommandService private readonly _commandService: ICommandService, + @ISCMViewService private readonly _scmViewService: ISCMViewService, + @IViewsService private readonly _viewsService: IViewsService + ) { + super(); + this._focusedView = this._viewsService.getFocusedViewName(); + } + + onClose(): void { + switch (this._focusedView) { + case 'Source Control': + this._commandService.executeCommand('workbench.scm'); + break; + case 'Source Control Repositories': + this._commandService.executeCommand('workbench.scm.repositories'); + break; + case 'Source Control Graph': + this._commandService.executeCommand('workbench.scm.history'); + break; + default: + this._commandService.executeCommand('workbench.view.scm'); + } + } + + provideContent(): string { + const content: string[] = []; + + // Active Repository State + if (this._scmViewService.visibleRepositories.length > 1) { + const repositoryList = this._scmViewService.visibleRepositories.map(r => r.provider.name).join(', '); + content.push(localize('state-msg1', "Visible repositories: {0}", repositoryList)); + } + + const activeRepository = this._scmViewService.activeRepository.get(); + if (activeRepository) { + content.push(localize('state-msg2', "Repository: {0}", activeRepository.provider.name)); + + // History Item Reference + const currentHistoryItemRef = activeRepository.provider.historyProvider.get()?.historyItemRef.get(); + if (currentHistoryItemRef) { + content.push(localize('state-msg3', "History item reference: {0}", currentHistoryItemRef.name)); + } + + // Commit Message + if (activeRepository.input.visible && activeRepository.input.enabled && activeRepository.input.value !== '') { + content.push(localize('state-msg4', "Commit message: {0}", activeRepository.input.value)); + } + + // Action Button + const actionButton = activeRepository.provider.actionButton.get(); + if (actionButton) { + const label = actionButton.command.tooltip ?? actionButton.command.title; + const enablementLabel = actionButton.enabled ? localize('enabled', "enabled") : localize('disabled', "disabled"); + content.push(localize('state-msg5', "Action button: {0}, {1}", label, enablementLabel)); + } + + // Resource Groups + const resourceGroups: string[] = []; + for (const resourceGroup of activeRepository.provider.groups) { + resourceGroups.push(`${resourceGroup.label} (${resourceGroup.resources.length} resource(s))`); + } + + activeRepository.provider.groups.map(g => g.label).join(', '); + content.push(localize('state-msg6', "Resource groups: {0}", resourceGroups.join(', '))); + } + + // Source Control Repositories + content.push(localize('scm-repositories-msg1', "Use the \"Source Control: Focus on Source Control Repositories View\" command to open the Source Control Repositories view.")); + content.push(localize('scm-repositories-msg2', "The Source Control Repositories view lists all repositories from the workspace and is only shown when the workspace contains more than one repository.")); + content.push(localize('scm-repositories-msg3', "Once the Source Control Repositories view is opened you can:")); + content.push(localize('scm-repositories-msg4', " - Use the up/down arrow keys to navigate the list of repositories.")); + content.push(localize('scm-repositories-msg5', " - Use the Enter or Space keys to select a repository.")); + content.push(localize('scm-repositories-msg6', " - Use Shift + up/down keys to select multiple repositories.")); + + // Source Control + content.push(localize('scm-msg1', "Use the \"Source Control: Focus on Source Control View\" command to open the Source Control view.")); + content.push(localize('scm-msg2', "The Source Control view displays the resource groups and resources of the repository. If the workspace contains more than one repository it will list the resource groups and resources of the repositories selected in the Source Control Repositories view.")); + content.push(localize('scm-msg3', "Once the Source Control view is opened you can:")); + content.push(localize('scm-msg4', " - Use the up/down arrow keys to navigate the list of repositories, resource groups and resources.")); + content.push(localize('scm-msg5', " - Use the Space key to expand or collapse a resource group.")); + + // Source Control Graph + content.push(localize('scm-graph-msg1', "Use the \"Source Control: Focus on Source Control Graph View\" command to open the Source Control Graph view.")); + content.push(localize('scm-graph-msg2', "The Source Control Graph view displays a graph history items of the repository. If the workspace contains more than one repository it will list the history items of the active repository.")); + content.push(localize('scm-graph-msg3', "Once the Source Control Graph view is opened you can:")); + content.push(localize('scm-graph-msg4', " - Use the up/down arrow keys to navigate the list of history items.")); + content.push(localize('scm-graph-msg5', " - Use the Space key to open the history item details in the multi-file diff editor.")); + + return content.join('\n'); + } +} diff --git a/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 1c3f71ac1ca..0d20c1d6de1 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -6,7 +6,7 @@ import './media/scm.css'; import * as platform from '../../../../base/common/platform.js'; import { $, append, h, reset } from '../../../../base/browser/dom.js'; -import { IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; +import { IHoverAction, IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; @@ -15,7 +15,7 @@ import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode, ITreeRenderer } fro import { fromNow, safeIntl } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange, observableSignal } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -40,11 +40,11 @@ import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/lis import { stripIcons } from '../../../../base/common/iconLabels.js'; import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; -import { Action2, IMenuService, MenuId, MenuItemAction, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, IMenuService, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Sequencer, Throttler } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ActionRunner, IAction, IActionRunner, Separator, SubmenuAction } from '../../../../base/common/actions.js'; +import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js'; import { delta, groupBy } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; @@ -238,13 +238,14 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.scm.action.graph.viewChanges', - title: localize('viewChanges', "View Changes"), + title: localize('openChanges', "Open Changes"), f1: false, menu: [ { - id: MenuId.SCMChangesContext, + id: MenuId.SCMHistoryItemContext, + when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), group: '0_view', - when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true) + order: 1 } ] }); @@ -324,7 +325,9 @@ class HistoryItemRenderer implements ITreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + const provider = node.element.repository.provider; const historyItemViewModel = node.element.historyItemViewModel; const historyItem = historyItemViewModel.historyItem; const historyItemHover = this._hoverService.setupManagedHover(this.hoverDelegate, templateData.element, this._getHoverContent(node.element), { - actions: this._getHoverActions(historyItem), + actions: this._getHoverActions(provider, historyItem), }); templateData.elementDisposables.add(historyItemHover); @@ -355,7 +359,6 @@ class HistoryItemRenderer implements ITreeRenderer item[1]); + return [ { commandId: 'workbench.scm.action.graph.copyHistoryItemId', @@ -445,12 +453,18 @@ class HistoryItemRenderer implements ITreeRenderer this._clipboardService.writeText(historyItem.id) }, - { - commandId: 'workbench.scm.action.graph.copyHistoryItemMessage', - iconClass: 'codicon.codicon-copy', - label: localize('historyItemMessage', "Message"), - run: () => this._clipboardService.writeText(historyItem.message) - } + ...actions.map(action => { + const iconClass = ThemeIcon.isThemeIcon(action.item.icon) + ? ThemeIcon.asClassNameArray(action.item.icon).join('.') + : undefined; + + return { + commandId: action.id, + label: action.label, + iconClass, + run: () => action.run(historyItem) + }; + }) satisfies IHoverAction[] ]; } @@ -461,11 +475,17 @@ class HistoryItemRenderer implements ITreeRenderer; private readonly _scmCurrentHistoryItemRefInFilter: IContextKey; + private readonly _contextMenuDisposables = new MutableDisposable(); + constructor( options: IViewPaneOptions, @ICommandService private readonly _commandService: ICommandService, @@ -1576,46 +1598,79 @@ export class SCMHistoryViewPane extends ViewPane { return; } - const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMChangesContext, this.scopedContextKeyService, { - arg: element.repository.provider, - shouldForwardArgs: true - }); + this._contextMenuDisposables.value = new DisposableStore(); - const actions = getFlatContextMenuActions(historyItemMenuActions); - if (element.historyItemViewModel.historyItem.references?.length) { - actions.push(new Separator()); - } + const historyItemRefMenuItems = MenuRegistry.getMenuItems(MenuId.SCMHistoryItemRefContext).filter(item => isIMenuItem(item)); - const that = this; - for (const ref of element.historyItemViewModel.historyItem.references ?? []) { - const contextKeyService = this.scopedContextKeyService.createOverlay([ - ['scmHistoryItemRef', ref.id] - ]); - - const historyItemRefMenuActions = this._menuService.getMenuActions(MenuId.SCMHistoryItemRefContext, contextKeyService); - const historyItemRefSubMenuActions = getFlatContextMenuActions(historyItemRefMenuActions) - .map(action => new class extends MenuItemAction { - constructor() { - super( - { id: action.id, title: action.label }, undefined, - { arg: element!.repository.provider, shouldForwardArgs: true }, - undefined, undefined, contextKeyService, that._commandService); - } + // If there are any history item references we have to add a submenu item for each orignal action, + // and a menu item for each history item ref that matches the `when` clause of the original action. + if (historyItemRefMenuItems.length > 0 && element.historyItemViewModel.historyItem.references?.length) { + const historyItemRefActions = new Map(); + + for (const ref of element.historyItemViewModel.historyItem.references) { + const contextKeyService = this.scopedContextKeyService.createOverlay([ + ['scmHistoryItemRef', ref.id] + ]); - override run(): Promise { - return super.run(element.historyItemViewModel.historyItem, ref.id); + const menuActions = this._menuService.getMenuActions( + MenuId.SCMHistoryItemRefContext, contextKeyService); + + for (const action of menuActions.flatMap(a => a[1])) { + if (!historyItemRefActions.has(action.id)) { + historyItemRefActions.set(action.id, []); } - }); - if (historyItemRefSubMenuActions.length > 0) { - actions.push(new SubmenuAction(`scm.historyItemRef.${ref.id}`, ref.name, historyItemRefSubMenuActions)); + historyItemRefActions.get(action.id)!.push(ref); + } + } + + // Register submenu, menu items + for (const historyItemRefMenuItem of historyItemRefMenuItems) { + const actionId = historyItemRefMenuItem.command.id; + + if (!historyItemRefActions.has(actionId)) { + continue; + } + + // Register the submenu for the original action + this._contextMenuDisposables.value.add(MenuRegistry.appendMenuItem(MenuId.SCMHistoryItemContext, { + title: historyItemRefMenuItem.command.title, + submenu: MenuId.for(actionId), + group: historyItemRefMenuItem?.group, + order: historyItemRefMenuItem?.order + })); + + // Register the action for the history item ref + for (const historyItemRef of historyItemRefActions.get(actionId) ?? []) { + this._contextMenuDisposables.value.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `${actionId}.${historyItemRef.id}`, + title: historyItemRef.name, + menu: { + id: MenuId.for(actionId), + group: historyItemRef.category + } + }); + } + override run(accessor: ServicesAccessor, ...args: any[]): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(actionId, ...args, historyItemRef.id); + } + })); + } } } + const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMHistoryItemContext, this.scopedContextKeyService, { + arg: element.repository.provider, + shouldForwardArgs: true + }); + this.contextMenuService.showContextMenu({ contextKeyService: this.scopedContextKeyService, getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => getFlatContextMenuActions(historyItemMenuActions), getActionsContext: () => element.historyItemViewModel.historyItem }); } @@ -1648,6 +1703,7 @@ export class SCMHistoryViewPane extends ViewPane { } override dispose(): void { + this._contextMenuDisposables.dispose(); this._visibilityDisposables.dispose(); super.dispose(); } diff --git a/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 8a05bb6c06f..a786b73aab1 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -21,7 +21,7 @@ import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from ' import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from '../../../../platform/actions/common/actions.js'; -import { IAction, ActionRunner, Action, Separator, IActionRunner } from '../../../../base/common/actions.js'; +import { IAction, ActionRunner, Action, Separator, IActionRunner, toAction } from '../../../../base/common/actions.js'; import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IThemeService, IFileIconTheme } from '../../../../platform/theme/common/themeService.js'; import { isSCMResource, isSCMResourceGroup, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMResourceNode, connectPrimaryMenu } from './util.js'; @@ -2988,7 +2988,14 @@ export class SCMActionButton implements IDisposable { for (let index = 0; index < button.secondaryCommands.length; index++) { const commands = button.secondaryCommands[index]; for (const command of commands) { - actions.push(new Action(command.id, command.title, undefined, true, async () => await this.executeCommand(command.id, ...(command.arguments || [])))); + actions.push(toAction({ + id: command.id, + label: command.title, + enabled: true, + run: async () => { + await this.executeCommand(command.id, ...(command.arguments || [])); + } + })); } if (commands.length) { actions.push(new Separator()); diff --git a/code/src/vs/workbench/contrib/scm/common/history.ts b/code/src/vs/workbench/contrib/scm/common/history.ts index e8a6cda495d..3e9846603f3 100644 --- a/code/src/vs/workbench/contrib/scm/common/history.ts +++ b/code/src/vs/workbench/contrib/scm/common/history.ts @@ -64,6 +64,7 @@ export interface ISCMHistoryItem { readonly displayId?: string; readonly author?: string; readonly authorEmail?: string; + readonly authorIcon?: URI | { light: URI; dark: URI } | ThemeIcon; readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; readonly references?: ISCMHistoryItemRef[]; @@ -97,5 +98,4 @@ export interface ISCMHistoryItemChange { readonly uri: URI; readonly originalUri?: URI; readonly modifiedUri?: URI; - readonly renameUri?: URI; } diff --git a/code/src/vs/workbench/contrib/scm/common/scmService.ts b/code/src/vs/workbench/contrib/scm/common/scmService.ts index 14c69f73b0d..bd3c282b499 100644 --- a/code/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/code/src/vs/workbench/contrib/scm/common/scmService.ts @@ -388,7 +388,7 @@ export class SCMService implements ISCMService { const historyProviderCount = () => { return Array.from(this._repositories.values()) - .filter(r => !!r.provider.historyProvider).length; + .filter(r => !!r.provider.historyProvider.get()).length; }; disposables.add(toDisposable(() => { diff --git a/code/src/vs/workbench/contrib/search/browser/searchView.ts b/code/src/vs/workbench/contrib/search/browser/searchView.ts index 7dcd7c9d5a4..0cb56aa9c21 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1657,12 +1657,13 @@ export class SearchView extends ViewPane { await this.refreshAndUpdateCount(); } - const hasResults = !this.viewModel.searchResult.isEmpty(); - if (completed?.exit === SearchCompletionExitCode.NewSearchStarted) { + const allResults = !this.viewModel.searchResult.isEmpty(); + const aiResults = this.searchResult.getCachedSearchComplete(true); + if (completed?.exit === SearchCompletionExitCode.NewSearchStarted || (this.shouldShowAIResults() && !aiResults)) { return; } - if (!hasResults) { + if (!allResults) { const hasExcludes = !!excludePatternText; const hasIncludes = !!includePatternText; let message: string; diff --git a/code/src/vs/workbench/contrib/search/browser/searchWidget.ts b/code/src/vs/workbench/contrib/search/browser/searchWidget.ts index 3b1bab8cf71..77b589490ec 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -512,7 +512,7 @@ export class SearchWidget extends Widget { label: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview'), placeholder: nls.localize('search.replace.placeHolder', "Replace"), appendPreserveCaseLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.TogglePreserveCaseId)), - history: options.replaceHistory, + history: new Set(options.replaceHistory), showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT, diff --git a/code/src/vs/workbench/contrib/splash/browser/partsSplash.ts b/code/src/vs/workbench/contrib/splash/browser/partsSplash.ts index d410339e5ad..935a7432d71 100644 --- a/code/src/vs/workbench/contrib/splash/browser/partsSplash.ts +++ b/code/src/vs/workbench/contrib/splash/browser/partsSplash.ts @@ -95,6 +95,7 @@ export class PartsSplash { titleBarHeight: this._layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow) ? dom.getTotalHeight(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.TITLEBAR_PART))) : 0, activityBarWidth: this._layoutService.isVisible(Parts.ACTIVITYBAR_PART) ? dom.getTotalWidth(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.ACTIVITYBAR_PART))) : 0, sideBarWidth: this._layoutService.isVisible(Parts.SIDEBAR_PART) ? dom.getTotalWidth(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.SIDEBAR_PART))) : 0, + auxiliarySideBarWidth: this._layoutService.isVisible(Parts.AUXILIARYBAR_PART) ? dom.getTotalWidth(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.AUXILIARYBAR_PART))) : 0, statusBarHeight: this._layoutService.isVisible(Parts.STATUSBAR_PART, mainWindow) ? dom.getTotalHeight(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.STATUSBAR_PART))) : 0, windowBorder: this._layoutService.hasMainWindowBorder(), windowBorderRadius: this._layoutService.getMainWindowBorderRadius() diff --git a/code/src/vs/workbench/contrib/splash/browser/splash.contribution.ts b/code/src/vs/workbench/contrib/splash/browser/splash.contribution.ts index e690968bb84..e9c46b3355a 100644 --- a/code/src/vs/workbench/contrib/splash/browser/splash.contribution.ts +++ b/code/src/vs/workbench/contrib/splash/browser/splash.contribution.ts @@ -10,12 +10,14 @@ import { PartsSplash } from './partsSplash.js'; import { IPartsSplash } from '../../../../platform/theme/common/themeService.js'; registerSingleton(ISplashStorageService, class SplashStorageService implements ISplashStorageService { + _serviceBrand: undefined; async saveWindowSplash(splash: IPartsSplash): Promise { const raw = JSON.stringify(splash); localStorage.setItem('monaco-parts-splash', raw); } + }, InstantiationType.Delayed); registerWorkbenchContribution2( diff --git a/code/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts b/code/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts index 7081bd0c6c2..503f15712b4 100644 --- a/code/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts +++ b/code/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts @@ -11,7 +11,9 @@ import { PartsSplash } from '../browser/partsSplash.js'; import { IPartsSplash } from '../../../../platform/theme/common/themeService.js'; class SplashStorageService implements ISplashStorageService { + _serviceBrand: undefined; + readonly saveWindowSplash: (splash: IPartsSplash) => Promise; constructor(@INativeHostService nativeHostService: INativeHostService) { diff --git a/code/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/code/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index a38705cd01a..ab86e967c41 100644 --- a/code/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/code/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -195,6 +195,7 @@ const ModulesToLookFor = [ '@langchain/anthropic', 'langsmith', 'llamaindex', + '@google-cloud/aiplatform', '@mistralai/mistralai', 'mongodb', 'neo4j-driver', @@ -363,6 +364,7 @@ const PyModulesToLookFor = [ 'transformers', 'langchain', 'llama-index', + 'google-cloud-aiplatform', 'guidance', 'openai', 'semantic-kernel', @@ -572,6 +574,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@langchain/anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.langsmith" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.llamaindex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@google-cloud/aiplatform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@mistralai/mistralai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.milvus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.mongodb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -943,6 +946,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.langchain-fireworks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.langchain-huggingface" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.llama-index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.google-cloud-aiplatform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.guidance" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.ollama" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.onnxruntime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 88792098d83..2b00a6a1f05 100644 --- a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -83,6 +83,7 @@ import { IPaneCompositePartService } from '../../../services/panecomposite/brows import { IPathService } from '../../../services/path/common/pathService.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -2055,12 +2056,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; const error = (error: any) => { try { - if (error && Types.isString(error.message)) { - this._log(`Error: ${error.message}\n`); - this._showOutput(); - } else { - this._log('Unknown error received while collecting tasks from providers.'); - this._showOutput(); + if (!isCancellationError(error)) { + if (error && Types.isString(error.message)) { + this._log(`Error: ${error.message}\n`); + this._showOutput(); + } else { + this._log('Unknown error received while collecting tasks from providers.'); + this._showOutput(); + } } } finally { if (--counter === 0) { diff --git a/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 73de190f4cc..a350e7a74b0 100644 --- a/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -20,7 +20,6 @@ import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService import { ITextFileService, ITextFileSaveEvent, ITextFileResolveEvent } from '../../../services/textfile/common/textfiles.js'; import { extname, basename, isEqual, isEqualOrParent } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; -import { Event } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { getMimeTypes } from '../../../../editor/common/services/languagesAssociations.js'; import { hash } from '../../../../base/common/hash.js'; @@ -32,7 +31,6 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '. import { isBoolean, isNumber, isString } from '../../../../base/common/types.js'; import { LayoutSettings } from '../../../services/layout/browser/layoutService.js'; import { AutoRestartConfigurationKey, AutoUpdateConfigurationKey } from '../../extensions/common/extensions.js'; -import { KEYWORD_ACTIVIATION_SETTING_ID } from '../../chat/common/chatService.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; type TelemetryData = { @@ -144,7 +142,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsReadClassification = { - owner: 'bpasero'; + owner: 'isidorn'; settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was read.' }; comment: 'Track when a settings file was read, for example from an editor.'; }; @@ -152,7 +150,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data } else { type FileGetClassification = { - owner: 'bpasero'; + owner: 'isidorn'; comment: 'Track when a file was read, for example from an editor.'; } & FileTelemetryDataFragment; @@ -164,14 +162,14 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsWrittenClassification = { - owner: 'bpasero'; + owner: 'isidorn'; settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was written to.' }; comment: 'Track when a settings file was written to, for example from an editor.'; }; this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data } else { type FilePutClassfication = { - owner: 'bpasero'; + owner: 'isidorn'; comment: 'Track when a file was written to, for example from an editor.'; } & FileTelemetryDataFragment; this.telemetryService.publicLog2('filePUT', this.getTelemetryData(e.model.resource, e.reason)); @@ -246,31 +244,6 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc ) { super(); - // Debounce the event by 1000 ms and merge all affected keys into one event - const debouncedConfigService = Event.debounce(configurationService.onDidChangeConfiguration, (last, cur) => { - const newAffectedKeys: ReadonlySet = last ? new Set([...last.affectedKeys, ...cur.affectedKeys]) : cur.affectedKeys; - return { ...cur, affectedKeys: newAffectedKeys }; - }, 1000, true); - - this._register(debouncedConfigService(event => { - if (event.source !== ConfigurationTarget.DEFAULT) { - type UpdateConfigurationClassification = { - owner: 'sandy081'; - comment: 'Event which fires when user updates settings'; - configurationSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What configuration file was updated i.e user or workspace' }; - configurationKeys: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What configuration keys were updated' }; - }; - type UpdateConfigurationEvent = { - configurationSource: string; - configurationKeys: string[]; - }; - telemetryService.publicLog2('updateConfiguration', { - configurationSource: ConfigurationTargetToString(event.source), - configurationKeys: Array.from(event.affectedKeys) - }); - } - })); - const { user, workspace } = configurationService.keys(); for (const setting of user) { this.reportTelemetry(setting, ConfigurationTarget.USER_LOCAL); @@ -332,15 +305,6 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc }>('extensions.autoUpdate', { settingValue: this.getValueToReport(key, target), source }); return; - case 'files.autoSave': - this.telemetryService.publicLog2('files.autoSave', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'editor.stickyScroll.enabled': this.telemetryService.publicLog2('editor.stickyScroll.enabled', { settingValue: this.getValueToReport(key, target), source }); return; - case KEYWORD_ACTIVIATION_SETTING_ID: - this.telemetryService.publicLog2('accessibility.voice.keywordActivation', { settingValue: this.getValueToReport(key, target), source }); - return; - - case 'window.zoomLevel': - this.telemetryService.publicLog2('window.zoomLevel', { settingValue: this.getValueToReport(key, target), source }); - return; - - case 'window.zoomPerWindow': - this.telemetryService.publicLog2('window.zoomPerWindow', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'window.titleBarStyle': this.telemetryService.publicLog2('window.titleBarStyle', { settingValue: this.getValueToReport(key, target), source }); return; - case 'window.commandCenter': - this.telemetryService.publicLog2('window.commandCenter', { settingValue: this.getValueToReport(key, target), source }); - return; - - case 'chat.commandCenter.enabled': - this.telemetryService.publicLog2('chat.commandCenter.enabled', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'window.customTitleBarVisibility': this.telemetryService.publicLog2('extensions.verifySignature', { settingValue: this.getValueToReport(key, target), source }); return; - case 'window.systemColorTheme': - this.telemetryService.publicLog2('window.systemColorTheme', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'window.newWindowProfile': { const valueToReport = this.getValueToReport(key, target); diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminal.ts b/code/src/vs/workbench/contrib/terminal/browser/terminal.ts index ead75598c0e..7e65338102b 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -31,6 +31,7 @@ import type { ICurrentPartialCommand } from '../../../../platform/terminal/commo import type { IXtermCore } from './xterm-private.js'; import type { IMenu } from '../../../../platform/actions/common/actions.js'; import type { Barrier } from '../../../../base/common/async.js'; +import type { IProgressState } from '@xterm/addon-progress'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalConfigurationService = createDecorator('terminalConfigurationService'); @@ -275,6 +276,7 @@ export interface ITerminalService extends ITerminalInstanceHost { readonly onAnyInstanceProcessIdReady: Event; readonly onAnyInstanceSelectionChange: Event; readonly onAnyInstanceTitleChange: Event; + readonly onAnyInstanceShellTypeChanged: Event; /** * Creates a terminal. @@ -610,6 +612,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { readonly processName: string; readonly sequence?: string; readonly staticTitle?: string; + readonly progressState?: IProgressState; readonly workspaceFolder?: IWorkspaceFolder; readonly cwd?: string; readonly initialCwd?: string; diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 9218eff9f83..00927f236f3 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -61,6 +61,7 @@ import { isKeyboardEvent, isMouseEvent, isPointerEvent } from '../../../../base/ import { editorGroupToColumn } from '../../../services/editor/common/editorGroupColumn.js'; import { InstanceContext } from './terminalContextMenu.js'; import { AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { TerminalTabList } from './terminalTabsList.js'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -317,8 +318,8 @@ export function registerTerminalActions() { return; } c.service.setActiveInstance(instance); + await focusActiveTerminal(instance, c); } - await c.groupService.showPanel(true); } }); @@ -1463,7 +1464,8 @@ function getSelectedInstances(accessor: ServicesAccessor, args?: unknown, args2? const terminalGroupService = accessor.get(ITerminalGroupService); const result: ITerminalInstance[] = []; - const list = listService.lastFocusedList; + // Assign list only if it's an instance of TerminalTabList (#234791) + const list = listService.lastFocusedList instanceof TerminalTabList ? listService.lastFocusedList : undefined; // Get selected tab list instance(s) const selections = list?.getSelection(); // Get inline tab instance if there are not tab list selections #196578 diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index d18dd081e56..c242e1f1144 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -90,6 +90,7 @@ import { openContextMenu } from './terminalContextMenu.js'; import type { IMenu } from '../../../../platform/actions/common/actions.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { TerminalContribCommandId } from '../terminalContribExports.js'; +import type { IProgressState } from '@xterm/addon-progress'; const enum Constants { /** @@ -283,6 +284,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get processName(): string { return this._processName; } get sequence(): string | undefined { return this._sequence; } get staticTitle(): string | undefined { return this._staticTitle; } + get progressState(): IProgressState | undefined { return this.xterm?.progressState; } get workspaceFolder(): IWorkspaceFolder | undefined { return this._workspaceFolder; } get cwd(): string | undefined { return this._cwd; } get initialCwd(): string | undefined { return this._initialCwd; } @@ -863,6 +865,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { xterm.refresh(); } })); + this._register(xterm.onDidChangeProgress(() => this._labelComputer?.refreshLabel(this))); // Set up updating of the process cwd on key press, this is only needed when the cwd // detection capability has not been registered @@ -2472,6 +2475,7 @@ interface ITerminalLabelTemplateProperties { local?: string | null | undefined; process?: string | null | undefined; sequence?: string | null | undefined; + progress?: string | null | undefined; task?: string | null | undefined; fixedDimensions?: string | null | undefined; separator?: string | ISeparator | null | undefined; @@ -2511,7 +2515,7 @@ export class TerminalLabelComputer extends Disposable { } computeLabel( - instance: Pick, + instance: Pick, labelTemplate: string, labelType: TerminalLabelType, reset?: boolean @@ -2542,6 +2546,7 @@ export class TerminalLabelComputer extends Disposable { shellPromptInput: commandDetection?.executingCommand && promptInputModel ? promptInputModel.getCombinedString(true) + nonTaskSpinner : promptInputModel?.getCombinedString(true), + progress: this._getProgressStateString(instance.progressState) }; templateProperties.workspaceFolderName = instance.workspaceFolder?.name ?? templateProperties.workspaceFolder; labelTemplate = labelTemplate.trim(); @@ -2579,6 +2584,19 @@ export class TerminalLabelComputer extends Disposable { const label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null }).replace(/[\n\r\t]/g, '').trim(); return label === '' && labelType === TerminalLabelType.Title ? (instance.processName || '') : label; } + + private _getProgressStateString(progressState?: IProgressState): string { + if (!progressState) { + return ''; + } + switch (progressState.state) { + case 0: return ''; + case 1: return `${Math.round(progressState.value)}%`; + case 2: return '$(error)'; + case 3: return '$(loading~spin)'; + case 4: return '$(alert)'; + } + } } export function parseExitResult( @@ -2680,6 +2698,7 @@ function guessShellTypeFromExecutable(os: OperatingSystem, executable: string): const exeBasename = path.basename(executable); const generalShellTypeMap: Map = new Map([ [GeneralShellType.Julia, /^julia$/], + [GeneralShellType.Node, /^node$/], [GeneralShellType.NuShell, /^nu$/], [GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/], [GeneralShellType.Python, /^py(?:thon)?$/] diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 7fea79a51a7..0aa49202b03 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -165,7 +165,7 @@ export class TerminalService extends Disposable implements ITerminalService { @memoize get onAnyInstanceProcessIdReady() { return this._register(this.createOnInstanceEvent(e => e.onProcessIdReady)).event; } @memoize get onAnyInstanceSelectionChange() { return this._register(this.createOnInstanceEvent(e => e.onDidChangeSelection)).event; } @memoize get onAnyInstanceTitleChange() { return this._register(this.createOnInstanceEvent(e => e.onTitleChanged)).event; } - + @memoize get onAnyInstanceShellTypeChanged() { return this._register(this.createOnInstanceEvent(e => Event.map(e.onDidChangeShellType, () => e))).event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, diff --git a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts index 753ba6f9e79..30e6bd1905b 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts @@ -6,6 +6,7 @@ import type { ClipboardAddon as ClipboardAddonType } from '@xterm/addon-clipboard'; import type { ImageAddon as ImageAddonType } from '@xterm/addon-image'; import type { LigaturesAddon as LigaturesAddonType } from '@xterm/addon-ligatures'; +import type { ProgressAddon as ProgressAddonType } from '@xterm/addon-progress'; import type { SearchAddon as SearchAddonType } from '@xterm/addon-search'; import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize'; import type { Unicode11Addon as Unicode11AddonType } from '@xterm/addon-unicode11'; @@ -16,6 +17,7 @@ export interface IXtermAddonNameToCtor { clipboard: typeof ClipboardAddonType; image: typeof ImageAddonType; ligatures: typeof LigaturesAddonType; + progress: typeof ProgressAddonType; search: typeof SearchAddonType; serialize: typeof SerializeAddonType; unicode11: typeof Unicode11AddonType; @@ -42,6 +44,7 @@ export class XtermAddonImporter { case 'clipboard': addon = (await importAMDNodeModule('@xterm/addon-clipboard', 'lib/addon-clipboard.js')).ClipboardAddon as IXtermAddonNameToCtor[T]; break; case 'image': addon = (await importAMDNodeModule('@xterm/addon-image', 'lib/addon-image.js')).ImageAddon as IXtermAddonNameToCtor[T]; break; case 'ligatures': addon = (await importAMDNodeModule('@xterm/addon-ligatures', 'lib/addon-ligatures.js')).LigaturesAddon as IXtermAddonNameToCtor[T]; break; + case 'progress': addon = (await importAMDNodeModule('@xterm/addon-progress', 'lib/addon-progress.js')).ProgressAddon as IXtermAddonNameToCtor[T]; break; case 'search': addon = (await importAMDNodeModule('@xterm/addon-search', 'lib/addon-search.js')).SearchAddon as IXtermAddonNameToCtor[T]; break; case 'serialize': addon = (await importAMDNodeModule('@xterm/addon-serialize', 'lib/addon-serialize.js')).SerializeAddon as IXtermAddonNameToCtor[T]; break; case 'unicode11': addon = (await importAMDNodeModule('@xterm/addon-unicode11', 'lib/addon-unicode11.js')).Unicode11Addon as IXtermAddonNameToCtor[T]; break; diff --git a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index ac41efa9c1b..e753e86f97f 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -43,6 +43,8 @@ import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../.. import { scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { XtermAddonImporter } from './xtermAddonImporter.js'; import { equals } from '../../../../../base/common/objects.js'; +import type { IProgressState } from '@xterm/addon-progress'; +import type { CommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js'; const enum RenderConstants { SmoothScrollDuration = 125 @@ -99,6 +101,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach private _isPhysicalMouseWheel = MouseWheelClassifier.INSTANCE.isPhysicalMouseWheel(); private _lastInputEvent: string | undefined; get lastInputEvent(): string | undefined { return this._lastInputEvent; } + private _progressState: IProgressState = { state: 0, value: 0 }; + get progressState(): IProgressState { return this._progressState; } // Always on addons private _markNavigationAddon: MarkNavigationAddon; @@ -141,6 +145,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach readonly onDidChangeFocus = this._onDidChangeFocus.event; private readonly _onDidDispose = this._register(new Emitter()); readonly onDidDispose = this._onDidDispose.event; + private readonly _onDidChangeProgress = this._register(new Emitter()); + readonly onDidChangeProgress = this._onDidChangeProgress.event; get markTracker(): IMarkTracker { return this._markNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } @@ -286,6 +292,33 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach }); this.raw.loadAddon(this._clipboardAddon); }); + this._xtermAddonLoader.importAddon('progress').then(ProgressAddon => { + if (this._store.isDisposed) { + return; + } + const progressAddon = this._instantiationService.createInstance(ProgressAddon); + this.raw.loadAddon(progressAddon); + const updateProgress = () => { + if (!equals(this._progressState, progressAddon.progress)) { + this._progressState = progressAddon.progress; + this._onDidChangeProgress.fire(this._progressState); + } + }; + this._register(progressAddon.onChange(() => updateProgress())); + updateProgress(); + const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection); + if (commandDetection) { + commandDetection.onCommandFinished(() => progressAddon.progress = { state: 0, value: 0 }); + } else { + const disposable = this._capabilities.onDidAddCapability(e => { + if (e.id === TerminalCapability.CommandDetection) { + (e.capability as CommandDetectionCapability).onCommandFinished(() => progressAddon.progress = { state: 0, value: 0 }); + this._store.delete(disposable); + } + }); + this._store.add(disposable); + } + }); this._anyTerminalFocusContextKey = TerminalContextKeys.focusInAny.bindTo(contextKeyService); this._anyFocusedTerminalHasSelection = TerminalContextKeys.textSelectedInFocused.bindTo(contextKeyService); diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index fbeaa22f90a..090f40212d2 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -214,6 +214,17 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$__vsc_cwd")" } +# __vsc_update_env() { +# builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce +# for var in $(compgen -v); do +# if printenv "$var" >/dev/null 2>&1; then +# value=$(builtin printf '%s' "${!var}") +# builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce +# fi +# done +# builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce +# } + __vsc_command_output_start() { if [[ -z "${__vsc_first_prompt-}" ]]; then builtin return @@ -240,6 +251,10 @@ __vsc_command_complete() { builtin printf '\e]633;D;%s\a' "$__vsc_status" fi __vsc_update_cwd + + # if [ "$__vsc_stable" = "0" ]; then + # __vsc_update_env + # fi } __vsc_update_prompt() { # in command execution @@ -269,6 +284,10 @@ __vsc_precmd() { fi __vsc_first_prompt=1 __vsc_update_prompt + + # if [ "$__vsc_stable" = "0" ]; then + # __vsc_update_env + # fi } __vsc_preexec() { diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh index 128bd648616..8edbca365e4 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh @@ -3,6 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- +# Prevent recursive sourcing +if [[ -n "$VSCODE_LOGIN_INITIALIZED" ]]; then + return +fi +export VSCODE_LOGIN_INITIALIZED=1 + ZDOTDIR=$USER_ZDOTDIR if [[ $options[norcs] = off && -o "login" && -f $ZDOTDIR/.zlogin ]]; then . $ZDOTDIR/.zlogin diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh index c25ded3d7eb..7401c10d376 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh @@ -2,6 +2,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- + +# Prevent recursive sourcing +if [[ -n "$VSCODE_PROFILE_INITIALIZED" ]]; then + return +fi +export VSCODE_PROFILE_INITIALIZED=1 + if [[ $options[norcs] = off && -o "login" ]]; then if [[ -f $USER_ZDOTDIR/.zprofile ]]; then VSCODE_ZDOTDIR=$ZDOTDIR diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 117a2285405..4c5a7059a51 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -88,6 +88,16 @@ function Global:Prompt() { # Current working directory # OSC 633 ; = ST $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a" } + + # Send current environment variables as JSON + # OSC 633 ; Env ; ; + if ($isStable -eq "0") { + $envMap = @{} + Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value } + $envJson = $envMap | ConvertTo-Json -Compress + $Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a" + } + # Before running the original prompt, put $? back to what it was: if ($FakeCode -ne 0) { Write-Error "failure" -ea ignore diff --git a/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 635c3c2cab4..2870078c2c2 100644 --- a/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -21,6 +21,7 @@ const terminalDescriptors = '\n- ' + [ '`\${workspaceFolderName}`: ' + localize('workspaceFolderName', "the `name` of the workspace in which the terminal was launched"), '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace"), '`\${process}`: ' + localize('process', "the name of the terminal process"), + '`\${progress}`: ' + localize('progress', "the progress state as reported by the `OSC 9;4` sequence"), '`\${separator}`: ' + localize('separator', "a conditional separator {0} that only shows when surrounded by variables with values or static text.", '(` - `)'), '`\${sequence}`: ' + localize('sequence', "the name provided to the terminal by the process"), '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task"), diff --git a/code/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/code/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index 74c18f3557b..532950a3908 100644 --- a/code/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/code/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -287,6 +287,8 @@ suite('ShellIntegrationAddon', () => { ['escaped newline (upper hex)', `${Backslash}x0A`, Newline], ['escaped backslash followed by literal "x0a" is not a newline', `${Backslash}${Backslash}x0a`, `${Backslash}x0a`], ['non-initial escaped backslash followed by literal "x0a" is not a newline', `foo${Backslash}${Backslash}x0a`, `foo${Backslash}x0a`], + ['PS1 simple', '[\\u@\\h \\W]\\$', '[\\u@\\h \\W]\\$'], + ['PS1 VSC SI', `${Backslash}x1b]633;A${Backslash}x07\\[${Backslash}x1b]0;\\u@\\h:\\w\\a\\]${Backslash}x1b]633;B${Backslash}x07`, '\x1b]633;A\x07\\[\x1b]0;\\u@\\h:\\w\\a\\]\x1b]633;B\x07'] ]; cases.forEach(([title, input, expected]) => { diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 5017c56364d..1b62a8d1b93 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -5,7 +5,7 @@ import { localize } from '../../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; @@ -13,7 +13,7 @@ import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; import { TerminalChatController } from './terminalChatController.js'; -export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentation { +export class TerminalChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 110; readonly name = 'terminalChat'; readonly when = TerminalChatContextKeys.focused; diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index d864fcbd0bf..734ac7e62a6 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -7,13 +7,13 @@ import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { TerminalChatController } from './terminalChatController.js'; -import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMenuService, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; import { MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatContextKeys } from './terminalChat.js'; import { IAction } from '../../../../../base/common/actions.js'; -export class TerminalInlineChatAccessibleView implements IAccessibleViewImplentation { +export class TerminalInlineChatAccessibleView implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'terminalInlineChat'; readonly type = AccessibleViewType.View; diff --git a/code/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts b/code/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts index 5227135598d..4c06a09c979 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts @@ -335,11 +335,13 @@ export async function showRunRecentQuickPick( text = result.rawLabel; } quickPick.hide(); + terminalScrollStateSaved = false; + instance.xterm?.markTracker.clear(); + instance.scrollToBottom(); instance.runCommand(text, !quickPick.keyMods.alt); if (quickPick.keyMods.alt) { instance.focus(); } - restoreScrollState(); })); disposables.add(quickPick.onDidHide(() => restoreScrollState())); if (value) { diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts index 596b6ce452d..53a0eb48bcc 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts @@ -56,7 +56,7 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal id: string = PwshCompletionProviderAddon.ID; triggerCharacters?: string[] | undefined; isBuiltin?: boolean = true; - static readonly ID = 'terminal.pwshCompletionProvider'; + static readonly ID = 'pwsh-shell-integration'; static cachedPwshCommands: Set; readonly shellTypes = [GeneralShellType.PowerShell]; private _codeCompletionsRequested: boolean = false; @@ -348,6 +348,7 @@ function rawCompletionToITerminalCompletion(rawCompletion: PwshCompletion, repla return { label, + provider: PwshCompletionProviderAddon.ID, icon, detail, isFile: rawCompletion.ResultType === 3, diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index c67d6c4cdaf..7773917c9a9 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -27,6 +27,11 @@ import { InstantiationType, registerSingleton } from '../../../../../platform/in import { SuggestAddon } from './terminalSuggestAddon.js'; import { TerminalClipboardContribution } from '../../clipboard/browser/terminal.clipboard.contribution.js'; import { PwshCompletionProviderAddon } from './pwshCompletionProviderAddon.js'; +import { SimpleSuggestContext } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; +import { SuggestDetailsClassName } from '../../../../services/suggest/browser/simpleSuggestWidgetDetails.js'; +import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; registerSingleton(ITerminalCompletionService, TerminalCompletionService, InstantiationType.Delayed); @@ -60,20 +65,15 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo })); this._terminalSuggestWidgetVisibleContextKey = TerminalContextKeys.suggestWidgetVisible.bindTo(this._contextKeyService); this.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSuggestSettingId.EnableExtensionCompletions) || e.affectsConfiguration(TerminalSuggestSettingId.Enabled)) { - const extensionCompletionsEnabled = this._configurationService.getValue(terminalSuggestConfigSection).enableExtensionCompletions; + if (e.affectsConfiguration(TerminalSuggestSettingId.Enabled)) { const completionsEnabled = this._configurationService.getValue(terminalSuggestConfigSection).enabled; - if (!extensionCompletionsEnabled || !completionsEnabled) { - this._addon.clear(); - } if (!completionsEnabled) { + this._addon.clear(); this._pwshAddon.clear(); } const xtermRaw = this._ctx.instance.xterm?.raw; - if (!!xtermRaw && extensionCompletionsEnabled || completionsEnabled) { - if (xtermRaw) { - this._loadAddons(xtermRaw); - } + if (!!xtermRaw && completionsEnabled) { + this._loadAddons(xtermRaw); } } })); @@ -112,7 +112,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo this._ctx.instance.focus(); this._ctx.instance.sendText(text, false); })); - this.add(this._terminalCompletionService.registerTerminalCompletionProvider('builtinPwsh', 'pwsh', pwshCompletionProviderAddon)); + this.add(this._terminalCompletionService.registerTerminalCompletionProvider('builtinPwsh', pwshCompletionProviderAddon.id, pwshCompletionProviderAddon)); // If completions are requested, pause and queue input events until completions are // received. This fixing some problems in PowerShell, particularly enter not executing // when typing quickly and some characters being printed twice. On Windows this isn't @@ -152,7 +152,16 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!); } addon.setScreen(xterm.element!.querySelector('.xterm-screen')!); - this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget())); + + this.add(dom.addDisposableListener(this._ctx.instance.domElement, dom.EventType.FOCUS_OUT, (e) => { + const focusedElement = e.relatedTarget as HTMLElement; + if (focusedElement?.classList.contains(SuggestDetailsClassName)) { + // Don't hide the suggest widget if the focus is moving to the details + return; + } + addon.hideSuggestWidget(); + })); + this.add(addon.onAcceptedCompletion(async text => { this._ctx.instance.focus(); this._ctx.instance.sendText(text, false); @@ -178,6 +187,11 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo return; } addon.shellType = this._ctx.instance.shellType; + if (!this._ctx.instance.xterm?.raw) { + return; + } + // Relies on shell type being set + this._loadPwshCompletionAddon(this._ctx.instance.xterm.raw); } } @@ -194,7 +208,7 @@ registerActiveInstanceAction({ primary: KeyMod.CtrlCmd | KeyCode.Space, mac: { primary: KeyMod.WinCtrl | KeyCode.Space }, weight: KeybindingWeight.WorkbenchContrib + 1, - when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) + when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.suggestWidgetVisible.negate(), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions(true) }); @@ -244,6 +258,48 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.selectNextSuggestion() }); +registerActiveInstanceAction({ + id: 'terminalSuggestToggleExplainMode', + title: localize2('workbench.action.terminal.suggestToggleExplainMode', 'Suggest Toggle Explain Modes'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), + keybinding: { + // Down is bound to other workbench keybindings that this needs to beat + weight: KeybindingWeight.WorkbenchContrib + 1, + primary: KeyMod.CtrlCmd | KeyCode.Slash, + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleExplainMode() +}); + +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ToggleDetailsFocus, + title: localize2('workbench.action.terminal.suggestToggleDetailsFocus', 'Suggest Toggle Suggestion Focus'), + f1: false, + // HACK: This does not work with a precondition of `TerminalContextKeys.suggestWidgetVisible`, so make sure to not override the editor's keybinding + precondition: EditorContextKeys.textInputFocus.negate(), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Space, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space } + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleSuggestionFocus() +}); + +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ToggleDetails, + title: localize2('workbench.action.terminal.suggestToggleDetails', 'Suggest Toggle Details'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen, TerminalContextKeys.focus, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), + keybinding: { + // HACK: Force weight to be higher than that to start terminal chat + weight: KeybindingWeight.ExternalExtension + 2, + primary: KeyMod.CtrlCmd | KeyCode.Space, + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI], + mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] } + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleSuggestionDetails() +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.SelectNextPageSuggestion, title: localize2('workbench.action.terminal.selectNextPageSuggestion', 'Select the Next Page Suggestion'), @@ -259,7 +315,7 @@ registerActiveInstanceAction({ registerActiveInstanceAction({ id: TerminalSuggestCommandId.AcceptSelectedSuggestion, - title: localize2('workbench.action.terminal.acceptSelectedSuggestion', 'Accept Selected Suggestion'), + title: localize2('workbench.action.terminal.acceptSelectedSuggestion', 'Insert'), f1: false, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), keybinding: { @@ -267,6 +323,11 @@ registerActiveInstanceAction({ // Tab is bound to other workbench keybindings that this needs to beat weight: KeybindingWeight.WorkbenchContrib + 1 }, + menu: { + id: MenuId.MenubarTerminalSuggestStatusMenu, + order: 1, + group: 'left' + }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.acceptSelectedSuggestion() }); @@ -297,6 +358,23 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.hideSuggestWidget() }); +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ConfigureSettings, + title: localize2('workbench.action.terminal.configureSuggestSettings', 'Configure'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.MenubarTerminalSuggestStatusMenu, + group: 'right', + order: 1 + }, + run: (activeInstance, c, accessor) => accessor.get(IPreferencesService).openSettings({ query: terminalSuggestConfigSection }) +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.ClearSuggestCache, title: localize2('workbench.action.terminal.clearSuggestCache', 'Clear Suggest Cache'), diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index ebbeae34b59..7bbc60a4bac 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -2,17 +2,19 @@ * 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 '../../../../../base/common/cancellation.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { basename } from '../../../../../base/common/path.js'; +import { isWindows } from '../../../../../base/common/platform.js'; import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; +import { GeneralShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; -import { ITerminalSuggestConfiguration, terminalSuggestConfigSection } from '../common/terminalSuggestConfiguration.js'; +import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; export const ITerminalCompletionService = createDecorator('terminalCompletionService'); @@ -62,6 +64,8 @@ export interface TerminalResourceRequestConfig { foldersRequested?: boolean; cwd?: URI; pathSeparator: string; + shouldNormalizePrefix?: boolean; + env?: { [key: string]: string | null | undefined }; } @@ -123,11 +127,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise { - if (!this._providers || !this._providers.values) { + if (!this._providers || !this._providers.values || cursorPosition < 0) { return undefined; } - const extensionCompletionsEnabled = this._configurationService.getValue(terminalSuggestConfigSection).enableExtensionCompletions; let providers; if (triggerCharacter) { const providersToRequest: ITerminalCompletionProvider[] = []; @@ -147,8 +150,19 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo providers = [...this._providers.values()].flatMap(providerMap => [...providerMap.values()]); } - if (!extensionCompletionsEnabled || skipExtensionCompletions) { + if (skipExtensionCompletions) { providers = providers.filter(p => p.isBuiltin); + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); + } + + const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); + providers = providers.filter(p => { + const providerId = p.id; + return providerId && providerId in providerConfig && providerConfig[providerId] !== false; + }); + + if (!providers.length) { + return; } return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); @@ -164,17 +178,25 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } const completionItems = Array.isArray(completions) ? completions : completions.items ?? []; - const itemsWithModifiedLabels = this._addDevModeLabel(completionItems, provider.id); - + for (const completion of completionItems) { + completion.isFile ??= completion.kind === TerminalCompletionItemKind.File || (shellType === GeneralShellType.PowerShell && completion.kind === TerminalCompletionItemKind.Method && completion.replacementIndex === 0); + completion.isDirectory ??= completion.kind === TerminalCompletionItemKind.Folder; + } + if (provider.isBuiltin) { + //TODO: why is this needed? + for (const item of completionItems) { + item.provider = provider.id; + } + } if (Array.isArray(completions)) { - return itemsWithModifiedLabels; + return completionItems; } if (completions.resourceRequestConfig) { - const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition); + const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition, provider.id); if (resourceCompletions) { - itemsWithModifiedLabels.push(...this._addDevModeLabel(resourceCompletions, provider.id)); + completionItems.push(...resourceCompletions); } - return itemsWithModifiedLabels; + return completionItems; } return; }); @@ -183,19 +205,11 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return results.filter(result => !!result).flat(); } - private _addDevModeLabel(completions: ITerminalCompletion[], providerId: string): ITerminalCompletion[] { - const devModeEnabled = this._configurationService.getValue(TerminalSettingId.DevMode); - return completions.map(completion => { - // TODO: This providerId check shouldn't be necessary, instead we should ensure this - // function is never called twice - if (devModeEnabled && !completion.detail?.includes(providerId)) { - completion.detail = `(${providerId}) ${completion.detail ?? ''}`; - } - return completion; - }); - } - - async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number): Promise { + async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string): Promise { + if (resourceRequestConfig.shouldNormalizePrefix) { + // for tests, make sure the right path separator is used + promptValue = promptValue.replaceAll(/[\\/]/g, resourceRequestConfig.pathSeparator); + } const cwd = URI.revive(resourceRequestConfig.cwd); const foldersRequested = resourceRequestConfig.foldersRequested ?? false; const filesRequested = resourceRequestConfig.filesRequested ?? false; @@ -210,50 +224,165 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const resourceCompletions: ITerminalCompletion[] = []; const cursorPrefix = promptValue.substring(0, cursorPosition); - const endsWithSpace = cursorPrefix.endsWith(' '); - const lastWord = endsWithSpace ? '' : cursorPrefix.split(' ').at(-1) ?? ''; - for (const stat of fileStat.children) { - let kind: TerminalCompletionItemKind | undefined; - if (foldersRequested && stat.isDirectory) { - kind = TerminalCompletionItemKind.Folder; - } - if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { - kind = TerminalCompletionItemKind.File; - } - if (kind === undefined) { - continue; + const useForwardSlash = !resourceRequestConfig.shouldNormalizePrefix && isWindows; + + // The last word (or argument). When the cursor is following a space it will be the empty + // string + const lastWord = cursorPrefix.endsWith(' ') ? '' : cursorPrefix.split(' ').at(-1) ?? ''; + + // Get the nearest folder path from the prefix. This ignores everything after the `/` as + // they are what triggers changes in the directory. + let lastSlashIndex: number; + if (useForwardSlash) { + lastSlashIndex = Math.max(lastWord.lastIndexOf('\\'), lastWord.lastIndexOf('/')); + } else { + lastSlashIndex = lastWord.lastIndexOf(resourceRequestConfig.pathSeparator); + } + + // The _complete_ folder of the last word. For example if the last word is `./src/file`, + // this will be `./src/`. This also always ends in the path separator if it is not the empty + // string and path separators are normalized on Windows. + let lastWordFolder = lastSlashIndex === -1 ? '' : lastWord.slice(0, lastSlashIndex + 1); + if (isWindows) { + lastWordFolder = lastWordFolder.replaceAll('/', '\\'); + } + + const lastWordFolderHasDotPrefix = lastWordFolder.match(/^\.\.?[\\\/]/); + + const lastWordFolderHasTildePrefix = lastWordFolder.match(/^~[\\\/]/); + if (lastWordFolderHasTildePrefix) { + // Handle specially + const resolvedFolder = resourceRequestConfig.env?.HOME ? URI.file(resourceRequestConfig.env.HOME) : undefined; + if (resolvedFolder) { + resourceCompletions.push({ + label: lastWordFolder, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + detail: getFriendlyPath(resolvedFolder, resourceRequestConfig.pathSeparator), + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + return resourceCompletions; } - const isDirectory = kind === TerminalCompletionItemKind.Folder; - const fileName = basename(stat.resource.fsPath); - - let label; - if (!lastWord.startsWith('.' + resourceRequestConfig.pathSeparator) && !lastWord.startsWith('..' + resourceRequestConfig.pathSeparator)) { - // add a dot to the beginning of the label if it doesn't already have one - label = `.${resourceRequestConfig.pathSeparator}${fileName}`; - } else { - if (lastWord.endsWith(resourceRequestConfig.pathSeparator)) { - label = `${lastWord}${fileName}`; - } else { - label = `${lastWord}${resourceRequestConfig.pathSeparator}${fileName}`; + } + // Add current directory. This should be shown at the top because it will be an exact match + // and therefore highlight the detail, plus it improves the experience when runOnEnter is + // used. + // + // For example: + // - `|` -> `.`, this does not have the trailing `/` intentionally as it's common to + // complete the current working directory and we do not want to complete `./` when + // `runOnEnter` is used. + // - `./src/|` -> `./src/` + if (foldersRequested) { + resourceCompletions.push({ + label: lastWordFolder.length === 0 ? '.' : lastWordFolder, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + + // Handle absolute paths differently to avoid adding `./` prefixes + // TODO: Deal with git bash case + const isAbsolutePath = useForwardSlash + ? /^[a-zA-Z]:\\/.test(lastWord) + : lastWord.startsWith(resourceRequestConfig.pathSeparator) && lastWord.endsWith(resourceRequestConfig.pathSeparator); + + // Add all direct children files or folders + // + // For example: + // - `cd ./src/` -> `cd ./src/folder1`, ... + if (!isAbsolutePath) { + for (const stat of fileStat.children) { + let kind: TerminalCompletionItemKind | undefined; + if (foldersRequested && stat.isDirectory) { + kind = TerminalCompletionItemKind.Folder; } - if (lastWord.length && lastWord.at(-1) !== resourceRequestConfig.pathSeparator && lastWord.at(-1) !== '.') { - label = `.${resourceRequestConfig.pathSeparator}${fileName}`; + if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { + kind = TerminalCompletionItemKind.File; } + if (kind === undefined) { + continue; + } + const isDirectory = kind === TerminalCompletionItemKind.Folder; + const resourceName = basename(stat.resource.fsPath); + + let label = `${lastWordFolder}${resourceName}`; + + // Normalize suggestion to add a ./ prefix to the start of the path if there isn't + // one already. We may want to change this behavior in the future to go with + // whatever format the user has + if (!lastWordFolderHasDotPrefix) { + label = `.${resourceRequestConfig.pathSeparator}${label}`; + } + + // Ensure directories end with a path separator + if (isDirectory && !label.endsWith(resourceRequestConfig.pathSeparator)) { + label = `${label}${resourceRequestConfig.pathSeparator}`; + } + + // Normalize path separator to `\` on Windows. It should act the exact same as `/` but + // suggestions should all use `\` + if (useForwardSlash) { + label = label.replaceAll('/', '\\'); + } + + resourceCompletions.push({ + label, + provider, + kind, + detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, TerminalCompletionItemKind.File), + isDirectory, + isFile: kind === TerminalCompletionItemKind.File, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); } - if (isDirectory && !label.endsWith(resourceRequestConfig.pathSeparator)) { - label = label + resourceRequestConfig.pathSeparator; - } + } + + // Add parent directory to the bottom of the list because it's not as useful as other suggestions + // + // For example: + // - `|` -> `../` + // - `./src/|` -> `./src/../` + // + // On Windows, the path seprators are normalized to `\`: + // - `./src/|` -> `.\src\..\` + if (!isAbsolutePath && foldersRequested) { + const parentDir = URI.joinPath(cwd, '..' + resourceRequestConfig.pathSeparator); resourceCompletions.push({ - label, - kind, - isDirectory, - isFile: kind === TerminalCompletionItemKind.File, + label: lastWordFolder + '..' + resourceRequestConfig.pathSeparator, + provider, + kind: TerminalCompletionItemKind.Folder, + detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator), + isDirectory: true, + isFile: false, replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length > 0 ? lastWord.length : cursorPosition + replacementLength: lastWord.length }); } return resourceCompletions.length ? resourceCompletions : undefined; } } + +function getFriendlyPath(uri: URI, pathSeparator: string, kind?: TerminalCompletionItemKind): string { + let path = uri.fsPath; + // Ensure folders end with the path separator to differentiate presentation from files + if (kind !== TerminalCompletionItemKind.File && !path.endsWith(pathSeparator)) { + path += pathSeparator; + } + // Ensure drive is capitalized on Windows + if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { + path = `${path[0].toUpperCase()}:${path.slice(2)}`; + } + return path; +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 52d7c1b8c37..efc1efa72e5 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -19,19 +19,18 @@ import { TerminalCapability, type ITerminalCapabilityStore } from '../../../../. import type { IPromptInputModel, IPromptInputModelState } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; import { getListStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { activeContrastBorder } from '../../../../../platform/theme/common/colorRegistry.js'; -import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js'; import type { IXtermCore } from '../../../terminal/browser/xterm-private.js'; import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js'; -import { terminalSuggestConfigSection, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; +import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { LineContext, SimpleCompletionModel } from '../../../../services/suggest/browser/simpleCompletionModel.js'; import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; -import type { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js'; -import { ITerminalCompletion, ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js'; +import { ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js'; import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; export interface ISuggestController { isPasting: boolean; @@ -58,7 +57,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private _enableWidget: boolean = true; private _pathSeparator: string = sep; private _isFilteringDirectories: boolean = false; - private _mostRecentCompletion?: ITerminalCompletion; // TODO: Remove these in favor of prompt input state private _leadingLineContent?: string; @@ -98,7 +96,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest @ITerminalCompletionService private readonly _terminalCompletionService: ITerminalCompletionService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, @IExtensionService private readonly _extensionService: IExtensionService ) { super(); @@ -159,19 +156,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest doNotRequestExtensionCompletions = true; } - const enableExtensionCompletions = this._configurationService.getValue(terminalSuggestConfigSection).enableExtensionCompletions; - if (enableExtensionCompletions && !doNotRequestExtensionCompletions) { + if (!doNotRequestExtensionCompletions) { await this._extensionService.activateByEvent('onTerminalCompletionsRequested'); } - - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); - if (!providedCompletions?.length || token.isCancellationRequested) { - return; - } - this._onDidReceiveCompletions.fire(); - - this._requestedCompletionsIndex = this._promptInputModel.cursorIndex; - this._currentPromptInputState = { value: this._promptInputModel.value, prefix: this._promptInputModel.prefix, @@ -179,8 +166,17 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest cursorIndex: this._promptInputModel.cursorIndex, ghostTextIndex: this._promptInputModel.ghostTextIndex }; + this._requestedCompletionsIndex = this._currentPromptInputState.cursorIndex; - this._leadingLineContent = this._currentPromptInputState.prefix.substring(0, this._requestedCompletionsIndex + this._cursorIndexDelta); + const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); + + if (!providedCompletions?.length || token.isCancellationRequested) { + return; + } + this._onDidReceiveCompletions.fire(); + + this._cursorIndexDelta = this._promptInputModel.cursorIndex - this._requestedCompletionsIndex; + this._leadingLineContent = this._promptInputModel.prefix.substring(0, this._requestedCompletionsIndex + this._cursorIndexDelta); const completions = providedCompletions.flat(); if (!completions?.length) { @@ -193,13 +189,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._leadingLineContent = this._promptInputModel.prefix; } - if (this._mostRecentCompletion?.isDirectory && completions.every(e => e.isDirectory)) { - completions.push(this._mostRecentCompletion); - } - this._mostRecentCompletion = undefined; - - this._cursorIndexDelta = this._currentPromptInputState.cursorIndex - this._requestedCompletionsIndex; - let normalizedLeadingLineContent = this._leadingLineContent; // If there is a single directory in the completions: @@ -234,6 +223,18 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._screen = screen; } + toggleExplainMode(): void { + this._suggestWidget?.toggleExplainMode(); + } + + toggleSuggestionFocus(): void { + this._suggestWidget?.toggleDetailsFocus(); + } + + toggleSuggestionDetails(): void { + this._suggestWidget?.toggleDetails(); + } + resetWidgetSize(): void { this._suggestWidget?.resetWidgetSize(); } @@ -262,13 +263,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // If input has been added let sent = false; - // Quick suggestions + // Quick suggestions - Trigger whenever a new non-whitespace character is used if (!this._terminalSuggestWidgetVisibleContextKey.get()) { if (config.quickSuggestions) { - // TODO: Make the regex code generic - // TODO: Don't use `\[` in bash/zsh - // If first character or first character after a space (or `[` in pwsh), request completions - if (promptInputState.cursorIndex === 1 || promptInputState.prefix.match(/([\s\[])[^\s]$/)) { + if (promptInputState.prefix.match(/[^\s]$/)) { // Never request completions if the last key sequence was up or down as the user was likely // navigating history if (!this._lastUserData?.match(/^\x1b[\[O]?[A-D]$/)) { @@ -373,6 +371,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } const suggestWidget = this._ensureSuggestWidget(this._terminal); suggestWidget.setCompletionModel(model); + this._register(suggestWidget.onDidFocus(() => this._terminal?.focus())); if (!this._promptInputModel || !explicitlyInvoked && model.items.length === 0) { return; } @@ -390,31 +389,46 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }); } + private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget { if (!this._suggestWidget) { - const c = this._terminalConfigurationService.config; - const font = this._terminalConfigurationService.getFont(dom.getActiveWindow()); - const fontInfo: ISimpleSuggestWidgetFontInfo = { - fontFamily: font.fontFamily, - fontSize: font.fontSize, - lineHeight: Math.ceil(1.5 * font.fontSize), - fontWeight: c.fontWeight.toString(), - letterSpacing: font.letterSpacing - }; this._suggestWidget = this._register(this._instantiationService.createInstance( SimpleSuggestWidget, this._container!, this._instantiationService.createInstance(PersistedWidgetSize), - () => fontInfo, - {} + { + statusBarMenuId: MenuId.MenubarTerminalSuggestStatusMenu, + showStatusBarSettingId: TerminalSuggestSettingId.ShowStatusBar + }, )); this._suggestWidget.list.style(getListStyles({ listInactiveFocusBackground: editorSuggestWidgetSelectedBackground, listInactiveFocusOutline: activeContrastBorder })); this._register(this._suggestWidget.onDidSelect(async e => this.acceptSelectedSuggestion(e))); - this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.set(false))); + this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.reset())); this._register(this._suggestWidget.onDidShow(() => this._terminalSuggestWidgetVisibleContextKey.set(true))); + + const element = this._terminal?.element?.querySelector('.xterm-helper-textarea'); + if (element) { + this._register(dom.addDisposableListener(dom.getActiveDocument(), 'click', (event) => { + const target = event.target as HTMLElement; + if (this._terminal?.element?.contains(target)) { + this._suggestWidget?.hide(); + } + })); + } + + this._register(this._suggestWidget.onDidBlurDetails((e) => { + const elt = e.relatedTarget as HTMLElement; + if (this._terminal?.element?.contains(elt)) { + // Do nothing, just the terminal getting focused + // If there was a mouse click, the suggest widget will be + // hidden above + return; + } + this._suggestWidget?.hide(); + })); this._terminalSuggestWidgetVisibleContextKey.set(false); } return this._suggestWidget; @@ -501,8 +515,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest SuggestAddon.lastAcceptedCompletionTimestamp = 0; } - this._mostRecentCompletion = completion; - const commonPrefixLen = commonPrefixLength(replacementText, completionText); const commonPrefix = replacementText.substring(replacementText.length - 1 - commonPrefixLen, replacementText.length - 1); const completionSuffix = completionText.substring(commonPrefixLen); diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index 0b62f790124..30d9c86ae46 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -14,6 +14,9 @@ export const enum TerminalSuggestCommandId { ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', RequestCompletions = 'workbench.action.terminal.requestCompletions', ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', + ToggleDetails = 'workbench.action.terminal.suggestToggleDetails', + ToggleDetailsFocus = 'workbench.action.terminal.suggestToggleDetailsFocus', + ConfigureSettings = 'workbench.action.terminal.configureSuggestSettings', } export const defaultTerminalSuggestCommandsToSkipShell = [ @@ -26,4 +29,6 @@ export const defaultTerminalSuggestCommandsToSkipShell = [ TerminalSuggestCommandId.HideSuggestWidget, TerminalSuggestCommandId.ClearSuggestCache, TerminalSuggestCommandId.RequestCompletions, + TerminalSuggestCommandId.ToggleDetails, + TerminalSuggestCommandId.ToggleDetailsFocus, ]; diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 860bb28015a..e11d8d0587c 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -14,9 +14,30 @@ export const enum TerminalSuggestSettingId { SuggestOnTriggerCharacters = 'terminal.integrated.suggest.suggestOnTriggerCharacters', RunOnEnter = 'terminal.integrated.suggest.runOnEnter', BuiltinCompletions = 'terminal.integrated.suggest.builtinCompletions', - EnableExtensionCompletions = 'terminal.integrated.suggest.enableExtensionCompletions', + WindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', + Providers = 'terminal.integrated.suggest.providers', + ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', } +export const windowsDefaultExecutableExtensions: string[] = [ + 'exe', // Executable file + 'bat', // Batch file + 'cmd', // Command script + 'com', // Command file + + 'msi', // Windows Installer package + + 'ps1', // PowerShell script + + 'vbs', // VBScript file + 'js', // JScript file + 'jar', // Java Archive (requires Java runtime) + 'py', // Python script (requires Python interpreter) + 'rb', // Ruby script (requires Ruby interpreter) + 'pl', // Perl script (requires Perl interpreter) + 'sh', // Shell script (via WSL or third-party tools) +]; + export const terminalSuggestConfigSection = 'terminal.integrated.suggest'; export interface ITerminalSuggestConfiguration { @@ -28,17 +49,31 @@ export interface ITerminalSuggestConfiguration { 'pwshCode': boolean; 'pwshGit': boolean; }; - enableExtensionCompletions: boolean; + providers: { + 'terminal-suggest': boolean; + 'pwsh-shell-integration': boolean; + }; } export const terminalSuggestConfiguration: IStringDictionary = { [TerminalSuggestSettingId.Enabled]: { restricted: true, - markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script. \n\nFor extension provided completions, {5} will also need to be set.", 'PowerShell v7+, zsh, bash, fish', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`', `\`#${TerminalSuggestSettingId.EnableExtensionCompletions}#\``), + markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script.", 'PowerShell v7+, zsh, bash, fish', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`'), type: 'boolean', default: false, tags: ['experimental'], }, + [TerminalSuggestSettingId.Providers]: { + restricted: true, + markdownDescription: localize('suggest.providers', "Providers are enabled by default. Omit them by setting the id of the provider to `false`."), + type: 'object', + properties: {}, + default: { + 'terminal-suggest': true, + 'pwsh-shell-integration': false, + }, + tags: ['experimental'], + }, [TerminalSuggestSettingId.QuickSuggestions]: { restricted: true, markdownDescription: localize('suggest.quickSuggestions', "Controls whether suggestions should automatically show up while typing. Also be aware of the {0}-setting which controls if suggestions are triggered by special characters.", `\`#${TerminalSuggestSettingId.SuggestOnTriggerCharacters}#\``), @@ -83,11 +118,22 @@ export const terminalSuggestConfiguration: IStringDictionary `- ${extension}`).join('\n'), + ), + type: 'object', + default: {}, + tags: ['experimental'], + }, + [TerminalSuggestSettingId.ShowStatusBar]: { restricted: true, - markdownDescription: localize('suggest.enableExtensionCompletions', "Controls whether extension completions are enabled."), + markdownDescription: localize('suggest.showStatusBar', "Controls whether the terminal suggestions status bar should be shown."), type: 'boolean', - default: false, + default: true, tags: ['experimental'], }, }; + + diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 1ce22b4138c..7cdac7c3d67 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IFileService, IFileStatWithMetadata, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js'; -import { TerminalCompletionService, TerminalCompletionItemKind, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; +import { TerminalCompletionService, TerminalCompletionItemKind, TerminalResourceRequestConfig, ITerminalCompletion } from '../../browser/terminalCompletionService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import assert from 'assert'; import { isWindows } from '../../../../../../base/common/platform.js'; @@ -14,14 +14,45 @@ import { createFileStat } from '../../../../../test/common/workbenchTestServices import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +const pathSeparator = isWindows ? '\\' : '/'; + +interface IAssertionTerminalCompletion { + label: string; + detail?: string; + kind?: TerminalCompletionItemKind; +} + +interface IAssertionCommandLineConfig { + replacementIndex: number; + replacementLength: number; +} + +function assertCompletions(actual: ITerminalCompletion[] | undefined, expected: IAssertionTerminalCompletion[], expectedConfig: IAssertionCommandLineConfig) { + assert.deepStrictEqual( + actual?.map(e => ({ + label: e.label, + detail: e.detail ?? '', + kind: e.kind ?? TerminalCompletionItemKind.Folder, + replacementIndex: e.replacementIndex, + replacementLength: e.replacementLength, + })), expected.map(e => ({ + label: e.label.replaceAll('/', pathSeparator), + detail: e.detail ? e.detail.replaceAll('/', pathSeparator) : '', + kind: e.kind ?? TerminalCompletionItemKind.Folder, + replacementIndex: expectedConfig.replacementIndex, + replacementLength: expectedConfig.replacementLength, + })) + ); +} + suite('TerminalCompletionService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let validResources: URI[]; let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean }[]; - const pathSeparator = isWindows ? '\\' : '/'; let terminalCompletionService: TerminalCompletionService; + const provider = 'testProvider'; setup(() => { instantiationService = store.add(new TestInstantiationService()); @@ -36,7 +67,7 @@ suite('TerminalCompletionService', () => { }, async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise { return createFileStat(resource, undefined, undefined, undefined, childResources); - } + }, }); terminalCompletionService = store.add(instantiationService.createInstance(TerminalCompletionService)); validResources = []; @@ -45,10 +76,8 @@ suite('TerminalCompletionService', () => { suite('resolveResources should return undefined', () => { test('if cwd is not provided', async () => { - const resourceRequestConfig: TerminalResourceRequestConfig = { - pathSeparator - }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); assert(!result); }); @@ -58,7 +87,7 @@ suite('TerminalCompletionService', () => { pathSeparator }; validResources = [URI.parse('file:///test')]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); assert(!result); }); }); @@ -66,122 +95,275 @@ suite('TerminalCompletionService', () => { suite('resolveResources should return folder completions', () => { setup(() => { validResources = [URI.parse('file:///test')]; - const childFolder = { resource: URI.parse('file:///test/folder1/'), name: 'folder1', isDirectory: true, isFile: false }; - const childFile = { resource: URI.parse('file:///test/file1.txt'), name: 'file1.txt', isDirectory: false, isFile: true }; - childResources = [childFolder, childFile]; + childResources = [ + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/file1.txt'), isFile: true }, + ]; }); - test('|', async () => { + test('| should return root-level completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 1, - replacementLength: 1 - }]); - }); - test('.|', async () => { + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: '../', detail: '/' }, + ], { replacementIndex: 1, replacementLength: 0 }); + }); + + test('./| should return folder completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.', 2); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 1, - replacementLength: 1 - }]); - }); - test('./|', async () => { + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './../', detail: '/' }, + ], { replacementIndex: 1, replacementLength: 2 }); + }); + + test('cd ./| should return folder completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 1, - replacementLength: 2 - }]); - }); - test('cd |', async () => { + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 2 }); + }); + test('cd ./f| should return folder completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 3 - }]); - }); - test('cd .|', async () => { + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 3 }); + }); + test('cd ~/| should return home folder completions', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test/folder1'),// Updated to reflect home directory + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true, + env: { HOME: '/test/' } + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider); + + assertCompletions(result, [ + { label: '~/', detail: '/test/' }, + ], { replacementIndex: 3, replacementLength: 2 }); + }); + }); + + suite('resolveResources should handle file and folder completion requests correctly', () => { + setup(() => { + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/.hiddenFile'), isFile: true }, + { resource: URI.parse('file:///test/.hiddenFolder/'), isDirectory: true }, + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/file1.txt'), isFile: true }, + ]; + }); + + test('./| should handle hidden files and folders', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd .', 4); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 1 // replacing . - }]); - }); - test('cd ./|', async () => { + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './.hiddenFile', detail: '/test/.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/', detail: '/test/.hiddenFolder/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './file1.txt', detail: '/test/file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './../', detail: '/' }, + ], { replacementIndex: 0, replacementLength: 2 }); + }); + + test('./h| should handle hidden files and folders', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 2 // replacing ./ - }]); - }); - test('cd ./f|', async () => { + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './.hiddenFile', detail: '/test/.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/', detail: '/test/.hiddenFolder/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './file1.txt', detail: '/test/file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './../', detail: '/' }, + ], { replacementIndex: 0, replacementLength: 3 }); + }); + }); + suite('resolveResources edge cases and advanced scenarios', () => { + setup(() => { + validResources = []; + childResources = []; + }); + + if (!isWindows) { + test('/usr/| Missing . should show correct results', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///usr')]; + childResources = []; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider); + + assertCompletions(result, [ + { label: '/usr/', detail: '/' }, + ], { replacementIndex: 0, replacementLength: 5 }); + }); + } + if (isWindows) { + test('.\\folder | Case insensitivity should resolve correctly on Windows', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///C:/test'), + foldersRequested: true, + pathSeparator: '\\', + shouldNormalizePrefix: true + }; + + validResources = [URI.parse('file:///C:/test')]; + childResources = [ + { resource: URI.parse('file:///C:/test/FolderA/'), isDirectory: true }, + { resource: URI.parse('file:///C:/test/anotherFolder/'), isDirectory: true } + ]; + + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider); + + assertCompletions(result, [ + { label: '.\\', detail: 'C:\\test\\' }, + { label: '.\\FolderA\\', detail: 'C:\\test\\FolderA\\' }, + { label: '.\\anotherFolder\\', detail: 'C:\\test\\anotherFolder\\' }, + { label: '.\\..\\', detail: 'C:\\' }, + ], { replacementIndex: 0, replacementLength: 8 }); + }); + } else { + test('./folder | Case sensitivity should resolve correctly on Mac/Unix', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator: '/', + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/FolderA/'), isDirectory: true }, + { resource: URI.parse('file:///test/foldera/'), isDirectory: true } + ]; + + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './FolderA/', detail: '/test/FolderA/' }, + { label: './foldera/', detail: '/test/foldera/' }, + { label: './../', detail: '/' } + ], { replacementIndex: 0, replacementLength: 8 }); + }); + + } + test('| Empty input should resolve to current directory', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/folder2/'), isDirectory: true } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './folder2/', detail: '/test/folder2/' }, + { label: '../', detail: '/' } + ], { replacementIndex: 0, replacementLength: 0 }); + }); + + test('./| Large directory should handle many results gracefully', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 3 // replacing ./f - }]); + validResources = [URI.parse('file:///test')]; + childResources = Array.from({ length: 1000 }, (_, i) => ({ + resource: URI.parse(`file:///test/folder${i}/`), + isDirectory: true + })); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + + assert(result); + // includes the 1000 folders + ./ and ./../ + assert.strictEqual(result?.length, 1002); + assert.strictEqual(result[0].label, `.${pathSeparator}`); + assert.strictEqual(result.at(-1)?.label, `.${pathSeparator}..${pathSeparator}`); + }); + + test('./folder| Folders should be resolved even if the trailing / is missing', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/folder2/'), isDirectory: true } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './folder2/', detail: '/test/folder2/' }, + { label: './../', detail: '/' } + ], { replacementIndex: 1, replacementLength: 9 }); }); }); }); diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index 83ab2165113..b3ee142148d 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -95,11 +95,6 @@ suite('Terminal Contrib Suggest Recordings', () => { setup(async () => { const terminalConfig = { - fontFamily: 'monospace', - fontSize: 12, - fontWeight: 'normal', - letterSpacing: 0, - lineHeight: 1, integrated: { suggest: { enabled: true, @@ -110,14 +105,18 @@ suite('Terminal Contrib Suggest Recordings', () => { pwshCode: true, pwshGit: true }, - enableExtensionCompletions: false + providers: { + 'terminal-suggest': true, + 'pwsh-shell-integration': true, + }, } satisfies ITerminalSuggestConfiguration } }; const instantiationService = workbenchInstantiationService({ configurationService: () => new TestConfigurationService({ files: { autoSave: false }, - terminal: terminalConfig + terminal: terminalConfig, + editor: { fontSize: 14, fontFamily: 'Arial', lineHeight: 12, fontWeight: 'bold' } }) }, store); const terminalConfigurationService = instantiationService.get(ITerminalConfigurationService) as TestTerminalConfigurationService; @@ -126,7 +125,7 @@ suite('Terminal Contrib Suggest Recordings', () => { instantiationService.stub(ITerminalCompletionService, store.add(completionService)); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, new NullLogService)); pwshCompletionProvider = store.add(instantiationService.createInstance(PwshCompletionProviderAddon, new Set(parseCompletionsFromShell(testRawPwshCompletions, -1, -1)), shellIntegrationAddon.capabilities)); - store.add(completionService.registerTerminalCompletionProvider('builtin-pwsh', 'pwsh', pwshCompletionProvider)); + store.add(completionService.registerTerminalCompletionProvider('builtin-pwsh', PwshCompletionProviderAddon.ID, pwshCompletionProvider)); const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; xterm = store.add(new TerminalCtor({ allowProposedApi: true })); capabilities = shellIntegrationAddon.capabilities; diff --git a/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 57543747b13..986b6385629 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -48,6 +48,7 @@ import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js' import { IViewsService } from '../../../services/views/common/viewsService.js'; import { allTestActions, discoverAndRunTests } from './testExplorerActions.js'; import './testingConfigurationUi.js'; +import { URI } from '../../../../base/common/uri.js'; registerSingleton(ITestService, TestService, InstantiationType.Delayed); registerSingleton(ITestResultStorage, TestResultStorage, InstantiationType.Delayed); @@ -279,5 +280,13 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'vscode.testing.getTestsInFile', + handler: async (accessor: ServicesAccessor, uri: URI) => { + const testService = accessor.get(ITestService); + return [...testService.collection.getNodeByUrl(uri)].map(t => TestId.split(t.item.extId)); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(testingConfiguration); diff --git a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 808e68a6ca9..21417a0ea2e 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -709,10 +709,16 @@ class TestResultsPeek extends PeekViewWidget { return defaultMaxHeight; } + if (this.testingPeek.historyVisible.value) { // don't cap height with the history split + return defaultMaxHeight; + } + const lineHeight = this.editor.getOption(EditorOption.lineHeight); // 41 is experimentally determined to be the overhead of the peek view itself // to avoid showing scrollbars by default in its content. - return Math.min(defaultMaxHeight || Infinity, (contentHeight + 41) / lineHeight); + const basePeekOverhead = 41; + + return Math.min(defaultMaxHeight || Infinity, (contentHeight + basePeekOverhead) / lineHeight + 1); } private applyTheme() { diff --git a/code/src/vs/workbench/contrib/testing/common/testId.ts b/code/src/vs/workbench/contrib/testing/common/testId.ts index 0b66669342a..73bac84ca43 100644 --- a/code/src/vs/workbench/contrib/testing/common/testId.ts +++ b/code/src/vs/workbench/contrib/testing/common/testId.ts @@ -77,6 +77,13 @@ export class TestId { return new TestId([...base.path, b]); } + /** + * Splits a test ID into its parts. + */ + public static split(idString: string) { + return idString.split(TestIdPathParts.Delimiter); + } + /** * Gets the string ID resulting from adding b to the base ID. */ diff --git a/code/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/code/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index 50a4b4b6837..02079b12c3f 100644 --- a/code/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/code/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -91,7 +91,7 @@ suite('Color Registry', function () { const docUrl = 'https://raw.githubusercontent.com/microsoft/vscode-docs/main/api/references/theme-color.md'; - const reqContext = await new RequestService(new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); + const reqContext = await new RequestService('local', new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/code/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/code/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index 5fffc20c8a0..535b637dd54 100644 --- a/code/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/code/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -51,13 +51,13 @@ configurationRegistry.registerConfiguration({ properties: { 'timeline.pageSize': { type: ['number', 'null'], - default: null, - markdownDescription: localize('timeline.pageSize', "The number of items to show in the Timeline view by default and when loading more items. Setting to `null` (the default) will automatically choose a page size based on the visible area of the Timeline view."), + default: 50, + markdownDescription: localize('timeline.pageSize', "The number of items to show in the Timeline view by default and when loading more items. Setting to `null` will automatically choose a page size based on the visible area of the Timeline view."), }, 'timeline.pageOnScroll': { type: 'boolean', - default: false, - description: localize('timeline.pageOnScroll', "Experimental. Controls whether the Timeline view will load the next page of items when you scroll to the end of the list."), + default: true, + description: localize('timeline.pageOnScroll', "Controls whether the Timeline view will load the next page of items when you scroll to the end of the list."), }, } }); diff --git a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index bef5074bad8..6cd4321ee42 100644 --- a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -1209,7 +1209,6 @@ export class UserDataProfilesEditorModel extends EditorModel { ); } } else if (isUserDataProfile(copyFrom)) { - this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); profile = await this.userDataProfileImportExportService.createFromProfile( copyFrom, { @@ -1222,7 +1221,6 @@ export class UserDataProfilesEditorModel extends EditorModel { token ?? CancellationToken.None ); } else { - this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); profile = await this.userDataProfileManagementService.createProfile(name, { useDefaultFlags, icon, transient }); } } diff --git a/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 6a2c8ceb17a..261987d460e 100644 --- a/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -189,14 +189,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo { label: localize('replace remote', "Replace Remote"), run: () => { - this.telemetryService.publicLog2<{ source: string; action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflict.syncResource, action: 'acceptLocal' }); this.acceptLocal(conflict, conflict.conflicts[0]); } }, { label: localize('replace local', "Replace Local"), run: () => { - this.telemetryService.publicLog2<{ source: string; action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflict.syncResource, action: 'acceptRemote' }); this.acceptRemote(conflict, conflict.conflicts[0]); } }, diff --git a/code/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts b/code/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts index dde553ec057..33ac5a816c1 100644 --- a/code/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts +++ b/code/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import type { WebviewStyles } from 'vs/workbench/contrib/webview/browser/webview'; +import type { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; +import type { WebviewStyles } from './webview.js'; type KeyEvent = { key: string; diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index d680b5c7780..817658b59f9 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -552,6 +552,9 @@ export class GettingStartedPage extends EditorPane { } else if (stepToExpand.media.type === 'markdown') { this.webview = this.mediaDisposables.add(this.webviewService.createWebviewElement({ options: {}, contentOptions: { localResourceRoots: [stepToExpand.media.root], allowScripts: true }, title: '', extension: undefined })); this.webview.mountTo(this.stepMediaComponent, this.window); + } else if (stepToExpand.media.type === 'video') { + this.webview = this.mediaDisposables.add(this.webviewService.createWebviewElement({ options: {}, contentOptions: { localResourceRoots: [stepToExpand.media.root], allowScripts: true }, title: '', extension: undefined })); + this.webview.mountTo(this.stepMediaComponent, this.window); } } @@ -559,6 +562,7 @@ export class GettingStartedPage extends EditorPane { this.stepsContent.classList.add('image'); this.stepsContent.classList.remove('markdown'); + this.stepsContent.classList.remove('video'); const media = stepToExpand.media; const mediaElement = $('img'); @@ -584,6 +588,7 @@ export class GettingStartedPage extends EditorPane { else if (stepToExpand.media.type === 'svg') { this.stepsContent.classList.add('image'); this.stepsContent.classList.remove('markdown'); + this.stepsContent.classList.remove('video'); const media = stepToExpand.media; this.webview.setHtml(await this.detailsRenderer.renderSVG(media.path)); @@ -621,6 +626,7 @@ export class GettingStartedPage extends EditorPane { this.stepsContent.classList.remove('image'); this.stepsContent.classList.add('markdown'); + this.stepsContent.classList.remove('video'); const media = stepToExpand.media; @@ -702,6 +708,46 @@ export class GettingStartedPage extends EditorPane { } })); } + else if (stepToExpand.media.type === 'video') { + this.stepsContent.classList.add('video'); + this.stepsContent.classList.remove('markdown'); + this.stepsContent.classList.remove('image'); + + const media = stepToExpand.media; + + const themeType = this.themeService.getColorTheme().type; + const videoPath = media.path[themeType]; + const videoPoster = media.poster ? media.poster[themeType] : undefined; + + const rawHTML = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + this.webview.setHtml(rawHTML); + + let isDisposed = false; + this.stepDisposables.add(toDisposable(() => { isDisposed = true; })); + + this.stepDisposables.add(this.themeService.onDidColorThemeChange(async () => { + // Render again since color vars change + const themeType = this.themeService.getColorTheme().type; + const videoPath = media.path[themeType]; + const videoPoster = media.poster ? media.poster[themeType] : undefined; + const body = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + + if (!isDisposed) { // Make sure we weren't disposed of in the meantime + this.webview.setHtml(body); + } + })); + + this.stepDisposables.add(this.webview.onMessage(async e => { + const message: string = e.message as string; + if (message === 'playVideo') { + this.telemetryService.publicLog2('gettingStarted.ActionExecuted', { + command: 'playVideo', + walkthroughId: this.currentWalkthrough?.id, + argument: stepId, + }); + } + })); + } } async selectStepLoose(id: string) { @@ -1340,7 +1386,8 @@ export class GettingStartedPage extends EditorPane { } } } else { - const link = this.instantiationService.createInstance(Link, p, node, { opener: (href) => this.runStepCommand(href) }); + const nodeWithTitle: ILink = matchesScheme(node.href, Schemas.http) || matchesScheme(node.href, Schemas.https) ? { ...node, title: node.href } : node; + const link = this.instantiationService.createInstance(Link, p, nodeWithTitle, { opener: (href) => this.runStepCommand(href) }); this.detailsPageDisposables.add(link); } } @@ -1442,6 +1489,10 @@ export class GettingStartedPage extends EditorPane { stepDescription.appendChild( $('.image-description', { 'aria-label': localize('imageShowing', "Image showing {0}", step.media.altText) }), ); + } else if (step.media.type === 'video') { + stepDescription.appendChild( + $('.video-description', { 'aria-label': localize('videoShowing', "Video showing {0}", step.media.altText) }), + ); } return $('button.getting-started-step', @@ -1460,7 +1511,7 @@ export class GettingStartedPage extends EditorPane { buildStepList(); this.detailsPageDisposables.add(this.contextService.onDidChangeContext(e => { - if (e.affectsSome(contextKeysToWatch)) { + if (e.affectsSome(contextKeysToWatch) && this.currentWalkthrough) { buildStepList(); this.registerDispatchListeners(); this.selectStep(this.editorInput.selectedStep, false); diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts index b7de7bccf06..e1d9858430b 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { GettingStartedPage, inWelcomeContext } from './gettingStarted.js'; @@ -22,7 +22,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; -export class GettingStartedAccessibleView implements IAccessibleViewImplentation { +export class GettingStartedAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; readonly priority = 110; readonly name = 'walkthroughs'; diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 53e8a273940..ca5d099418c 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -201,6 +201,64 @@ export class GettingStartedDetailsRenderer { `; } + async renderVideo(path: URI, poster?: URI): Promise { + const nonce = generateUuid(); + + return ` + + + + + + + + + + + + + + `; + } + private async readAndCacheSVGFile(path: URI): Promise { if (!this.svgCache.has(path)) { const contents = await this.readContentsOfPath(path, false); diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 01a8fe3692b..4b2800fe031 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -35,6 +35,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { DefaultIconPath } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { asWebviewUri } from '../../webview/common/webview.js'; export const HasMultipleNewFileEntries = new RawContextKey('hasMultipleNewFileEntries', false); @@ -83,7 +84,8 @@ export interface IWalkthroughStep { media: | { type: 'image'; path: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; altText: string } | { type: 'svg'; path: URI; altText: string } - | { type: 'markdown'; path: URI; base: URI; root: URI }; + | { type: 'markdown'; path: URI; base: URI; root: URI } + | { type: 'video'; path: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; poster?: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; root: URI; altText: string }; } type StepProgress = { done: boolean }; @@ -205,12 +207,20 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ altText: step.media.altText, path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/' + step.media.path }) }) } - : { - type: 'markdown', - path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/' + step.media.path }) }), - base: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), - root: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), - }, + : step.media.type === 'markdown' + ? { + type: 'markdown', + path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/' + step.media.path }) }), + base: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), + root: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), + } + : { + type: 'video', + path: convertRelativeMediaPathsToWebviewURIs(FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), step.media.path), + altText: step.media.altText, + root: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), + poster: step.media.poster ? convertRelativeMediaPathsToWebviewURIs(FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), step.media.poster) : undefined + }, }); }) }); @@ -368,6 +378,16 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ altText: step.media.svg, }; } + else if (step.media.video) { + const baseURI = FileAccess.uriToFileUri(extension.extensionLocation); + media = { + type: 'video', + path: convertRelativeMediaPathsToWebviewURIs(baseURI, step.media.video), + root: FileAccess.uriToFileUri(extension.extensionLocation), + altText: step.media.altText, + poster: step.media.poster ? convertRelativeMediaPathsToWebviewURIs(baseURI, step.media.poster) : undefined + }; + } // Throw error for unknown walkthrough format else { @@ -681,6 +701,25 @@ const convertInternalMediaPathsToBrowserURIs = (path: string | { hc: string; hcL } }; +const convertRelativeMediaPathsToWebviewURIs = (basePath: URI, path: string | { hc: string; hcLight?: string; dark: string; light: string }): { hcDark: URI; hcLight: URI; dark: URI; light: URI } => { + const convertPath = (path: string) => path.startsWith('https://') + ? URI.parse(path, true) + : asWebviewUri(joinPath(basePath, path)); + + if (typeof path === 'string') { + const converted = convertPath(path); + return { hcDark: converted, hcLight: converted, dark: converted, light: converted }; + } else { + return { + hcDark: convertPath(path.hc), + hcLight: convertPath(path.hcLight ?? path.light), + light: convertPath(path.light), + dark: convertPath(path.dark) + }; + } +}; + + registerAction2(class extends Action2 { constructor() { super({ diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 0b9c875e842..3657036b268 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -579,6 +579,10 @@ align-self: center; } +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { + height: inherit; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.markdown > .getting-started-media { height: inherit; } @@ -598,6 +602,11 @@ justify-content: center; } +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media > video { + max-width: 100%; + max-height: 100%; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-footer { grid-area: footer; align-self: flex-end; diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 53c8ae80511..5b01dd40914 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -67,7 +67,8 @@ export type BuiltinGettingStartedStep = { media: | { type: 'image'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string } | { type: 'svg'; path: string; altText: string } - | { type: 'markdown'; path: string }; + | { type: 'markdown'; path: string } + | { type: 'video'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; poster?: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string }; }; export type BuiltinGettingStartedCategory = { @@ -232,7 +233,6 @@ function createCopilotSetupStep(id: string, button: string, when: string, includ }; } - export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'Setup', diff --git a/code/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/code/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 474a87bd92e..8d37002068d 100644 --- a/code/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/code/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -42,7 +42,7 @@ import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { debugIconStartForeground } from '../../debug/browser/debugColors.js'; import { IExtensionsWorkbenchService, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from '../../extensions/common/extensions.js'; -import { IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { WorkspaceTrustEditorInput } from '../../../services/workspaces/browser/workspaceTrustEditorInput.js'; @@ -889,7 +889,7 @@ export class WorkspaceTrustEditor extends EditorPane { const property = configurationRegistry.getConfigurationProperties()[key]; // cannot be configured in workspace - if (property.scope === ConfigurationScope.APPLICATION || property.scope === ConfigurationScope.MACHINE) { + if (property.scope && (APPLICATION_SCOPES.includes(property.scope) || property.scope === ConfigurationScope.MACHINE)) { return false; } diff --git a/code/src/vs/workbench/electron-sandbox/desktop.main.ts b/code/src/vs/workbench/electron-sandbox/desktop.main.ts index 10527541426..04b054017cf 100644 --- a/code/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/code/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -85,7 +85,12 @@ export class DesktopMain extends Disposable { // Apply custom title override to defaults if any if (isLinux && product.quality === 'stable' && this.configuration.overrideDefaultTitlebarStyle === 'custom') { const configurationRegistry = Registry.as(Extensions.Configuration); - configurationRegistry.registerDefaultConfigurations([{ overrides: { 'window.titleBarStyle': 'custom' } }]); + configurationRegistry.registerDefaultConfigurations([{ + overrides: { + 'window.titleBarStyle': 'custom', + 'window.customTitleBarVisibility': 'auto' + } + }]); } } diff --git a/code/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/code/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index e4fd872ab4f..ded6c47ab61 100644 --- a/code/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/code/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -183,10 +183,16 @@ const apiMenus: IAPIMenu[] = [ }, { key: 'scm/historyItem/context', - id: MenuId.SCMChangesContext, + id: MenuId.SCMHistoryItemContext, description: localize('menus.historyItemContext', "The Source Control history item context menu"), proposed: 'contribSourceControlHistoryItemMenu' }, + { + key: 'scm/historyItem/hover', + id: MenuId.SCMHistoryItemHover, + description: localize('menus.historyItemHover', "The Source Control history item hover menu"), + proposed: 'contribSourceControlHistoryItemMenu' + }, { key: 'scm/historyItemRef/context', id: MenuId.SCMHistoryItemRefContext, diff --git a/code/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts b/code/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts index 514e1ce72da..08f8436fdf7 100644 --- a/code/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts +++ b/code/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts @@ -81,11 +81,11 @@ export class AuthenticationUsageService extends Disposable implements IAuthentic } } - this._authenticationService.onDidRegisterAuthenticationProvider( + this._register(this._authenticationService.onDidRegisterAuthenticationProvider( provider => this._queue.queue( () => this._addExtensionsToCache(provider.id) ) - ); + )); } async initializeExtensionUsageCache(): Promise { diff --git a/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index ee504955a5a..e716f2af8d8 100644 --- a/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -225,7 +225,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili declare readonly _serviceBrand: undefined; - private static readonly DEFAULT_SIZE = { width: 800, height: 600 }; + private static readonly DEFAULT_SIZE = { width: 1024, height: 768 }; private static WINDOW_IDS = getWindowId(mainWindow) + 1; // start from the main window ID + 1 diff --git a/code/src/vs/workbench/services/configuration/browser/configuration.ts b/code/src/vs/workbench/services/configuration/browser/configuration.ts index 217084c9e9b..a3d2d34f58c 100644 --- a/code/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/code/src/vs/workbench/services/configuration/browser/configuration.ts @@ -11,7 +11,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult, FileOperation, FileOperationEvent } from '../../../../platform/files/common/files.js'; import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, UserSettings } from '../../../../platform/configuration/common/configurationModels.js'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from '../common/configurationModels.js'; -import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, APPLY_ALL_PROFILES_SETTING } from '../common/configuration.js'; +import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, APPLY_ALL_PROFILES_SETTING, APPLICATION_SCOPES } from '../common/configuration.js'; import { IStoredWorkspaceFolder } from '../../../../platform/workspaces/common/workspaces.js'; import { WorkbenchState, IWorkspaceFolder, IWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; import { ConfigurationScope, Extensions, IConfigurationRegistry, OVERRIDE_PROPERTY_REGEX } from '../../../../platform/configuration/common/configurationRegistry.js'; @@ -133,7 +133,7 @@ export class ApplicationConfiguration extends UserSettings { uriIdentityService: IUriIdentityService, logService: ILogService, ) { - super(userDataProfilesService.defaultProfile.settingsResource, { scopes: [ConfigurationScope.APPLICATION], skipUnregistered: true }, uriIdentityService.extUri, fileService, logService); + super(userDataProfilesService.defaultProfile.settingsResource, { scopes: APPLICATION_SCOPES, skipUnregistered: true }, uriIdentityService.extUri, fileService, logService); this._register(this.onDidChange(() => this.reloadConfigurationScheduler.schedule())); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); } diff --git a/code/src/vs/workbench/services/configuration/browser/configurationService.ts b/code/src/vs/workbench/services/configuration/browser/configurationService.ts index b87b49442b5..c56106ddd6a 100644 --- a/code/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/code/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -15,9 +15,9 @@ import { ConfigurationModel, ConfigurationChangeEvent, mergeChanges } from '../. import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange, ConfigurationTargetToString, IConfigurationUpdateOverrides, isConfigurationUpdateOverrides, IConfigurationService, IConfigurationUpdateOptions } from '../../../../platform/configuration/common/configuration.js'; import { IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from '../../../../platform/configuration/common/configurations.js'; import { Configuration } from '../common/configurationModels.js'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings, PROFILE_SCOPES, LOCAL_MACHINE_PROFILE_SCOPES, profileSettingsSchemaId, APPLY_ALL_PROFILES_SETTING } from '../common/configuration.js'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings, PROFILE_SCOPES, LOCAL_MACHINE_PROFILE_SCOPES, profileSettingsSchemaId, APPLY_ALL_PROFILES_SETTING, APPLICATION_SCOPES } from '../common/configuration.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope, IConfigurationPropertySchema, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_PATTERN, resourceLanguageSettingsSchemaId, configurationDefaultsSchemaId } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope, IConfigurationPropertySchema, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_PATTERN, resourceLanguageSettingsSchemaId, configurationDefaultsSchemaId, applicationMachineSettings } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, getStoredWorkspaceFolder, toWorkspaceFolders } from '../../../../platform/workspaces/common/workspaces.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ConfigurationEditing, EditableConfigurationTarget } from '../common/configurationEditing.js'; @@ -49,9 +49,11 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { runWhenWindowIdle } from '../../../../base/browser/dom.js'; function getLocalUserConfigurationScopes(userDataProfile: IUserDataProfile, hasRemote: boolean): ConfigurationScope[] | undefined { - return (userDataProfile.isDefault || userDataProfile.useDefaultFlags?.settings) - ? hasRemote ? LOCAL_MACHINE_SCOPES : undefined - : hasRemote ? LOCAL_MACHINE_PROFILE_SCOPES : PROFILE_SCOPES; + const isDefaultProfile = userDataProfile.isDefault || userDataProfile.useDefaultFlags?.settings; + if (isDefaultProfile) { + return hasRemote ? LOCAL_MACHINE_SCOPES : undefined; + } + return hasRemote ? LOCAL_MACHINE_PROFILE_SCOPES : PROFILE_SCOPES; } class Workspace extends BaseWorkspace { @@ -492,7 +494,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } isSettingAppliedForAllProfiles(key: string): boolean { - if (this.configurationRegistry.getConfigurationProperties()[key]?.scope === ConfigurationScope.APPLICATION) { + const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope; + if (scope && APPLICATION_SCOPES.includes(scope)) { return true; } const allProfilesSettings = this.getValue(APPLY_ALL_PROFILES_SETTING) ?? []; @@ -780,7 +783,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat const configurationProperties = this.configurationRegistry.getConfigurationProperties(); const changedKeys: string[] = []; for (const changedKey of change.keys) { - if (configurationProperties[changedKey]?.scope === ConfigurationScope.APPLICATION) { + const scope = configurationProperties[changedKey]?.scope; + if (scope && APPLICATION_SCOPES.includes(scope)) { changedKeys.push(changedKey); if (changedKey === APPLY_ALL_PROFILES_SETTING) { for (const previousAllProfileSetting of previousAllProfilesSettings) { @@ -1124,7 +1128,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat if (target === ConfigurationTarget.USER) { if (this.remoteUserConfiguration) { const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope; - if (scope === ConfigurationScope.MACHINE || scope === ConfigurationScope.MACHINE_OVERRIDABLE) { + if (scope === ConfigurationScope.MACHINE || scope === ConfigurationScope.MACHINE_OVERRIDABLE || scope === ConfigurationScope.APPLICATION_MACHINE) { return EditableConfigurationTarget.USER_REMOTE; } if (this.inspect(key).userRemoteValue !== undefined) { @@ -1207,6 +1211,7 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo const machineSettingsSchema: IJSONSchema = { properties: Object.assign({}, + applicationMachineSettings.properties, machineSettings.properties, machineOverridableSettings.properties, windowSettings.properties, diff --git a/code/src/vs/workbench/services/configuration/common/configuration.ts b/code/src/vs/workbench/services/configuration/common/configuration.ts index a97808cda18..12e011d3977 100644 --- a/code/src/vs/workbench/services/configuration/common/configuration.ts +++ b/code/src/vs/workbench/services/configuration/common/configuration.ts @@ -24,11 +24,11 @@ export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; export const tasksSchemaId = 'vscode://schemas/tasks'; -export const APPLICATION_SCOPES = [ConfigurationScope.APPLICATION]; +export const APPLICATION_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.APPLICATION_MACHINE]; export const PROFILE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const LOCAL_MACHINE_PROFILE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ...LOCAL_MACHINE_PROFILE_SCOPES]; -export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; +export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.APPLICATION_MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; diff --git a/code/src/vs/workbench/services/configuration/common/configurationEditing.ts b/code/src/vs/workbench/services/configuration/common/configurationEditing.ts index 31864ea3105..150c993d79d 100644 --- a/code/src/vs/workbench/services/configuration/common/configurationEditing.ts +++ b/code/src/vs/workbench/services/configuration/common/configurationEditing.ts @@ -13,7 +13,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { ITextFileService } from '../../textfile/common/textfiles.js'; import { IConfigurationUpdateOptions, IConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js'; -import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT, FOLDER_SCOPES, IWorkbenchConfigurationService } from './configuration.js'; +import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT, FOLDER_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES } from './configuration.js'; import { FileOperationError, FileOperationResult, IFileService } from '../../../../platform/files/common/files.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_REGEX } from '../../../../platform/configuration/common/configurationRegistry.js'; @@ -519,7 +519,7 @@ export class ConfigurationEditing { if (target === EditableConfigurationTarget.WORKSPACE) { if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_REGEX.test(operation.key)) { - if (configurationScope === ConfigurationScope.APPLICATION) { + if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) { throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation); } if (configurationScope === ConfigurationScope.MACHINE) { diff --git a/code/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/code/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 833dec287e0..cd3b770a0d1 100644 --- a/code/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/code/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -751,6 +751,11 @@ suite('WorkspaceConfigurationService - Folder', () => { 'default': 'isSet', scope: ConfigurationScope.MACHINE }, + 'configurationService.folder.applicationMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION_MACHINE + }, 'configurationService.folder.machineOverridableSetting': { 'type': 'string', 'default': 'isSet', @@ -779,6 +784,14 @@ suite('WorkspaceConfigurationService - Folder', () => { minimumVersion: '1.0.0', } }, + 'configurationService.folder.policyObjectSetting': { + 'type': 'object', + 'default': {}, + policy: { + name: 'configurationService.folder.policyObjectSetting', + minimumVersion: '1.0.0', + } + }, } }); @@ -827,7 +840,20 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('defaults', () => { - assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet', 'policySetting': 'isSet' } }); + assert.deepStrictEqual(testObject.getValue('configurationService'), + { + 'folder': { + 'applicationSetting': 'isSet', + 'machineSetting': 'isSet', + 'applicationMachineSetting': 'isSet', + 'machineOverridableSetting': 'isSet', + 'testSetting': 'isSet', + 'languageSetting': 'isSet', + 'restrictedSetting': 'isSet', + 'policySetting': 'isSet', + 'policyObjectSetting': {} + } + }); }); test('globals override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { @@ -933,7 +959,25 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); })); - test('get application scope settings are not loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('application machine overridable settings are not read from workspace', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting'), 'userValue'); + })); + + test('application machine overridable settings are not read from workspace when workspace folder uri is passed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + })); + + test('get application scope settings are loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "workspaceValue" }')); @@ -983,6 +1027,56 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); })); + test('get application machine overridable scope settings are loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-2": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-2": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-2'), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.applicationMachineSetting-2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-2'), 'userValue'); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-2'), 'userValue'); + })); + + test('get application machine overridable scope settings are loaded after defaults are registered when workspace folder uri is passed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-3": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-3": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.applicationMachineSetting-3': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + })); + test('get machine scope settings are not loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "workspaceValue" }')); @@ -1052,6 +1146,19 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, undefined); })); + test('policy value override all for object type setting', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationService.folder.policyObjectSetting": {"a": true} }')); + return promise; + }); + + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policyObjectSetting": {"b": true} }')); + await testObject.reloadConfiguration(); + + assert.deepStrictEqual(testObject.getValue('configurationService.folder.policyObjectSetting'), { a: true }); + })); + test('reload configuration emits events after global configuraiton changes', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); const target = sinon.spy(); @@ -1333,6 +1440,11 @@ suite('WorkspaceConfigurationService - Folder', () => { .then(() => assert.fail('Should not be supported'), (e) => assert.strictEqual(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); }); + test('update application machine overridable setting into workspace configuration in a workspace is not supported', () => { + return testObject.updateValue('configurationService.folder.applicationMachineSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, { donotNotifyError: true }) + .then(() => assert.fail('Should not be supported'), (e) => assert.strictEqual(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); + }); + test('update machine setting into workspace configuration in a workspace is not supported', () => { return testObject.updateValue('configurationService.folder.machineSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, { donotNotifyError: true }) .then(() => assert.fail('Should not be supported'), (e) => assert.strictEqual(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE)); @@ -1587,6 +1699,11 @@ suite('WorkspaceConfigurationService - Profiles', () => { 'default': 'isSet', scope: ConfigurationScope.APPLICATION }, + 'configurationService.profiles.applicationMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION_MACHINE + }, 'configurationService.profiles.testSetting': { 'type': 'string', 'default': 'isSet', @@ -1693,6 +1810,13 @@ suite('WorkspaceConfigurationService - Profiles', () => { assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting'), 'applicationValue'); })); + test('update application machine setting', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('configurationService.profiles.applicationMachineSetting', 'applicationValue'); + + assert.deepStrictEqual(JSON.parse((await fileService.readFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource)).value.toString()), { 'configurationService.profiles.applicationMachineSetting': 'applicationValue', 'configurationService.profiles.applicationSetting2': 'applicationValue', 'configurationService.profiles.testSetting2': 'userValue' }); + assert.strictEqual(testObject.getValue('configurationService.profiles.applicationMachineSetting'), 'applicationValue'); + })); + test('update normal setting', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await testObject.updateValue('configurationService.profiles.testSetting', 'profileValue'); @@ -2006,21 +2130,21 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); - assert.strictEqual(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue'); + assert.strictEqual(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue'); })); test('application settings are not read from workspace when folder is passed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); - assert.strictEqual(testObject.getValue('configurationService.folder.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + assert.strictEqual(testObject.getValue('configurationService.workspace.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); })); test('machine settings are not read from workspace', () => runWithFakedTimers({ useFakeTimers: true }, async () => { @@ -2688,6 +2812,11 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { 'default': 'isSet', scope: ConfigurationScope.MACHINE }, + 'configurationService.remote.applicationMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION_MACHINE + }, 'configurationService.remote.machineOverridableSetting': { 'type': 'string', 'default': 'isSet', @@ -2752,7 +2881,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { })); } - test('remote settings override globals', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('remote machine settings override globals', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }')); registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); @@ -2760,7 +2889,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.getValue('configurationService.remote.machineSetting'), 'remoteValue'); })); - test('remote settings override globals after remote provider is registered on activation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('remote machine settings override globals after remote provider is registered on activation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }')); resolveRemoteEnvironment(); registerRemoteFileSystemProviderOnActivation(); @@ -2768,7 +2897,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.getValue('configurationService.remote.machineSetting'), 'remoteValue'); })); - test('remote settings override globals after remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('remote machine settings override globals after remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }')); registerRemoteFileSystemProvider(); await initialize(); @@ -2816,6 +2945,70 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.getValue('configurationService.remote.machineSetting'), 'isSet'); })); + test('remote application machine settings override globals', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + })); + + test('remote application machine settings override globals after remote provider is registered on activation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + resolveRemoteEnvironment(); + registerRemoteFileSystemProviderOnActivation(); + await initialize(); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + })); + + test('remote application machine settings override globals after remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + registerRemoteFileSystemProvider(); + await initialize(); + const promise = new Promise((c, e) => { + disposables.add(testObject.onDidChangeConfiguration(event => { + try { + assert.strictEqual(event.source, ConfigurationTarget.USER); + assert.deepStrictEqual([...event.affectedKeys], ['configurationService.remote.applicationMachineSetting']); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + c(); + } catch (error) { + e(error); + } + })); + }); + resolveRemoteEnvironment(); + return promise; + })); + + test('remote application machine settings override globals after remote provider is registered on activation and remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + registerRemoteFileSystemProviderOnActivation(); + await initialize(); + const promise = new Promise((c, e) => { + disposables.add(testObject.onDidChangeConfiguration(event => { + try { + assert.strictEqual(event.source, ConfigurationTarget.USER); + assert.deepStrictEqual([...event.affectedKeys], ['configurationService.remote.applicationMachineSetting']); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + c(); + } catch (error) { + e(error); + } + })); + }); + resolveRemoteEnvironment(); + return promise; + })); + + test('application machine settings in local user settings does not override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "globalValue" }')); + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'isSet'); + })); + test('machine overridable settings in local user settings does not override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.machineOverridableSetting": "globalValue" }')); registerRemoteFileSystemProvider(); @@ -2842,6 +3035,16 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.inspect('configurationService.remote.machineSetting').userRemoteValue, 'machineValue'); })); + test('application machine setting is written in remote settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + await testObject.updateValue('configurationService.remote.applicationMachineSetting', 'machineValue'); + await testObject.reloadConfiguration(); + const actual = testObject.inspect('configurationService.remote.applicationMachineSetting'); + assert.strictEqual(actual.userRemoteValue, 'machineValue'); + })); + test('machine overridable setting is written in remote settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); diff --git a/code/src/vs/workbench/services/driver/browser/driver.ts b/code/src/vs/workbench/services/driver/browser/driver.ts index d78e55aa973..bb11f37a641 100644 --- a/code/src/vs/workbench/services/driver/browser/driver.ts +++ b/code/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,18 +133,54 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } + if (isHTMLDivElement(element)) { + // Edit context is enabled + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + const selectionStart = editContext.selectionStart; + const selectionEnd = editContext.selectionEnd; + const event = new TextUpdateEvent('textupdate', { + updateRangeStart: selectionStart, + updateRangeEnd: selectionEnd, + text, + selectionStart: selectionStart + text.length, + selectionEnd: selectionStart + text.length, + compositionStart: 0, + compositionEnd: 0 + }); + editContext.dispatchEvent(event); + } else if (isHTMLTextAreaElement(element)) { + const start = element.selectionStart; + const newStart = start + text.length; + const value = element.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + element.value = newValue; + element.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + element.dispatchEvent(event); + } + } - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); + async getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }> { + const element = mainWindow.document.querySelector(selector); + if (!element) { + throw new Error(`Editor not found: ${selector}`); + } + if (isHTMLDivElement(element)) { + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + return { selectionStart: editContext.selectionStart, selectionEnd: editContext.selectionEnd }; + } else if (isHTMLTextAreaElement(element)) { + return { selectionStart: element.selectionStart, selectionEnd: element.selectionEnd }; + } else { + throw new Error(`Unknown type of element: ${selector}`); + } } async getTerminalBuffer(selector: string): Promise { diff --git a/code/src/vs/workbench/services/driver/common/driver.ts b/code/src/vs/workbench/services/driver/common/driver.ts index 5a00a201414..4cc5952df5d 100644 --- a/code/src/vs/workbench/services/driver/common/driver.ts +++ b/code/src/vs/workbench/services/driver/common/driver.ts @@ -38,6 +38,7 @@ export interface IWindowDriver { getElements(selector: string, recursive: boolean): Promise; getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }>; typeInEditor(selector: string, text: string): Promise; + getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }>; getTerminalBuffer(selector: string): Promise; writeInTerminal(selector: string, text: string): Promise; getLocaleInfo(): Promise; diff --git a/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 0a909924e60..2ad63cc370b 100644 --- a/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -80,7 +80,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench }); this._register(this.globalExtensionEnablementService.onDidChangeEnablement(({ extensions, source }) => this._onDidChangeGloballyDisabledExtensions(extensions, source))); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this._onDidChangeExtensions([], [], false))); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this._onDidChangeExtensions([], [], false))); // delay notification for extensions disabled until workbench restored if (this.allUserExtensionsDisabled) { diff --git a/code/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts b/code/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts index edcd2c4775c..cd083d884f9 100644 --- a/code/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts +++ b/code/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts @@ -6,7 +6,6 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from '../../../../platform/extensionManagement/common/extensionsProfileScannerService.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -18,10 +17,9 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, ) { - super(environmentService.userRoamingDataHome, fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + super(environmentService.userRoamingDataHome, fileService, userDataProfilesService, uriIdentityService, logService); } } diff --git a/code/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/code/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 1deee908d89..927712e5b4b 100644 --- a/code/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/code/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -92,6 +92,9 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise; updateMetadata(local: ILocalExtension, metadata: Partial): Promise; + + isPublisherTrusted(extension: IGalleryExtension): boolean; + trustPublishers(...publishers: string[]): void; } export const enum EnablementState { diff --git a/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 7bc18909d7f..85385c2fcbb 100644 --- a/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event, EventMultiplexer } from '../../../../base/common/event.js'; +import './media/extensionManagement.css'; import { ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, IProductVersion, ExtensionInstallSource, DidUpdateExtensionMetadata, - UninstallExtensionInfo + UninstallExtensionInfo, + IAllowedExtensionsService, } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, IWorkbenchInstallOptions, UninstallExtensionOnServerEvent } from './extensionManagement.js'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from '../../../../platform/extensions/common/extensions.js'; @@ -22,7 +24,7 @@ import { localize } from '../../../../nls.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { Schemas } from '../../../../base/common/network.js'; import { IDownloadService } from '../../../../platform/download/common/download.js'; -import { coalesce } from '../../../../base/common/arrays.js'; +import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { IDialogService, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; import Severity from '../../../../base/common/severity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; @@ -43,6 +45,11 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { verifiedPublisherIcon } from './extensionsIcons.js'; +import { Codicon } from '../../../../base/common/codicons.js'; + +const TrustedPublishersStorageKey = 'extensions.trustedPublishers'; function isGalleryExtension(extension: IResourceExtension | IGalleryExtension): extension is IGalleryExtension { return extension.type === 'gallery'; @@ -52,6 +59,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench declare readonly _serviceBrand: undefined; + private readonly defaultTrustedPublishers: readonly string[]; + private readonly _onInstallExtension = this._register(new Emitter()); readonly onInstallExtension: Event; @@ -98,10 +107,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, + @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); + this.defaultTrustedPublishers = productService.trustedExtensionPublishers ?? []; this.workspaceExtensionManagementService = this._register(this.instantiationService.createInstance(WorkspaceExtensionsManagementService)); this.onDidEnableExtensions = this.workspaceExtensionManagementService.onDidChangeInvalidExtensions; @@ -159,6 +171,18 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this._register(onDidProfileAwareUpdateExtensionMetadaEventMultiplexer.add(server.extensionManagementService.onProfileAwareDidUpdateExtensionMetadata)); this._register(onDidChangeProfileEventMultiplexer.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); } + + this._register(this.onProfileAwareDidInstallExtensions(results => { + const untrustedPublishers = new Set(); + for (const result of results) { + if (result.local && result.source && !URI.isUri(result.source) && !this.isPublisherTrusted(result.source)) { + untrustedPublishers.add(result.source.publisher); + } + } + if (untrustedPublishers.size) { + this.trustPublishers(...untrustedPublishers); + } + })); } async getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise { @@ -424,7 +448,20 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const extensionsByServer = new Map(); await Promise.all(extensions.map(async ({ extension, options }) => { try { - const servers = await this.validateAndGetExtensionManagementServersToInstall(extension, options); + const manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); + if (!manifest) { + throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", extension.displayName || extension.name)); + } + + if (options?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { + await this.checkForWorkspaceTrust(manifest, false); + + if (!options?.donotIncludePackAndDependencies) { + await this.checkInstallingExtensionOnWeb(extension, manifest); + } + } + + const servers = await this.getExtensionManagementServersToInstall(extension, manifest, options); if (!options.isMachineScoped && this.isExtensionsSyncEnabled()) { if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) @@ -460,7 +497,22 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } async installFromGallery(gallery: IGalleryExtension, installOptions?: IWorkbenchInstallOptions): Promise { - const servers = await this.validateAndGetExtensionManagementServersToInstall(gallery, installOptions); + const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); + if (!manifest) { + throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); + } + + if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { + await this.checkForTrustedPublisher(gallery, manifest, !installOptions?.donotIncludePackAndDependencies); + + await this.checkForWorkspaceTrust(manifest, false); + + if (!installOptions?.donotIncludePackAndDependencies) { + await this.checkInstallingExtensionOnWeb(gallery, manifest); + } + } + + const servers = await this.getExtensionManagementServersToInstall(gallery, manifest, installOptions); if (!installOptions || isUndefined(installOptions.isMachineScoped)) { const isMachineScoped = await this.hasToFlagExtensionsMachineScoped([gallery]); installOptions = { ...(installOptions || {}), isMachineScoped }; @@ -605,13 +657,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } } - private async validateAndGetExtensionManagementServersToInstall(gallery: IGalleryExtension, installOptions?: IWorkbenchInstallOptions): Promise { - - const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); - if (!manifest) { - return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); - } - + private async getExtensionManagementServersToInstall(gallery: IGalleryExtension, manifest: IExtensionManifest, installOptions?: IWorkbenchInstallOptions): Promise { const servers: IExtensionManagementServer[] = []; if (installOptions?.servers?.length) { @@ -644,14 +690,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw error; } - if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { - await this.checkForWorkspaceTrust(manifest, false); - } - - if (!installOptions?.donotIncludePackAndDependencies) { - await this.checkInstallingExtensionOnWeb(gallery, manifest); - } - return servers; } @@ -751,7 +789,188 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw new Error('No extension server found'); } - protected async checkForWorkspaceTrust(manifest: IExtensionManifest, requireTrust: boolean): Promise { + private async checkForTrustedPublisher(extension: IGalleryExtension, manifest: IExtensionManifest, checkForPackAndDependencies: boolean): Promise { + if (this.isPublisherTrusted(extension)) { + return; + } + + const otherUntrustedPublishers = checkForPackAndDependencies ? await this.getOtherUntrustedPublishers(manifest) : []; + + type TrustPublisherClassification = { + owner: 'sandy081'; + comment: 'Report the action taken by the user on the publisher trust dialog'; + action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The action taken by the user on the publisher trust dialog. Can be trust, learn more or cancel.' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension for which the publisher trust dialog was shown.' }; + }; + type TrustPublisherEvent = { + action: string; + extensionId: string; + }; + + const installButton: IPromptButton = { + label: otherUntrustedPublishers.length ? localize({ key: 'trust publishers and install', comment: ['&& denotes a mnemonic'] }, "Trust Publishers & &&Install") : localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), + run: () => { + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: extension.identifier.id }); + this.trustPublishers(extension.publisher); + } + }; + + const learnMoreButton: IPromptButton = { + label: localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"), + run: () => { + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'learn', extensionId: extension.identifier.id }); + this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('vscode.open', URI.parse('https://aka.ms/vscode-extension-security'))); + throw new CancellationError(); + } + }; + + const getPublisherLink = ({ publisherDisplayName, publisher }: { publisherDisplayName: string; publisher: string }) => { + return `[${publisherDisplayName}](${joinPath(URI.parse(this.productService.extensionsGallery!.publisherUrl), publisher)})`; + }; + const unverifiedLink = 'https://aka.ms/vscode-verify-publisher'; + + const title = otherUntrustedPublishers.length + ? otherUntrustedPublishers.length === 1 + ? localize('checkTwoTrustedPublishersTitle', "Do you trust publishers \"{0}\" and \"{1}\"?", extension.publisherDisplayName, otherUntrustedPublishers[0].publisherDisplayName) + : localize('checkAllTrustedPublishersTitle', "Do you trust the publisher \"{0}\" and {1} others?", extension.publisherDisplayName, otherUntrustedPublishers.length) + : localize('checkTrustedPublisherTitle', "Do you trust the publisher \"{0}\"?", extension.publisherDisplayName); + + const customMessage = new MarkdownString('', { supportThemeIcons: true, isTrusted: true }); + + if (otherUntrustedPublishers.length) { + customMessage.appendMarkdown(localize('extension published by message', "The extension {0} is published by {1}.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); + customMessage.appendMarkdown(' '); + const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`).toString(); + if (otherUntrustedPublishers.length === 1) { + customMessage.appendMarkdown(localize('singleUntrustedPublisher', "Installing this extension will also install [extensions]({0}) published by {1}.", commandUri, getPublisherLink(otherUntrustedPublishers[0]))); + } else { + customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) published by {1} and {2}.", commandUri, otherUntrustedPublishers.slice(0, otherUntrustedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(otherUntrustedPublishers[otherUntrustedPublishers.length - 1]))); + } + customMessage.appendMarkdown(' '); + customMessage.appendMarkdown(localize('firstTimeInstallingMessage', "This is the first time you're installing extensions from these publishers.")); + + const allPublishers = [extension, ...otherUntrustedPublishers]; + const unverfiiedPublishers = allPublishers.filter(p => !p.publisherDomain?.verified); + const verifiedPublishers = allPublishers.filter(p => p.publisherDomain?.verified); + + if (verifiedPublishers.length) { + customMessage.appendText('\n'); + for (const publisher of verifiedPublishers) { + if (publisher.publisherDomain?.verified) { + customMessage.appendText('\n'); + const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[${URI.parse(publisher.publisherDomain.link).authority}](${publisher.publisherDomain.link})`); + customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); + } else { + unverfiiedPublishers.push(publisher); + } + } + if (unverfiiedPublishers.length) { + customMessage.appendText('\n'); + if (unverfiiedPublishers.length === 1) { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisherWithName', "{0} is **not** [verified]({1}).", getPublisherLink(unverfiiedPublishers[0]), unverifiedLink)}`); + } else { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublishers', "{0} and {1} are **not** [verified]({2}).", unverfiiedPublishers.slice(0, unverfiiedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(unverfiiedPublishers[unverfiiedPublishers.length - 1]), unverifiedLink)}`); + } + } + } else { + customMessage.appendText('\n'); + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('allUnverifed', "All publishers are **not** [verified]({0}).", unverifiedLink)}`); + } + + } else { + customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); + customMessage.appendText('\n'); + if (extension.publisherDomain?.verified) { + const publisherVerifiedMessage = localize('verifiedPublisher', "{0} has verified ownership of {1}.", getPublisherLink(extension), `[${URI.parse(extension.publisherDomain.link).authority}](${extension.publisherDomain.link})`); + customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); + } else { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisher', "{0} is **not** [verified]({1}).", getPublisherLink(extension), unverifiedLink)}`); + } + } + + customMessage.appendText('\n\n'); + if (otherUntrustedPublishers.length) { + customMessage.appendMarkdown(localize('message4', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publishers.", this.productService.nameLong)); + } else { + customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); + } + + await this.dialogService.prompt({ + message: title, + type: Severity.Warning, + buttons: [installButton, learnMoreButton], + cancelButton: { + run: () => { + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'cancel', extensionId: extension.identifier.id }); + throw new CancellationError(); + } + }, + custom: { + markdownDetails: [{ markdown: customMessage, classes: ['extensions-management-publisher-trust-dialog'] }], + closeOnLinkClick: true, + } + }); + + } + + private async getOtherUntrustedPublishers(manifest: IExtensionManifest): Promise<{ publisher: string; publisherDisplayName: string; publisherDomain?: { link: string; verified: boolean } }[]> { + const infos = []; + for (const id of [...(manifest.extensionPack ?? []), ...(manifest.extensionDependencies ?? [])]) { + const [publisherId] = id.split('.'); + if (publisherId.toLowerCase() === manifest.publisher.toLowerCase()) { + continue; + } + if (this.isPublisherUserTrusted(publisherId.toLowerCase())) { + continue; + } + infos.push(id); + } + if (!infos.length) { + return []; + } + const extensions = new Map(); + await this.getDependenciesAndPackedExtensionsRecursively(infos, extensions, CancellationToken.None); + const publishers = new Map(); + for (const [, extension] of extensions) { + if (this.isPublisherTrusted(extension)) { + continue; + } + publishers.set(extension.publisherDisplayName, extension); + } + return [...publishers.values()]; + } + + private async getDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map, token: CancellationToken): Promise { + if (toGet.length === 0) { + return; + } + + const extensions = await this.extensionGalleryService.getExtensions(toGet.map(id => ({ id })), token); + for (let idx = 0; idx < extensions.length; idx++) { + const extension = extensions[idx]; + result.set(extension.identifier.id.toLowerCase(), extension); + } + toGet = []; + for (const extension of extensions) { + if (isNonEmptyArray(extension.properties.dependencies)) { + for (const id of extension.properties.dependencies) { + if (!result.has(id.toLowerCase())) { + toGet.push(id); + } + } + } + if (isNonEmptyArray(extension.properties.extensionPack)) { + for (const id of extension.properties.extensionPack) { + if (!result.has(id.toLowerCase())) { + toGet.push(id); + } + } + } + } + return this.getDependenciesAndPackedExtensionsRecursively(toGet, result, token); + } + + private async checkForWorkspaceTrust(manifest: IExtensionManifest, requireTrust: boolean): Promise { if (requireTrust || this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(manifest) === false) { const buttons: WorkspaceTrustRequestButton[] = []; buttons.push({ label: localize('extensionInstallWorkspaceTrustButton', "Trust Workspace & Install"), type: 'ContinueWithTrust' }); @@ -881,6 +1100,35 @@ export class ExtensionManagementService extends Disposable implements IWorkbench registerParticipant() { throw new Error('Not Supported'); } installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { throw new Error('Not Supported'); } + + isPublisherTrusted(extension: IGalleryExtension): boolean { + const publisher = extension.publisher.toLowerCase(); + if (this.defaultTrustedPublishers.includes(publisher) || this.defaultTrustedPublishers.includes(extension.publisherDisplayName.toLowerCase())) { + return true; + } + + // Check if the extension is allowed by publisher or extension id + if (this.allowedExtensionsService.allowedExtensionsConfigValue && this.allowedExtensionsService.isAllowed(extension)) { + return true; + } + + return this.isPublisherUserTrusted(publisher); + } + + private isPublisherUserTrusted(publisher: string): boolean { + const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); + this.logService.debug('Trusted publishers', trustedPublishers); + return trustedPublishers.includes(publisher); + } + + trustPublishers(...publishers: string[]): void { + const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); + publishers = publishers.map(p => p.toLowerCase()).filter(p => !trustedPublishers.includes(p)); + if (publishers.length) { + trustedPublishers.push(...publishers); + this.storageService.store(TrustedPublishersStorageKey, trustedPublishers, StorageScope.APPLICATION, StorageTarget.USER); + } + } } class WorkspaceExtensionsManagementService extends Disposable { diff --git a/code/src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts b/code/src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts new file mode 100644 index 00000000000..ef1ea959ed8 --- /dev/null +++ b/code/src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../base/common/codicons.js'; +import { localize } from '../../../../nls.js'; +import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; +import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; + +export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.')); +export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', textLinkForeground, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), false); diff --git a/code/src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css b/code/src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css new file mode 100644 index 00000000000..1a03c09f437 --- /dev/null +++ b/code/src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.rendered-markdown.extensions-management-publisher-trust-dialog .codicon { + vertical-align: sub; +} + +.rendered-markdown.extensions-management-publisher-trust-dialog .codicon.codicon-extensions-verified-publisher { + color: var(--vscode-extensionIcon-verifiedForeground); +} diff --git a/code/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/code/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index 90c9d4af88a..eeae819a083 100644 --- a/code/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { generateUuid } from '../../../../base/common/uuid.js'; -import { ILocalExtension, IExtensionGalleryService, InstallOptions } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IExtensionGalleryService, InstallOptions, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { ExtensionManagementService as BaseExtensionManagementService } from '../common/extensionManagementService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -26,6 +26,7 @@ import { IUserDataProfileService } from '../../userDataProfile/common/userDataPr import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; export class ExtensionManagementService extends BaseExtensionManagementService { @@ -46,6 +47,8 @@ export class ExtensionManagementService extends BaseExtensionManagementService { @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IExtensionsScannerService extensionsScannerService: IExtensionsScannerService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, + @IStorageService storageService: IStorageService, @ITelemetryService telemetryService: ITelemetryService, ) { super( @@ -64,6 +67,8 @@ export class ExtensionManagementService extends BaseExtensionManagementService { logService, instantiationService, extensionsScannerService, + allowedExtensionsService, + storageService, telemetryService ); } diff --git a/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index d5a9d45a0a0..f7c0ff78feb 100644 --- a/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -8,7 +8,7 @@ import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtensio import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from '../../common/extensionManagement.js'; import { ExtensionEnablementService } from '../../browser/extensionEnablementService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { Emitter } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { IWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchEnvironmentService } from '../../../environment/common/environmentService.js'; import { IStorageService, InMemoryStorageService } from '../../../../../platform/storage/common/storage.js'; @@ -72,6 +72,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { onDidUninstallExtension: disposables.add(new Emitter()).event, onDidChangeProfile: disposables.add(new Emitter()).event, onDidUpdateExtensionMetadata: disposables.add(new Emitter()).event, + onProfileAwareDidInstallExtensions: Event.None, }, }, null, null)); const extensionManagementService = disposables.add(instantiationService.createInstance(ExtensionManagementService)); @@ -147,6 +148,7 @@ suite('ExtensionEnablementService Test', () => { onDidInstallExtensions: didInstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, onDidChangeProfile: didChangeProfileExtensionsEvent.event, + onProfileAwareDidInstallExtensions: Event.None, getInstalled: () => Promise.resolve(installed) }, }, null, null)); diff --git a/code/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/code/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 04aa6fe51f8..d1a25424a4e 100644 --- a/code/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/code/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -26,8 +26,6 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; -import { ResourceMap } from '../../../../base/common/map.js'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -88,34 +86,29 @@ type ExtensionUrlHandlerClassification = { comment: 'This is used to understand the drop funnel of extension URI handling by the OS & VS Code.'; }; -interface ExtensionUrlReloadHandlerEvent { - readonly extensionId: string; - readonly isRemote: boolean; -} - -type ExtensionUrlReloadHandlerClassification = { - owner: 'sandy081'; - readonly extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The ID of the extension that should handle the URI' }; - readonly isRemote: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Whether the current window is a remote window' }; - comment: 'This is used to understand the drop funnel of extension URI handling by the OS & VS Code.'; -}; - export interface IExtensionUrlHandlerOverride { + canHandleURL(uri: URI): boolean; handleURL(uri: URI): Promise; } export class ExtensionUrlHandlerOverrideRegistry { - private static readonly handlers = new ResourceMap(); + private static readonly handlers = new Set(); - static registerHandler(uri: URI, handler: IExtensionUrlHandlerOverride): IDisposable { - this.handlers.set(uri, handler); + static registerHandler(handler: IExtensionUrlHandlerOverride): IDisposable { + this.handlers.add(handler); - return toDisposable(() => this.handlers.delete(uri)); + return toDisposable(() => this.handlers.delete(handler)); } static getHandler(uri: URI): IExtensionUrlHandlerOverride | undefined { - return this.handlers.get(uri); + for (const handler of this.handlers) { + if (handler.canHandleURL(uri)) { + return handler; + } + } + + return undefined; } } @@ -148,7 +141,6 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { @ITelemetryService private readonly telemetryService: ITelemetryService, @INotificationService private readonly notificationService: INotificationService, @IProductService private readonly productService: IProductService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { this.userTrustedExtensionsStorage = new UserTrustedExtensionIdStorage(storageService); @@ -311,7 +303,6 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { /* Extension cannot be added and require window reload */ else { - this.telemetryService.publicLog2('uri_invoked/install_extension/reload', { extensionId, isRemote: !!this.workbenchEnvironmentService.remoteAuthority }); const result = await this.dialogService.confirm({ message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extensionId), primaryButton: localize({ key: 'reloadAndOpen', comment: ['&& denotes a mnemonic'] }, "&&Reload Window and Open") diff --git a/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 8e63847fb9b..786153b46b6 100644 --- a/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -111,7 +111,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx @IProductService protected readonly _productService: IProductService, @IWorkbenchExtensionManagementService protected readonly _extensionManagementService: IWorkbenchExtensionManagementService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @IConfigurationService protected readonly _configurationService: IConfigurationService, @IExtensionManifestPropertiesService private readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, @ILogService protected readonly _logService: ILogService, @IRemoteAgentService protected readonly _remoteAgentService: IRemoteAgentService, diff --git a/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index 33eabcc36dd..d38ab6b10ae 100644 --- a/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -15,7 +15,6 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ExtensionKind } from '../../../../platform/environment/common/environment.js'; import { IExtensionGalleryService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -414,7 +413,13 @@ export class NativeExtensionService extends AbstractExtensionService implements return this._startLocalExtensionHost(emitter); } - updateProxyConfigurationsScope(remoteEnv.useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE); + const useHostProxyDefault = remoteEnv.useHostProxy; + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('http.useLocalProxyConfiguration')) { + updateProxyConfigurationsScope(this._configurationService.getValue('http.useLocalProxyConfiguration'), useHostProxyDefault); + } + })); + updateProxyConfigurationsScope(this._configurationService.getValue('http.useLocalProxyConfiguration'), useHostProxyDefault); } else { this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => uri); @@ -462,16 +467,6 @@ export class NativeExtensionService extends AbstractExtensionService implements if (!recommendation) { return false; } - const sendTelemetry = (userReaction: 'install' | 'enable' | 'cancel') => { - /* __GDPR__ - "remoteExtensionRecommendations:popup" : { - "owner": "sandy081", - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('remoteExtensionRecommendations:popup', { userReaction, extensionId: resolverExtensionId }); - }; const resolverExtensionId = recommendation.extensionId; const allExtensions = await this._scanAllLocalExtensions(); @@ -483,7 +478,6 @@ export class NativeExtensionService extends AbstractExtensionService implements [{ label: nls.localize('enable', 'Enable and Reload'), run: async () => { - sendTelemetry('enable'); await this._extensionEnablementService.setEnablement([toExtension(extension)], EnablementState.EnabledGlobally); await this._hostService.reload(); } @@ -501,7 +495,6 @@ export class NativeExtensionService extends AbstractExtensionService implements [{ label: nls.localize('install', 'Install and Reload'), run: async () => { - sendTelemetry('install'); const [galleryExtension] = await this._extensionGalleryService.getExtensions([{ id: resolverExtensionId }], CancellationToken.None); if (galleryExtension) { await this._extensionManagementService.installFromGallery(galleryExtension); @@ -515,7 +508,6 @@ export class NativeExtensionService extends AbstractExtensionService implements { sticky: true, priority: NotificationPriority.URGENT, - onCancel: () => sendTelemetry('cancel') } ); diff --git a/code/src/vs/workbench/services/log/common/logConstants.ts b/code/src/vs/workbench/services/log/common/logConstants.ts index 688a1b2c006..81ea16db981 100644 --- a/code/src/vs/workbench/services/log/common/logConstants.ts +++ b/code/src/vs/workbench/services/log/common/logConstants.ts @@ -3,5 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../nls.js'; +import { LoggerGroup } from '../../../../platform/log/common/log.js'; + export const windowLogId = 'rendererLog'; +export const windowLogGroup: LoggerGroup = { id: windowLogId, name: localize('window', "Window") }; export const showWindowLogActionId = 'workbench.action.showWindowLog'; diff --git a/code/src/vs/workbench/services/log/electron-sandbox/logService.ts b/code/src/vs/workbench/services/log/electron-sandbox/logService.ts index 31cd8e8b5b0..34245d85ff0 100644 --- a/code/src/vs/workbench/services/log/electron-sandbox/logService.ts +++ b/code/src/vs/workbench/services/log/electron-sandbox/logService.ts @@ -7,8 +7,7 @@ import { ConsoleLogger, ILogger } from '../../../../platform/log/common/log.js'; import { INativeWorkbenchEnvironmentService } from '../../environment/electron-sandbox/environmentService.js'; import { LoggerChannelClient } from '../../../../platform/log/common/logIpc.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { windowLogId } from '../common/logConstants.js'; +import { windowLogGroup, windowLogId } from '../common/logConstants.js'; import { LogService } from '../../../../platform/log/common/logService.js'; export class NativeLogService extends LogService { @@ -17,7 +16,7 @@ export class NativeLogService extends LogService { const disposables = new DisposableStore(); - const fileLogger = disposables.add(loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: localize('rendererLog', "Window") })); + const fileLogger = disposables.add(loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: windowLogGroup.name, group: windowLogGroup })); let consoleLogger: ILogger; if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { diff --git a/code/src/vs/workbench/services/output/common/output.ts b/code/src/vs/workbench/services/output/common/output.ts index c43058f7970..0617017a0bc 100644 --- a/code/src/vs/workbench/services/output/common/output.ts +++ b/code/src/vs/workbench/services/output/common/output.ts @@ -8,6 +8,8 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { LogLevel } from '../../../../platform/log/common/log.js'; +import { Range } from '../../../../editor/common/core/range.js'; /** * Mime type used by the output editor. @@ -35,16 +37,33 @@ export const LOG_MODE_ID = 'log'; export const OUTPUT_VIEW_ID = 'workbench.panel.output'; export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); - export const CONTEXT_ACTIVE_FILE_OUTPUT = new RawContextKey('activeLogOutput', false); - +export const CONTEXT_ACTIVE_LOG_FILE_OUTPUT = new RawContextKey('activeLogOutput.isLog', false); export const CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE = new RawContextKey('activeLogOutput.levelSettable', false); - export const CONTEXT_ACTIVE_OUTPUT_LEVEL = new RawContextKey('activeLogOutput.level', ''); - export const CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT = new RawContextKey('activeLogOutput.levelIsDefault', false); - export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey(`outputView.scrollLock`, false); +export const ACTIVE_OUTPUT_CHANNEL_CONTEXT = new RawContextKey('activeOutputChannel', ''); +export const SHOW_TRACE_FILTER_CONTEXT = new RawContextKey('output.filter.trace', true); +export const SHOW_DEBUG_FILTER_CONTEXT = new RawContextKey('output.filter.debug', true); +export const SHOW_INFO_FILTER_CONTEXT = new RawContextKey('output.filter.info', true); +export const SHOW_WARNING_FILTER_CONTEXT = new RawContextKey('output.filter.warning', true); +export const SHOW_ERROR_FILTER_CONTEXT = new RawContextKey('output.filter.error', true); +export const OUTPUT_FILTER_FOCUS_CONTEXT = new RawContextKey('outputFilterFocus', false); +export const HIDE_CATEGORY_FILTER_CONTEXT = new RawContextKey('output.filter.categories', ''); + +export interface IOutputViewFilters { + readonly onDidChange: Event; + text: string; + trace: boolean; + debug: boolean; + info: boolean; + warning: boolean; + error: boolean; + categories: string; + toggleCategory(category: string): void; + hasCategory(category: string): boolean; +} export const IOutputService = createDecorator('outputService'); @@ -54,6 +73,11 @@ export const IOutputService = createDecorator('outputService'); export interface IOutputService { readonly _serviceBrand: undefined; + /** + * Output view filters. + */ + readonly filters: IOutputViewFilters; + /** * Given the channel id returns the output channel instance. * Channel should be first registered via OutputChannelRegistry. @@ -85,6 +109,35 @@ export interface IOutputService { * Allows to register on active output channel change. */ onActiveOutputChannel: Event; + + /** + * Register a compound log channel with the given channels. + */ + registerCompoundLogChannel(channels: IOutputChannelDescriptor[]): string; + + /** + * Save the logs to a file. + */ + saveOutputAs(...channels: IOutputChannelDescriptor[]): Promise; + + /** + * Checks if the log level can be set for the given channel. + * @param channel + */ + canSetLogLevel(channel: IOutputChannelDescriptor): boolean; + + /** + * Returns the log level for the given channel. + * @param channel + */ + getLogLevel(channel: IOutputChannelDescriptor): LogLevel | undefined; + + /** + * Sets the log level for the given channel. + * @param channel + * @param logLevel + */ + setLogLevel(channel: IOutputChannelDescriptor, logLevel: LogLevel): void; } export enum OutputChannelUpdateMode { @@ -93,22 +146,36 @@ export enum OutputChannelUpdateMode { Clear } +export interface ILogEntry { + readonly range: Range; + readonly timestamp: number; + readonly timestampRange: Range; + readonly logLevel: LogLevel; + readonly logLevelRange: Range; + readonly category: string | undefined; +} + export interface IOutputChannel { /** * Identifier of the output channel. */ - id: string; + readonly id: string; /** * Label of the output channel to be displayed to the user. */ - label: string; + readonly label: string; /** * URI of the output channel. */ - uri: URI; + readonly uri: URI; + + /** + * Log entries of the output channel. + */ + getLogEntries(): readonly ILogEntry[]; /** * Appends output to the channel. @@ -146,24 +213,48 @@ export interface IOutputChannelDescriptor { label: string; log: boolean; languageId?: string; - file?: URI; + source?: IOutputContentSource | ReadonlyArray; extensionId?: string; + user?: boolean; +} + +export interface ISingleSourceOutputChannelDescriptor extends IOutputChannelDescriptor { + source: IOutputContentSource; +} + +export interface IMultiSourceOutputChannelDescriptor extends IOutputChannelDescriptor { + source: ReadonlyArray; +} + +export function isSingleSourceOutputChannelDescriptor(descriptor: IOutputChannelDescriptor): descriptor is ISingleSourceOutputChannelDescriptor { + return !!descriptor.source && !Array.isArray(descriptor.source); } -export interface IFileOutputChannelDescriptor extends IOutputChannelDescriptor { - file: URI; +export function isMultiSourceOutputChannelDescriptor(descriptor: IOutputChannelDescriptor): descriptor is IMultiSourceOutputChannelDescriptor { + return Array.isArray(descriptor.source); +} + +export interface IOutputContentSource { + readonly name?: string; + readonly resource: URI; } export interface IOutputChannelRegistry { readonly onDidRegisterChannel: Event; - readonly onDidRemoveChannel: Event; + readonly onDidRemoveChannel: Event; + readonly onDidUpdateChannelSources: Event; /** * Make an output channel known to the output world. */ registerChannel(descriptor: IOutputChannelDescriptor): void; + /** + * Update the files for the given output channel. + */ + updateChannelSources(id: string, sources: IOutputContentSource[]): void; + /** * Returns the list of channels known to the output world. */ @@ -184,10 +275,13 @@ class OutputChannelRegistry implements IOutputChannelRegistry { private channels = new Map(); private readonly _onDidRegisterChannel = new Emitter(); - readonly onDidRegisterChannel: Event = this._onDidRegisterChannel.event; + readonly onDidRegisterChannel = this._onDidRegisterChannel.event; + + private readonly _onDidRemoveChannel = new Emitter(); + readonly onDidRemoveChannel = this._onDidRemoveChannel.event; - private readonly _onDidRemoveChannel = new Emitter(); - readonly onDidRemoveChannel: Event = this._onDidRemoveChannel.event; + private readonly _onDidUpdateChannelFiles = new Emitter(); + readonly onDidUpdateChannelSources = this._onDidUpdateChannelFiles.event; public registerChannel(descriptor: IOutputChannelDescriptor): void { if (!this.channels.has(descriptor.id)) { @@ -206,12 +300,21 @@ class OutputChannelRegistry implements IOutputChannelRegistry { return this.channels.get(id); } + public updateChannelSources(id: string, sources: IOutputContentSource[]): void { + const channel = this.channels.get(id); + if (channel && isMultiSourceOutputChannelDescriptor(channel)) { + channel.source = sources; + this._onDidUpdateChannelFiles.fire(channel); + } + } + public removeChannel(id: string): void { - this.channels.delete(id); - this._onDidRemoveChannel.fire(id); + const channel = this.channels.get(id); + if (channel) { + this.channels.delete(id); + this._onDidRemoveChannel.fire(channel); + } } } Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); - -export const ACTIVE_OUTPUT_CHANNEL_CONTEXT = new RawContextKey('activeOutputChannel', ''); diff --git a/code/src/vs/workbench/services/preferences/common/preferences.ts b/code/src/vs/workbench/services/preferences/common/preferences.ts index ea385543252..f9cb32deed3 100644 --- a/code/src/vs/workbench/services/preferences/common/preferences.ts +++ b/code/src/vs/workbench/services/preferences/common/preferences.ts @@ -138,13 +138,15 @@ export enum SettingMatchType { LanguageTagSettingMatch = 1 << 0, RemoteMatch = 1 << 1, DescriptionOrValueMatch = 1 << 2, - KeyMatch = 1 << 3 + KeyMatch = 1 << 3, + KeyIdMatch = 1 << 4, } export interface ISettingMatch { setting: ISetting; matches: IRange[] | null; matchType: SettingMatchType; + keyMatchScore: number; score: number; } @@ -184,7 +186,7 @@ export interface IPreferencesEditorModel { } export type IGroupFilter = (group: ISettingsGroup) => boolean | null; -export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[]; matchType: SettingMatchType; score: number } | null; +export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[]; matchType: SettingMatchType; keyMatchScore: number; score: number } | null; export interface ISettingsEditorModel extends IPreferencesEditorModel { readonly onDidChangeGroups: Event; diff --git a/code/src/vs/workbench/services/preferences/common/preferencesModels.ts b/code/src/vs/workbench/services/preferences/common/preferencesModels.ts index 6209d182d4e..44f3e8035de 100644 --- a/code/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/code/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -71,6 +71,7 @@ abstract class AbstractSettingsModel extends EditorModel { setting, matches: settingMatchResult && settingMatchResult.matches, matchType: settingMatchResult?.matchType ?? SettingMatchType.None, + keyMatchScore: settingMatchResult?.keyMatchScore ?? 0, score: settingMatchResult?.score ?? 0 }); } @@ -899,6 +900,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements setting: filteredMatch.setting, score: filteredMatch.score, matchType: filteredMatch.matchType, + keyMatchScore: filteredMatch.keyMatchScore, matches: filteredMatch.matches && filteredMatch.matches.map(match => { return new Range( match.startLineNumber - filteredMatch.setting.range.startLineNumber, diff --git a/code/src/vs/workbench/services/request/browser/requestService.ts b/code/src/vs/workbench/services/request/browser/requestService.ts index 63d46d34d09..b7fd8daa969 100644 --- a/code/src/vs/workbench/services/request/browser/requestService.ts +++ b/code/src/vs/workbench/services/request/browser/requestService.ts @@ -12,7 +12,10 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '../../../../platform/request/common/request.js'; import { request } from '../../../../base/parts/request/common/requestImpl.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; +import { ILoggerService } from '../../../../platform/log/common/log.js'; +import { localize } from '../../../../nls.js'; +import { LogService } from '../../../../platform/log/common/logService.js'; +import { windowLogGroup } from '../../log/common/logConstants.js'; export class BrowserRequestService extends AbstractRequestService implements IRequestService { @@ -21,15 +24,19 @@ export class BrowserRequestService extends AbstractRequestService implements IRe constructor( @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILogService logService: ILogService, + @ILoggerService loggerService: ILoggerService, ) { + const logger = loggerService.createLogger(`network`, { name: localize('network', "Network"), group: windowLogGroup }); + const logService = new LogService(logger); super(logService); + this._register(logger); + this._register(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { try { if (!options.proxyAuthorization) { - options.proxyAuthorization = this.configurationService.getValue('http.proxyAuthorization'); + options.proxyAuthorization = this.configurationService.inspect('http.proxyAuthorization').userLocalValue; } const context = await this.logAndRequest(options, () => request(options, token, () => navigator.onLine)); diff --git a/code/src/vs/workbench/services/request/electron-sandbox/requestService.ts b/code/src/vs/workbench/services/request/electron-sandbox/requestService.ts index 9cfba42f54f..5f6960784ee 100644 --- a/code/src/vs/workbench/services/request/electron-sandbox/requestService.ts +++ b/code/src/vs/workbench/services/request/electron-sandbox/requestService.ts @@ -10,7 +10,10 @@ import { INativeHostService } from '../../../../platform/native/common/native.js import { IRequestContext, IRequestOptions } from '../../../../base/parts/request/common/request.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { request } from '../../../../base/parts/request/common/requestImpl.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; +import { ILoggerService } from '../../../../platform/log/common/log.js'; +import { localize } from '../../../../nls.js'; +import { windowLogGroup } from '../../log/common/logConstants.js'; +import { LogService } from '../../../../platform/log/common/logService.js'; export class NativeRequestService extends AbstractRequestService implements IRequestService { @@ -19,14 +22,18 @@ export class NativeRequestService extends AbstractRequestService implements IReq constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILogService logService: ILogService, + @ILoggerService loggerService: ILoggerService, ) { + const logger = loggerService.createLogger(`network`, { name: localize('network', "Network"), group: windowLogGroup }); + const logService = new LogService(logger); super(logService); + this._register(logger); + this._register(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { if (!options.proxyAuthorization) { - options.proxyAuthorization = this.configurationService.getValue('http.proxyAuthorization'); + options.proxyAuthorization = this.configurationService.inspect('http.proxyAuthorization').userLocalValue; } return this.logAndRequest(options, () => request(options, token, () => navigator.onLine)); } diff --git a/code/src/vs/workbench/services/statusbar/browser/statusbar.ts b/code/src/vs/workbench/services/statusbar/browser/statusbar.ts index 7c81333e416..61cc9f3029e 100644 --- a/code/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/code/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -204,6 +204,12 @@ export interface IStatusbarEntry { * the entry to new auxiliary windows opening. */ readonly showInAllWindows?: boolean; + + /** + * If provided, signals what extension is providing the status bar entry. This allows for + * more actions to manage the extension from the status bar entry. + */ + readonly extensionId?: string; } export interface IStatusbarEntryAccessor extends IDisposable { diff --git a/code/src/vs/workbench/services/suggest/browser/media/suggest.css b/code/src/vs/workbench/services/suggest/browser/media/suggest.css index b540839efa3..1bcec527a44 100644 --- a/code/src/vs/workbench/services/suggest/browser/media/suggest.css +++ b/code/src/vs/workbench/services/suggest/browser/media/suggest.css @@ -12,6 +12,17 @@ position: fixed; left: 0; top: 0; + border-style: solid; + border-width: 1px; + border-color: var(--vscode-editorSuggestWidget-border); + background-color: var(--vscode-editorSuggestWidget-background); +} + +.workbench-suggest-widget .suggest-details { + border-style: solid; + border-width: 1px; + border-color: var(--vscode-editorSuggestWidget-border); + background-color: var(--vscode-editorSuggestWidget-background); } /* Suggest widget*/ @@ -29,7 +40,7 @@ } .workbench-suggest-widget, -.monaco-workbench .workbench-suggest-details { +.monaco-workbench .suggest-details { flex: 0 1 auto; width: 100%; border-style: solid; @@ -39,9 +50,9 @@ } .monaco-workbench.hc-black .workbench-suggest-widget, -.monaco-workbench.hc-black .workbench-suggest-details, +.monaco-workbench.hc-black .suggest-details, .monaco-workbench.hc-light .workbench-suggest-widget, -.monaco-workbench.hc-light .workbench-suggest-details { +.monaco-workbench.hc-light .suggest-details { border-width: 2px; } @@ -51,6 +62,18 @@ display: flex; } +.monaco-workbench .workbench-suggest-widget .suggest-status-bar { + box-sizing: border-box; + display: none; + flex-flow: row nowrap; + justify-content: space-between; + width: 100%; + font-size: 80%; + padding: 0 4px 0 4px; + border-top: 1px solid var(--vscode-editorSuggestWidget-border); + overflow: hidden; +} + .monaco-workbench .workbench-suggest-widget .suggest-status-bar .left { padding-right: 8px; } @@ -178,6 +201,28 @@ height: 0.7em; display: inline-block; } + +/** ReadMore Icon styles **/ + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .codicon-close, +.workbench-suggest-widget .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right > .readMore::before { + color: inherit; + opacity: 1; + font-size: 14px; + cursor: pointer; +} + +.monaco-workbench .workbench-suggest-widget .suggest-details .codicon.codicon-close { + position: absolute; + top: 6px; + right: 2px; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .codicon-close:hover, +.workbench-suggest-widget .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right > .readMore:hover { + opacity: 1; +} + /** signature, qualifier, type/details opacity **/ .workbench-suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right > .details-label { @@ -270,3 +315,123 @@ height: 18px; visibility: hidden; } + +.workbench-suggest-widget .suggest-details { + display: flex; + flex-direction: column; + cursor: default; + color: var(--vscode-editorSuggestWidget-foreground); +} + +.workbench-suggest-widget .suggest-details:focus { + border-color: var(--vscode-focusBorder); +} + +.workbench-suggest-widget .suggest-details a { + color: var(--vscode-textLink-foreground); +} + +.workbench-suggest-widget .suggest-details a:hover { + color: var(--vscode-textLink-activeForeground); +} + +.workbench-suggest-widget .suggest-details code { + background-color: var(--vscode-textCodeBlock-background); +} + +.workbench-suggest-widget .suggest-details.no-docs { + display: none; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element { + flex: 1; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body { + box-sizing: border-box; + height: 100%; + width: 100%; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .type { + flex: 2; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.7; + white-space: pre; + margin: 0 24px 0 0; + padding: 4px 0 4px 5px; +} + +.workbench-suggest-widget .suggest-details.detail-and-doc > .monaco-scrollable-element > .body > .header > .type { + padding-bottom: 12px; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .type.auto-wrap { + white-space: normal; + word-break: break-all; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs { + margin: 0; + padding: 4px 5px; + white-space: pre-wrap; +} + +.workbench-suggest-widget .suggest-details.no-type > .monaco-scrollable-element > .body > .docs { + margin-right: 24px; + overflow: hidden; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs { + padding: 0; + white-space: initial; + min-height: calc(1rem + 8px); +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > div, +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > span:not(:empty) { + padding: 4px 5px; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > div > p:first-child { + margin-top: 0; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > div > p:last-child { + margin-bottom: 0; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs .monaco-tokenized-source { + white-space: pre; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs .code { + white-space: pre-wrap; + word-wrap: break-word; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs .codicon { + vertical-align: sub; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > p:empty { + display: none; +} + +.workbench-suggest-widget .suggest-details code { + border-radius: 3px; + padding: 0 0.4em; +} + +.workbench-suggest-widget .suggest-details ul { + padding-left: 20px; +} + +.workbench-suggest-widget .suggest-details ol { + padding-left: 20px; +} + +.workbench-suggest-widget .suggest-details p code { + font-family: var(--monaco-monospace-font); +} diff --git a/code/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/code/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 06778c86038..9af7f683e6b 100644 --- a/code/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/code/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { FuzzyScore } from '../../../../base/common/filters.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { isWindows } from '../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -12,6 +13,10 @@ export interface ISimpleCompletion { * The completion's label which appears on the left beside the icon. */ label: string; + /** + * The ID of the provider the completion item came from + */ + provider: string; /** * The completion's icon to show on the left of the suggest widget. */ @@ -20,6 +25,12 @@ export interface ISimpleCompletion { * The completion's detail which appears on the right of the list. */ detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | MarkdownString; + /** * Whether the completion is a file. Files with the same score will be sorted against each other * first by extension length and then certain extensions will get a boost based on the OS. diff --git a/code/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/code/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index c80732669d7..df8b119efba 100644 --- a/code/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/code/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -208,6 +208,10 @@ export class SimpleCompletionModel { // Then by file extension length ascending score = a.fileExtLow.length - b.fileExtLow.length; } + if (score === 0 || fileExtScore(a.fileExtLow) === 0 && fileExtScore(b.fileExtLow) === 0) { + // both files or directories, sort alphabetically + score = a.completion.label.localeCompare(b.completion.label); + } return score; }); this._refilterKind = Refilter.Nothing; @@ -230,6 +234,8 @@ const fileExtScores = new Map(isWindows ? [ ['exe', 0.08], ['bat', 0.07], ['cmd', 0.07], + ['msi', 0.06], + ['com', 0.06], // Non-Windows ['sh', -0.05], ['bash', -0.05], diff --git a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index af70d32b896..aa898efba3c 100644 --- a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -11,7 +11,7 @@ import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resi import { SimpleCompletionItem } from './simpleCompletionItem.js'; import { LineContext, SimpleCompletionModel } from './simpleCompletionModel.js'; import { getAriaId, SimpleSuggestWidgetItemRenderer, type ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js'; -import { TimeoutTimer } from '../../../../base/common/async.js'; +import { CancelablePromise, createCancelablePromise, disposableTimeout, TimeoutTimer } from '../../../../base/common/async.js'; import { Emitter, Event, PauseableEmitter } from '../../../../base/common/event.js'; import { MutableDisposable, Disposable } from '../../../../base/common/lifecycle.js'; import { clamp } from '../../../../base/common/numbers.js'; @@ -20,6 +20,9 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { SuggestWidgetStatus } from '../../../../editor/contrib/suggest/browser/suggestWidgetStatus.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { canExpandCompletionItem, SimpleSuggestDetailsOverlay, SimpleSuggestDetailsWidget } from './simpleSuggestWidgetDetails.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; const $ = dom.$; @@ -49,12 +52,21 @@ const enum WidgetPositionPreference { Below } +export const SimpleSuggestContext = { + HasFocusedSuggestion: new RawContextKey('simpleSuggestWidgetHasFocusedSuggestion', false, localize('simpleSuggestWidgetHasFocusedSuggestion', "Whether any simple suggestion is focused")), +}; + export interface IWorkbenchSuggestWidgetOptions { /** * The {@link MenuId} to use for the status bar. Items on the menu must use the groups `'left'` * and `'right'`. */ statusBarMenuId?: MenuId; + + /** + * The setting for showing the status bar. + */ + showStatusBarSettingId?: string; } export class SimpleSuggestWidget extends Disposable { @@ -66,16 +78,20 @@ export class SimpleSuggestWidget extends Disposable { private _completionModel?: SimpleCompletionModel; private _cappedHeight?: { wanted: number; capped: number }; private _forceRenderingAbove: boolean = false; + private _explainMode: boolean = false; + private _preference?: WidgetPositionPreference; + private readonly _pendingShowDetails = this._register(new MutableDisposable()); private readonly _pendingLayout = this._register(new MutableDisposable()); - // private _currentSuggestionDetails?: CancelablePromise; + private _currentSuggestionDetails?: CancelablePromise; private _focusedItem?: SimpleCompletionItem; private _ignoreFocusEvents: boolean = false; readonly element: ResizableHTMLElement; private readonly _messageElement: HTMLElement; private readonly _listElement: HTMLElement; private readonly _list: List; - private readonly _status?: SuggestWidgetStatus; + private _status?: SuggestWidgetStatus; + private readonly _details: SimpleSuggestDetailsOverlay; private readonly _showTimeout = this._register(new TimeoutTimer()); @@ -87,16 +103,23 @@ export class SimpleSuggestWidget extends Disposable { readonly onDidShow: Event = this._onDidShow.event; private readonly _onDidFocus = new PauseableEmitter(); readonly onDidFocus: Event = this._onDidFocus.event; + private readonly _onDidBlurDetails = this._register(new Emitter()); + readonly onDidBlurDetails = this._onDidBlurDetails.event; + private readonly _onDidFontConfigurationChange = this._register(new Emitter()); + readonly onDidFontConfigurationChange = this._onDidFontConfigurationChange.event; get list(): List { return this._list; } + private readonly _ctxSuggestWidgetHasFocusedSuggestion: IContextKey; + constructor( private readonly _container: HTMLElement, private readonly _persistedSize: IPersistedWidgetSizeDelegate, - private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, - options: IWorkbenchSuggestWidgetOptions, + private readonly _options: IWorkbenchSuggestWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IStorageService private readonly _storageService: IStorageService, + @IContextKeyService _contextKeyService: IContextKeyService ) { super(); @@ -104,6 +127,8 @@ export class SimpleSuggestWidget extends Disposable { this.element.domNode.classList.add('workbench-suggest-widget'); this._container.appendChild(this.element.domNode); + this._ctxSuggestWidgetHasFocusedSuggestion = SimpleSuggestContext.HasFocusedSuggestion.bindTo(_contextKeyService); + class ResizeState { constructor( readonly persistedSize: dom.Dimension | undefined, @@ -151,10 +176,10 @@ export class SimpleSuggestWidget extends Disposable { state = undefined; })); - const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !configurationService.getValue('editor.suggest.showIcons')); + const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !_configurationService.getValue('editor.suggest.showIcons')); applyIconStyle(); - const renderer = new SimpleSuggestWidgetItemRenderer(_getFontInfo); + const renderer = new SimpleSuggestWidgetItemRenderer(this._getFontInfo.bind(this), this._configurationService); this._register(renderer); this._listElement = dom.append(this.element.domNode, $('.tree')); this._list = this._register(new List('SuggestWidget', this._listElement, { @@ -203,8 +228,13 @@ export class SimpleSuggestWidget extends Disposable { this._messageElement = dom.append(this.element.domNode, dom.$('.message')); - if (options.statusBarMenuId) { - this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, options.statusBarMenuId)); + const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget, this._getFontInfo.bind(this), this.onDidFontConfigurationChange)); + this._register(details.onDidClose(() => this.toggleDetails())); + this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); + this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); + + if (_options.statusBarMenuId && _options.showStatusBarSettingId && _configurationService.getValue(_options.showStatusBarSettingId)) { + this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, _options.statusBarMenuId)); this.element.domNode.classList.toggle('with-status-bar', true); } @@ -212,10 +242,33 @@ export class SimpleSuggestWidget extends Disposable { this._register(this._list.onTap(e => this._onListMouseDownOrTap(e))); this._register(this._list.onDidChangeFocus(e => this._onListFocus(e))); this._register(this._list.onDidChangeSelection(e => this._onListSelection(e))); - this._register(configurationService.onDidChangeConfiguration(e => { + this._register(_configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.suggest.showIcons')) { applyIconStyle(); } + if (this._completionModel && ( + e.affectsConfiguration('editor.fontSize') || + e.affectsConfiguration('editor.lineHeight') || + e.affectsConfiguration('editor.fontWeight') || + e.affectsConfiguration('editor.fontFamily'))) { + this._list.splice(0, this._completionModel.items.length, this._completionModel!.items); + this._onDidFontConfigurationChange.fire(); + } + if (_options.statusBarMenuId && _options.showStatusBarSettingId && e.affectsConfiguration(_options.showStatusBarSettingId)) { + const showStatusBar: boolean = _configurationService.getValue(_options.showStatusBarSettingId); + if (showStatusBar && !this._status) { + this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, _options.statusBarMenuId)); + this._status.show(); + } else if (showStatusBar && this._status) { + this._status.show(); + } else if (this._status) { + this._status.element.remove(); + this._status.dispose(); + this._status = undefined; + this._layout(undefined); + } + this.element.domNode.classList.toggle('with-status-bar', showStatusBar); + } })); } @@ -231,11 +284,12 @@ export class SimpleSuggestWidget extends Disposable { } if (!e.elements.length) { - // if (this._currentSuggestionDetails) { - // this._currentSuggestionDetails.cancel(); - // this._currentSuggestionDetails = undefined; - // this._focusedItem = undefined; - // } + if (this._currentSuggestionDetails) { + this._currentSuggestionDetails.cancel(); + this._currentSuggestionDetails = undefined; + this._focusedItem = undefined; + this._ctxSuggestWidgetHasFocusedSuggestion.set(false); + } this._clearAriaActiveDescendant(); return; } @@ -244,18 +298,19 @@ export class SimpleSuggestWidget extends Disposable { return; } - // this._ctxSuggestWidgetHasFocusedSuggestion.set(true); + this._ctxSuggestWidgetHasFocusedSuggestion.set(true); const item = e.elements[0]; const index = e.indexes[0]; if (item !== this._focusedItem) { - // this._currentSuggestionDetails?.cancel(); - // this._currentSuggestionDetails = undefined; + this._currentSuggestionDetails?.cancel(); + this._currentSuggestionDetails = undefined; this._focusedItem = item; this._list.reveal(index); + const id = getAriaId(index); const node = dom.getActiveWindow().document.activeElement; if (node && id) { @@ -265,6 +320,40 @@ export class SimpleSuggestWidget extends Disposable { } else { this._clearAriaActiveDescendant(); } + + this._currentSuggestionDetails = createCancelablePromise(async token => { + const loading = disposableTimeout(() => { + if (this._isDetailsVisible()) { + this._showDetails(true, false); + } + }, 250); + const sub = token.onCancellationRequested(() => loading.dispose()); + try { + return await Promise.resolve(); + } finally { + loading.dispose(); + sub.dispose(); + } + }); + + this._currentSuggestionDetails.then(() => { + if (index >= this._list.length || item !== this._list.element(index)) { + return; + } + + // item can have extra information, so re-render + this._ignoreFocusEvents = true; + this._list.splice(index, 1, [item]); + this._list.setFocus([index]); + this._ignoreFocusEvents = false; + + if (this._isDetailsVisible()) { + this._showDetails(false, false); + } else { + this.element.domNode.classList.remove('docs-side'); + } + + }).catch(); } // emit an event this._onDidFocus.fire({ item, index, model: this._completionModel }); @@ -343,6 +432,7 @@ export class SimpleSuggestWidget extends Disposable { // Reset focus border // this._details.widget.domNode.classList.remove('focused'); }); + this._afterRender(); } setLineContext(lineContext: LineContext): void { @@ -367,74 +457,70 @@ export class SimpleSuggestWidget extends Disposable { dom.hide(this._messageElement, this._listElement, this._status.element); } dom.hide(this._listElement); - if (this._status) { - dom.hide(this._status?.element); - } - // this._details.hide(true); + this._details.hide(true); this._status?.hide(); // this._contentWidget.hide(); // this._ctxSuggestWidgetVisible.reset(); // this._ctxSuggestWidgetMultipleSuggestions.reset(); - // this._ctxSuggestWidgetHasFocusedSuggestion.reset(); + this._ctxSuggestWidgetHasFocusedSuggestion.reset(); this._showTimeout.cancel(); this.element.domNode.classList.remove('visible'); this._list.splice(0, this._list.length); - // this._focusedItem = undefined; + this._focusedItem = undefined; this._cappedHeight = undefined; - // this._explainMode = false; + this._explainMode = false; break; case State.Loading: this.element.domNode.classList.add('message'); this._messageElement.textContent = SimpleSuggestWidget.LOADING_MESSAGE; dom.hide(this._listElement); if (this._status) { - dom.hide(this._status?.element); + dom.hide(this._status.element); } dom.show(this._messageElement); - // this._details.hide(); + this._details.hide(); this._show(); - // this._focusedItem = undefined; + this._focusedItem = undefined; break; case State.Empty: this.element.domNode.classList.add('message'); this._messageElement.textContent = SimpleSuggestWidget.NO_SUGGESTIONS_MESSAGE; dom.hide(this._listElement); if (this._status) { - dom.hide(this._status?.element); + dom.hide(this._status.element); } dom.show(this._messageElement); - // this._details.hide(); + this._details.hide(); this._show(); - // this._focusedItem = undefined; + this._focusedItem = undefined; break; case State.Open: dom.hide(this._messageElement); - dom.show(this._listElement); - if (this._status) { - dom.show(this._status?.element); - } + this._showListAndStatus(); this._show(); break; case State.Frozen: dom.hide(this._messageElement); - dom.show(this._listElement); - if (this._status) { - dom.show(this._status?.element); - } + this._showListAndStatus(); this._show(); break; case State.Details: dom.hide(this._messageElement); - dom.show(this._listElement); - if (this._status) { - dom.show(this._status?.element); - } - // this._details.show(); + this._showListAndStatus(); + this._details.show(); this._show(); break; } } + private _showListAndStatus(): void { + if (this._status) { + dom.show(this._listElement, this._status.element); + } else { + dom.show(this._listElement); + } + } + private _show(): void { // this._layout(this._persistedSize.restore()); // dom.show(this.element.domNode); @@ -453,9 +539,81 @@ export class SimpleSuggestWidget extends Disposable { }, 100); } + + toggleDetailsFocus(): void { + if (this._state === State.Details) { + // Should return the focus to the list item. + this._list.setFocus(this._list.getFocus()); + this._setState(State.Open); + } else if (this._state === State.Open) { + this._setState(State.Details); + if (!this._isDetailsVisible()) { + this.toggleDetails(true); + } else { + this._details.widget.focus(); + } + } + } + + toggleDetails(focused: boolean = false): void { + if (this._isDetailsVisible()) { + // hide details widget + this._pendingShowDetails.clear(); + // this._ctxSuggestWidgetDetailsVisible.set(false); + + this._setDetailsVisible(false); + this._details.hide(); + this.element.domNode.classList.remove('shows-details'); + + } else if ((canExpandCompletionItem(this._list.getFocusedElements()[0]) || this._explainMode) && (this._state === State.Open || this._state === State.Details || this._state === State.Frozen)) { + // show details widget (iff possible) + // this._ctxSuggestWidgetDetailsVisible.set(true); + + this._setDetailsVisible(true); + this._showDetails(false, focused); + } + } + + private _showDetails(loading: boolean, focused: boolean): void { + this._pendingShowDetails.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.element.domNode), () => { + this._pendingShowDetails.clear(); + this._details.show(); + let didFocusDetails = false; + if (loading) { + this._details.widget.renderLoading(); + } else { + this._details.widget.renderItem(this._list.getFocusedElements()[0], this._explainMode); + } + if (!this._details.widget.isEmpty) { + this._positionDetails(); + this.element.domNode.classList.add('shows-details'); + if (focused) { + this._details.widget.focus(); + didFocusDetails = true; + } + } else { + this._details.hide(); + } + if (!didFocusDetails) { + // this.editor.focus(); + } + }); + } + + toggleExplainMode(): void { + if (this._list.getFocusedElements()[0]) { + this._explainMode = !this._explainMode; + if (!this._isDetailsVisible()) { + this.toggleDetails(); + } else { + this._showDetails(false, false); + } + } + } + hide(): void { this._pendingLayout.clear(); - // this._pendingShowDetails.clear(); + this._pendingShowDetails.clear(); // this._loadingTimeout?.dispose(); this._setState(State.Hidden); @@ -495,7 +653,7 @@ export class SimpleSuggestWidget extends Disposable { // status bar if (this._status) { - this._status.element.style.lineHeight = `${info.itemHeight}px`; + this._status.element.style.height = `${info.itemHeight}px`; } // if (this._state === State.Empty || this._state === State.Loading) { @@ -517,7 +675,7 @@ export class SimpleSuggestWidget extends Disposable { const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width; // height math - const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight; + const fullHeight = info.statusBarHeight + this._list.contentHeight + this._messageElement.clientHeight + info.borderHeight; const minHeight = info.itemHeight + info.statusBarHeight; // const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); // const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); @@ -572,6 +730,23 @@ export class SimpleSuggestWidget extends Disposable { this._resize(width, height); } + _afterRender() { + // if (position === null) { + // if (this._isDetailsVisible()) { + // this._details.hide(); //todo@jrieken soft-hide + // } + // return; + // } + if (this._state === State.Empty || this._state === State.Loading) { + // no special positioning when widget isn't showing list + return; + } + if (this._isDetailsVisible() && !this._details.widget.isEmpty) { + this._details.show(); + } + this._positionDetails(); + } + private _resize(width: number, height: number): void { const { width: maxWidth, height: maxHeight } = this.element.maxSize; width = Math.min(maxWidth, width); @@ -584,18 +759,49 @@ export class SimpleSuggestWidget extends Disposable { this._listElement.style.height = `${height - statusBarHeight}px`; this._listElement.style.width = `${width}px`; - this._listElement.style.height = `${height}px`; this.element.layout(height, width); + if (this._cursorPosition) { + this.element.domNode.style.top = `${this._cursorPosition.top - height}px`; + } + this._positionDetails(); + } + + private _positionDetails(): void { + if (this._isDetailsVisible()) { + this._details.placeAtAnchor(this.element.domNode); + } + } + + private _getFontInfo(): ISimpleSuggestWidgetFontInfo { + let lineHeight: number = this._configurationService.getValue('editor.lineHeight'); + const fontSize: number = this._configurationService.getValue('editor.fontSize'); + const fontFamily: string = this._configurationService.getValue('editor.fontFamily'); + const fontWeight: string = this._configurationService.getValue('editor.fontWeight'); + const letterSpacing: number = this._configurationService.getValue('editor.letterSpacing'); + + if (lineHeight <= 1) { + // Scale so icon shows by default + lineHeight = fontSize < 16 ? Math.ceil(fontSize * 1.5) : fontSize; + } else if (lineHeight <= 8) { + lineHeight = fontSize * lineHeight; + } + + const fontInfo = { + fontSize, + lineHeight, + fontWeight: fontWeight.toString(), + letterSpacing, + fontFamily + }; - // this._positionDetails(); - // TODO: Position based on preference + return fontInfo; } private _getLayoutInfo() { const fontInfo = this._getFontInfo(); - const itemHeight = clamp(Math.ceil(fontInfo.lineHeight), 8, 1000); - const statusBarHeight = 0; //!this.editor.getOption(EditorOption.suggest).showStatusBar || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight; - const borderWidth = 1; //this._details.widget.borderWidth; + const itemHeight = clamp(fontInfo.lineHeight, 8, 1000); + const statusBarHeight = !this._options.statusBarMenuId || !this._options.showStatusBarSettingId || !this._configurationService.getValue(this._options.showStatusBarSettingId) || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight; + const borderWidth = this._details.widget.borderWidth; const borderHeight = 2 * borderWidth; return { @@ -682,6 +888,14 @@ export class SimpleSuggestWidget extends Disposable { return undefined; } + private _isDetailsVisible(): boolean { + return this._storageService.getBoolean('expandSuggestionDocs', StorageScope.PROFILE, false); + } + + private _setDetailsVisible(value: boolean) { + this._storageService.store('expandSuggestionDocs', value, StorageScope.PROFILE, StorageTarget.USER); + } + forceRenderingAbove() { if (!this._forceRenderingAbove) { this._forceRenderingAbove = true; diff --git a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts new file mode 100644 index 00000000000..a0727fd9a93 --- /dev/null +++ b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -0,0 +1,478 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js'; +import * as nls from '../../../../nls.js'; +import { SimpleCompletionItem } from './simpleCompletionItem.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js'; + +export function canExpandCompletionItem(item: SimpleCompletionItem | undefined): boolean { + return !!item && Boolean(item.completion.documentation || item.completion.detail && item.completion.detail !== item.completion.label); +} + +export const SuggestDetailsClassName = 'suggest-details'; + +export class SimpleSuggestDetailsWidget { + + readonly domNode: HTMLDivElement; + + private readonly _onDidClose = new Emitter(); + readonly onDidClose: Event = this._onDidClose.event; + + private readonly _onDidChangeContents = new Emitter(); + readonly onDidChangeContents: Event = this._onDidChangeContents.event; + + private readonly _close: HTMLElement; + private readonly _scrollbar: DomScrollableElement; + private readonly _body: HTMLElement; + private readonly _header: HTMLElement; + private readonly _type: HTMLElement; + private readonly _docs: HTMLElement; + private readonly _disposables = new DisposableStore(); + + private readonly _markdownRenderer: MarkdownRenderer; + + private readonly _renderDisposeable = this._disposables.add(new DisposableStore()); + private _borderWidth: number = 1; + private _size = new dom.Dimension(330, 0); + + constructor( + private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, + onDidFontInfoChange: Event, + @IInstantiationService instaService: IInstantiationService + ) { + this.domNode = dom.$('.suggest-details'); + this.domNode.classList.add('no-docs'); + + this._markdownRenderer = instaService.createInstance(MarkdownRenderer, {}); + + this._body = dom.$('.body'); + + this._scrollbar = new DomScrollableElement(this._body, { + alwaysConsumeMouseWheel: true, + }); + dom.append(this.domNode, this._scrollbar.getDomNode()); + this._disposables.add(this._scrollbar); + + this._header = dom.append(this._body, dom.$('.header')); + this._close = dom.append(this._header, dom.$('span' + ThemeIcon.asCSSSelector(Codicon.close))); + this._close.title = nls.localize('details.close', "Close"); + this._close.role = 'button'; + this._close.tabIndex = -1; + this._type = dom.append(this._header, dom.$('p.type')); + + this._docs = dom.append(this._body, dom.$('p.docs')); + + this._configureFont(); + + this._disposables.add(onDidFontInfoChange(() => this._configureFont())); + } + + private _configureFont(): void { + const fontInfo = this._getFontInfo(); + const fontFamily = fontInfo.fontFamily; + + const fontSize = fontInfo.fontSize; + const lineHeight = fontInfo.lineHeight; + const fontWeight = fontInfo.fontWeight; + const fontSizePx = `${fontSize}px`; + const lineHeightPx = `${lineHeight}px`; + + this.domNode.style.fontSize = fontSizePx; + this.domNode.style.lineHeight = `${lineHeight / fontSize}`; + this.domNode.style.fontWeight = fontWeight; + // this.domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; + this._type.style.fontFamily = fontFamily; + this._close.style.height = lineHeightPx; + this._close.style.width = lineHeightPx; + + } + + dispose(): void { + this._disposables.dispose(); + this._onDidClose.dispose(); + this._onDidChangeContents.dispose(); + } + + getLayoutInfo() { + const lineHeight = this._getFontInfo().lineHeight; + const borderWidth = this._borderWidth; + const borderHeight = borderWidth * 2; + return { + lineHeight, + borderWidth, + borderHeight, + verticalPadding: 22, + horizontalPadding: 14 + }; + } + + renderLoading(): void { + this._type.textContent = nls.localize('loading', "Loading..."); + this._docs.textContent = ''; + this.domNode.classList.remove('no-docs', 'no-type'); + this.layout(this.size.width, this.getLayoutInfo().lineHeight * 2); + this._onDidChangeContents.fire(this); + } + + renderItem(item: SimpleCompletionItem, explainMode: boolean): void { + this._renderDisposeable.clear(); + + let { detail, documentation } = item.completion; + + let md = ''; + + if (explainMode) { + md += `score: ${item.score[0]}\n`; + md += `prefix: ${item.word ?? '(no prefix)'}\n`; + md += `replacementIndex: ${item.completion.replacementIndex}\n`; + md += `replacementLength: ${item.completion.replacementLength}\n`; + md += `index: ${item.idx}\n`; + detail = `Provider: ${item.completion.provider}`; + documentation = new MarkdownString().appendCodeblock('empty', md); + } + + if (!explainMode && !canExpandCompletionItem(item)) { + this.clearContents(); + return; + } + + this.domNode.classList.remove('no-docs', 'no-type'); + + // --- details + + if (detail) { + const cappedDetail = detail.length > 100000 ? `${detail.substr(0, 100000)}…` : detail; + this._type.textContent = cappedDetail; + this._type.title = cappedDetail; + dom.show(this._type); + this._type.classList.toggle('auto-wrap', !/\r?\n^\s+/gmi.test(cappedDetail)); + } else { + dom.clearNode(this._type); + this._type.title = ''; + dom.hide(this._type); + this.domNode.classList.add('no-type'); + } + + // // --- documentation + + dom.clearNode(this._docs); + if (typeof documentation === 'string') { + this._docs.classList.remove('markdown-docs'); + this._docs.textContent = documentation; + + } else if (documentation) { + this._docs.classList.add('markdown-docs'); + dom.clearNode(this._docs); + const renderedContents = this._markdownRenderer.render(documentation, { + asyncRenderCallback: () => { + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight); + this._onDidChangeContents.fire(this); + } + }); + this._docs.appendChild(renderedContents.element); + this._renderDisposeable.add(renderedContents); + } + + this.domNode.classList.toggle('detail-and-doc', !!detail && !!documentation); + + this.domNode.style.userSelect = 'text'; + this.domNode.tabIndex = -1; + + this._close.onmousedown = e => { + e.preventDefault(); + e.stopPropagation(); + }; + this._close.onclick = e => { + e.preventDefault(); + e.stopPropagation(); + this._onDidClose.fire(); + }; + + this._body.scrollTop = 0; + + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight + this.getLayoutInfo().verticalPadding); + this._onDidChangeContents.fire(this); + } + + clearContents() { + this.domNode.classList.add('no-docs'); + this._type.textContent = ''; + this._docs.textContent = ''; + } + + get isEmpty(): boolean { + return this.domNode.classList.contains('no-docs'); + } + + get size() { + return this._size; + } + + layout(width: number, height: number): void { + const newSize = new dom.Dimension(width, height); + if (!dom.Dimension.equals(newSize, this._size)) { + this._size = newSize; + dom.size(this.domNode, width, height); + } + this._scrollbar.scanDomNode(); + } + + scrollDown(much = 8): void { + this._body.scrollTop += much; + } + + scrollUp(much = 8): void { + this._body.scrollTop -= much; + } + + scrollTop(): void { + this._body.scrollTop = 0; + } + + scrollBottom(): void { + this._body.scrollTop = this._body.scrollHeight; + } + + pageDown(): void { + this.scrollDown(80); + } + + pageUp(): void { + this.scrollUp(80); + } + + set borderWidth(width: number) { + this._borderWidth = width; + } + + get borderWidth() { + return this._borderWidth; + } + + focus() { + this.domNode.focus(); + } +} + +export class SimpleSuggestDetailsOverlay { + + private readonly _disposables = new DisposableStore(); + private readonly _resizable: ResizableHTMLElement; + + private _added: boolean = false; + private _anchorBox?: dom.IDomNodePagePosition; + // private _preferAlignAtTop: boolean = true; + private _userSize?: dom.Dimension; + private _topLeft?: TopLeftPosition; + + constructor( + readonly widget: SimpleSuggestDetailsWidget, + private _container: HTMLElement, + ) { + + this._resizable = this._disposables.add(new ResizableHTMLElement()); + this._resizable.domNode.classList.add('suggest-details-container'); + this._resizable.domNode.appendChild(widget.domNode); + this._resizable.enableSashes(false, true, true, false); + + let topLeftNow: TopLeftPosition | undefined; + let sizeNow: dom.Dimension | undefined; + let deltaTop: number = 0; + let deltaLeft: number = 0; + this._disposables.add(this._resizable.onDidWillResize(() => { + topLeftNow = this._topLeft; + sizeNow = this._resizable.size; + })); + + this._disposables.add(this._resizable.onDidResize(e => { + if (topLeftNow && sizeNow) { + this.widget.layout(e.dimension.width, e.dimension.height); + + let updateTopLeft = false; + if (e.west) { + deltaLeft = sizeNow.width - e.dimension.width; + updateTopLeft = true; + } + if (e.north) { + deltaTop = sizeNow.height - e.dimension.height; + updateTopLeft = true; + } + if (updateTopLeft) { + this._applyTopLeft({ + top: topLeftNow.top + deltaTop, + left: topLeftNow.left + deltaLeft, + }); + } + } + if (e.done) { + topLeftNow = undefined; + sizeNow = undefined; + deltaTop = 0; + deltaLeft = 0; + this._userSize = e.dimension; + } + })); + + this._disposables.add(this.widget.onDidChangeContents(() => { + if (this._anchorBox) { + this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size); + } + })); + } + + dispose(): void { + this.widget.dispose(); + this._disposables.dispose(); + this.hide(); + } + + getId(): string { + return 'suggest.details'; + } + + getDomNode(): HTMLElement { + return this._resizable.domNode; + } + + show(): void { + if (!this._added) { + this._container.appendChild(this._resizable.domNode); + this._added = true; + } + } + + hide(sessionEnded: boolean = false): void { + this._resizable.clearSashHoverState(); + + if (this._added) { + this._container.removeChild(this._resizable.domNode); + this._added = false; + this._anchorBox = undefined; + // this._topLeft = undefined; + } + if (sessionEnded) { + this._userSize = undefined; + this.widget.clearContents(); + } + } + + placeAtAnchor(anchor: HTMLElement) { + const anchorBox = anchor.getBoundingClientRect(); + this._anchorBox = anchorBox; + this.widget.layout(this._resizable.size.width, this._resizable.size.height); + this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size); + } + + _placeAtAnchor(anchorBox: dom.IDomNodePagePosition, size: dom.Dimension) { + const bodyBox = dom.getClientArea(this.getDomNode().ownerDocument.body); + + const info = this.widget.getLayoutInfo(); + + const defaultMinSize = new dom.Dimension(220, 2 * info.lineHeight); + const defaultTop = anchorBox.top; + + type Placement = { top: number; left: number; fit: number; maxSizeTop: dom.Dimension; maxSizeBottom: dom.Dimension; minSize: dom.Dimension }; + + // EAST + const eastPlacement: Placement = (function () { + const width = bodyBox.width - (anchorBox.left + anchorBox.width + info.borderWidth + info.horizontalPadding); + const left = -info.borderWidth + anchorBox.left + anchorBox.width; + const maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); + const maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + return { top: defaultTop, left, fit: width - size.width, maxSizeTop, maxSizeBottom, minSize: defaultMinSize.with(Math.min(width, defaultMinSize.width)) }; + })(); + + // WEST + const westPlacement: Placement = (function () { + const width = anchorBox.left - info.borderWidth - info.horizontalPadding; + const left = Math.max(info.horizontalPadding, anchorBox.left - size.width - info.borderWidth); + const maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); + const maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + return { top: defaultTop, left, fit: width - size.width, maxSizeTop, maxSizeBottom, minSize: defaultMinSize.with(Math.min(width, defaultMinSize.width)) }; + })(); + + // SOUTH + const southPacement: Placement = (function () { + const left = anchorBox.left; + const top = -info.borderWidth + anchorBox.top + anchorBox.height; + const maxSizeBottom = new dom.Dimension(anchorBox.width - info.borderHeight, bodyBox.height - anchorBox.top - anchorBox.height - info.verticalPadding); + return { top, left, fit: maxSizeBottom.height - size.height, maxSizeBottom, maxSizeTop: maxSizeBottom, minSize: defaultMinSize.with(maxSizeBottom.width) }; + })(); + + // take first placement that fits or the first with "least bad" fit + const placements = [eastPlacement, westPlacement, southPacement]; + const placement = placements.find(p => p.fit >= 0) ?? placements.sort((a, b) => b.fit - a.fit)[0]; + + // top/bottom placement + const bottom = anchorBox.top + anchorBox.height - info.borderHeight; + let alignAtTop: boolean; + let height = size.height; + const maxHeight = Math.max(placement.maxSizeTop.height, placement.maxSizeBottom.height); + if (height > maxHeight) { + height = maxHeight; + } + let maxSize: dom.Dimension; + // if (preferAlignAtTop) { + if (height <= placement.maxSizeTop.height) { + alignAtTop = true; + maxSize = placement.maxSizeTop; + } else { + alignAtTop = false; + maxSize = placement.maxSizeBottom; + } + // } else { + // if (height <= placement.maxSizeBottom.height) { + // alignAtTop = false; + // maxSize = placement.maxSizeBottom; + // } else { + // alignAtTop = true; + // maxSize = placement.maxSizeTop; + // } + // } + + let { top, left } = placement; + if (!alignAtTop && height > anchorBox.height) { + top = bottom - height; + } + const editorDomNode = this._container; + if (editorDomNode) { + // get bounding rectangle of the suggest widget relative to the editor + const editorBoundingBox = editorDomNode.getBoundingClientRect(); + top -= editorBoundingBox.top; + left -= editorBoundingBox.left; + } + this._applyTopLeft({ left, top }); + + this._resizable.enableSashes(!alignAtTop, placement === eastPlacement, alignAtTop, placement !== eastPlacement); + + this._resizable.minSize = placement.minSize; + this._resizable.maxSize = maxSize; + this._resizable.layout(height, Math.min(maxSize.width, size.width)); + this.widget.layout(this._resizable.size.width, this._resizable.size.height); + } + + private _applyTopLeft(topLeft: { left: number; top: number }): void { + this._topLeft = topLeft; + // this._editor.layoutOverlayWidget(this); + this._resizable.domNode.style.top = `${topLeft.top}px`; + this._resizable.domNode.style.left = `${topLeft.left}px`; + this._resizable.domNode.style.position = 'absolute'; + } +} + +interface TopLeftPosition { + top: number; + left: number; +} diff --git a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts index 83605e2156d..a12d4e5440a 100644 --- a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts +++ b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts @@ -12,6 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { createMatches } from '../../../../base/common/filters.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; export function getAriaId(index: number): string { return `simple-suggest-aria-id-${index}`; @@ -55,13 +56,16 @@ export class SimpleSuggestWidgetItemRenderer implements IListRenderer(); readonly onDidToggleDetails: Event = this._onDidToggleDetails.event; + private readonly _disposables = new DisposableStore(); + readonly templateId = 'suggestion'; - constructor(private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo) { + constructor(private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, @IConfigurationService private readonly _configurationService: IConfigurationService) { } dispose(): void { this._onDidToggleDetails.dispose(); + this._disposables.dispose(); } renderTemplate(container: HTMLElement): ISimpleSuggestionTemplateData { @@ -111,11 +115,11 @@ export class SimpleSuggestWidgetItemRenderer implements IListRenderer { - // if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.suggestFontSize) || e.hasChanged(EditorOption.suggestLineHeight)) { - // configureFont(); - // } - // })); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.fontSize') || e.affectsConfiguration('editor.fontFamily') || e.affectsConfiguration('editor.lineHeight') || e.affectsConfiguration('editor.fontWeight')) { + configureFont(); + } + })); return { root, left, right, icon, colorspan, iconLabel, iconContainer, parametersLabel, qualifierLabel, detailsLabel, disposables }; } diff --git a/code/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/code/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index d84171e4354..902e171d399 100644 --- a/code/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/code/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -19,7 +19,7 @@ import { IElevatedFileService } from '../../files/common/elevatedFileService.js' import { IFilesConfigurationService } from '../../filesConfiguration/common/filesConfigurationService.js'; import { ILifecycleService } from '../../lifecycle/common/lifecycle.js'; import { IPathService } from '../../path/common/pathService.js'; -import { IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; +import { IUntitledTextEditorModelManager, IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkingCopyFileService } from '../../workingCopy/common/workingCopyFileService.js'; import { IDecorationsService } from '../../decorations/common/decorations.js'; @@ -28,7 +28,7 @@ export class BrowserTextFileService extends AbstractTextFileService { constructor( @IFileService fileService: IFileService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, diff --git a/code/src/vs/workbench/services/textfile/browser/textFileService.ts b/code/src/vs/workbench/services/textfile/browser/textFileService.ts index fe8816efc16..9ba24786bc5 100644 --- a/code/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/code/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -57,7 +57,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex constructor( @IFileService protected readonly fileService: IFileService, - @IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService protected readonly lifecycleService: ILifecycleService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @@ -161,7 +161,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex this._register(this.decorationsService.registerDecorationsProvider(provider)); } - //#endregin + //#endregion //#region text file read / write / create @@ -451,6 +451,11 @@ export abstract class AbstractTextFileService extends Disposable implements ITex this.logService.error(error); } + // Events + if (source.scheme === Schemas.untitled) { + this.untitled.notifyDidSave(source, target); + } + return target; } diff --git a/code/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/code/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index e48add78adb..0da81d1e2f6 100644 --- a/code/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/code/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -394,7 +394,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE try { await model.resolve(options); } catch (error) { - onUnexpectedError(error); + if (!model.isDisposed()) { + onUnexpectedError(error); // only log if the model is still around + } } })(); } diff --git a/code/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts b/code/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts index 7acdc4d22d5..913208b2221 100644 --- a/code/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts +++ b/code/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts @@ -10,7 +10,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { URI } from '../../../../base/common/uri.js'; import { IFileService, IFileReadLimits } from '../../../../platform/files/common/files.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; -import { IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; +import { IUntitledTextEditorModelManager, IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; import { ILifecycleService } from '../../lifecycle/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -33,7 +33,7 @@ export class NativeTextFileService extends AbstractTextFileService { constructor( @IFileService fileService: IFileService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, diff --git a/code/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/code/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index 08a9b059113..e8715a898c5 100644 --- a/code/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/code/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -60,7 +60,7 @@ const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint = new DisposableMap(); + private readonly _tokenizersRegistrations: DisposableMap = this._register(new DisposableMap()); constructor( @ILanguageService private readonly _languageService: ILanguageService, @@ -86,7 +96,7 @@ class TreeSitterTokenizationFeature extends Disposable implements ITreeSitterTok } } -class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTokenizationSupport { +export class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTokenizationSupport { private _query: Parser.Query | undefined; private readonly _onDidChangeTokens: Emitter<{ textModel: ITextModel; changes: IModelTokensChangedEvent }> = new Emitter(); public readonly onDidChangeTokens: Event<{ textModel: ITextModel; changes: IModelTokensChangedEvent }> = this._onDidChangeTokens.event; @@ -99,19 +109,197 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok private readonly _languageIdCodec: ILanguageIdCodec, @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService, @IThemeService private readonly _themeService: IThemeService, + @ITreeSitterTokenizationStoreService private readonly _tokenizationStoreService: ITreeSitterTokenizationStoreService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, ) { super(); this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => this.reset())); this._register(this._treeSitterService.onDidUpdateTree((e) => { - const maxLine = e.textModel.getLineCount(); + if (this._tokenizationStoreService.hasTokens(e.textModel)) { + // Mark the range for refresh immediately + for (const range of e.ranges) { + this._tokenizationStoreService.markForRefresh(e.textModel, range.newRange); + } + } + if (e.versionId !== e.textModel.getVersionId()) { + return; + } + + // First time we see a tree we need to build a token store. + if (!this._tokenizationStoreService.hasTokens(e.textModel)) { + this._firstTreeUpdate(e.textModel, e.versionId); + } else { + this._handleTreeUpdate(e); + } + })); + } + + private _createEmptyTokens(textModel: ITextModel) { + const languageId = this._languageIdCodec.encodeLanguageId(this._languageId); + const emptyToken = this._emptyToken(languageId); + const modelEndOffset = textModel.getValueLength(); + + const emptyTokens: TokenUpdate[] = [{ token: emptyToken, length: modelEndOffset, startOffsetInclusive: 0 }]; + return emptyTokens; + } + + private _firstTreeUpdate(textModel: ITextModel, versionId: number) { + const tokens: TokenUpdate[] = this._createEmptyTokens(textModel); + this._tokenizationStoreService.setTokens(textModel, tokens); + this._setViewPortTokens(textModel, versionId); + } + + private _setViewPortTokens(textModel: ITextModel, versionId: number) { + const maxLine = textModel.getLineCount(); + const editor = this._codeEditorService.listCodeEditors().find(editor => editor.getModel() === textModel); + if (!editor) { + return; + } + + const viewPort = editor.getVisibleRangesPlusViewportAboveBelow(); + const ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = new Array(viewPort.length); + const rangeChanges: RangeChange[] = new Array(viewPort.length); + + for (let i = 0; i < viewPort.length; i++) { + const range = viewPort[i]; + ranges[i] = { fromLineNumber: range.startLineNumber, toLineNumber: range.endLineNumber < maxLine ? range.endLineNumber : maxLine }; + const newRangeStartOffset = textModel.getOffsetAt(range.getStartPosition()); + const newRangeEndOffset = textModel.getOffsetAt(range.getEndPosition()); + rangeChanges[i] = { + newRange: range, + newRangeStartOffset, + newRangeEndOffset, + oldRangeLength: newRangeEndOffset - newRangeStartOffset + }; + } + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }); + } + + /** + * Do not await in this method, it will cause a race + */ + private _handleTreeUpdate(e: TreeUpdateEvent) { + let rangeChanges: RangeChange[] = []; + const chunkSize = 10000; + + for (let i = 0; i < e.ranges.length; i++) { + const rangeLength = e.ranges[i].newRangeEndOffset - e.ranges[i].newRangeStartOffset; + if (e.ranges[i].oldRangeLength === rangeLength) { + if (rangeLength > chunkSize) { + // Split the range into chunks to avoid long operations + const fullRangeEndOffset = e.ranges[i].newRangeEndOffset; + let chunkStart = e.ranges[i].newRangeStartOffset; + let chunkEnd = chunkStart + chunkSize; + let chunkStartingPosition = e.ranges[i].newRange.getStartPosition(); + do { + const chunkEndPosition = e.textModel.getPositionAt(chunkEnd); + const chunkRange = Range.fromPositions(chunkStartingPosition, chunkEndPosition); + + rangeChanges.push({ + newRange: chunkRange, + newRangeStartOffset: chunkStart, + newRangeEndOffset: chunkEnd, + oldRangeLength: chunkEnd - chunkStart + }); + + chunkStart = chunkEnd; + if (chunkEnd < fullRangeEndOffset && chunkEnd + chunkSize > fullRangeEndOffset) { + chunkEnd = fullRangeEndOffset; + } else { + chunkEnd = chunkEnd + chunkSize; + } + chunkStartingPosition = chunkEndPosition; + } while (chunkEnd <= fullRangeEndOffset); + } else { + rangeChanges.push(e.ranges[i]); + } + } else { + rangeChanges = e.ranges; + break; + } + } + + // Get the captures immediately while the text model is correct + const captures = rangeChanges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); + // Don't block + this._updateTreeForRanges(e.textModel, rangeChanges, e.versionId, captures).then(() => { + const tree = this._getTree(e.textModel); + if (!e.textModel.isDisposed() && (tree?.versionId === e.textModel.getVersionId())) { + this._refreshNeedsRefresh(e.textModel); + } + + }); + } + + private async _updateTreeForRanges(textModel: ITextModel, rangeChanges: RangeChange[], versionId: number, captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }[]) { + let tokenUpdate: { oldRangeLength: number; newTokens: TokenUpdate[] } | undefined; + + for (let i = 0; i < rangeChanges.length; i++) { + if (versionId !== textModel.getVersionId()) { + // Our captures have become invalid and we need to re-capture + break; + } + const capture = captures[i]; + const range = rangeChanges[i]; + + const updates = this.getTokensInRange(textModel, range.newRange, range.newRangeStartOffset, range.newRangeEndOffset, capture); + if (updates) { + tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: updates }; + } else { + tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: [] }; + } + this._tokenizationStoreService.updateTokens(textModel, versionId, [tokenUpdate]); this._onDidChangeTokens.fire({ - textModel: e.textModel, + textModel: textModel, changes: { semanticTokensApplied: false, - ranges: e.ranges.map(range => ({ fromLineNumber: range.startLineNumber, toLineNumber: range.endLineNumber < maxLine ? range.endLineNumber : maxLine })), + ranges: [{ fromLineNumber: range.newRange.getStartPosition().lineNumber, toLineNumber: range.newRange.getEndPosition().lineNumber }] } }); - })); + await new Promise(resolve => setTimeout0(resolve)); + } + } + + private _refreshNeedsRefresh(textModel: ITextModel) { + const rangesToRefresh = this._tokenizationStoreService.getNeedsRefresh(textModel); + if (rangesToRefresh.length === 0) { + return; + } + const rangeChanges: RangeChange[] = new Array(rangesToRefresh.length); + + for (let i = 0; i < rangesToRefresh.length; i++) { + const range = rangesToRefresh[i]; + rangeChanges[i] = { + newRange: range.range, + newRangeStartOffset: range.startOffset, + newRangeEndOffset: range.endOffset, + oldRangeLength: range.endOffset - range.startOffset + }; + } + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }); + } + + private _rangeTokensAsUpdates(rangeOffset: number, endOffsetToken: EndOffsetToken[]) { + const updates: TokenUpdate[] = []; + let lastEnd = 0; + for (const token of endOffsetToken) { + if (token.endOffset <= lastEnd) { + continue; + } + updates.push({ startOffsetInclusive: rangeOffset + lastEnd, length: token.endOffset - lastEnd, token: token.metadata }); + lastEnd = token.endOffset; + } + return updates; + } + + public getTokensInRange(textModel: ITextModel, range: Range, rangeStartOffset: number, rangeEndOffset: number, captures?: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }): TokenUpdate[] | undefined { + const languageId = this._languageIdCodec.encodeLanguageId(this._languageId); + + const tokens = captures ? this._tokenizeCapturesWithMetadata(captures.tree, captures.captures, languageId, rangeStartOffset, rangeEndOffset) : this._tokenize(languageId, range, rangeStartOffset, rangeEndOffset, textModel); + if (tokens?.endOffsetsAndMetadata) { + return this._rangeTokensAsUpdates(rangeStartOffset, tokens.endOffsetsAndMetadata); + } + return undefined; } private _getTree(textModel: ITextModel): ITreeSitterParseResult | undefined { @@ -138,25 +326,34 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok this._colorThemeData = this._themeService.getColorTheme() as ColorThemeData; } - captureAtPosition(lineNumber: number, column: number, textModel: ITextModel): Parser.QueryCapture[] { + captureAtPosition(lineNumber: number, column: number, textModel: ITextModel): QueryCapture[] { const tree = this._getTree(textModel); - const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column + 1), tree?.tree); + const captures = this._captureAtRange(new Range(lineNumber, column, lineNumber, column + 1), tree?.tree); return captures; } - captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): Parser.QueryCapture[] { - const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column + 1), tree); + captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): QueryCapture[] { + const captures = this._captureAtRange(new Range(lineNumber, column, lineNumber, column + 1), tree); return captures; } - private _captureAtRange(lineNumber: number, columnRange: ColumnRange, tree: Parser.Tree | undefined): Parser.QueryCapture[] { + private _captureAtRange(range: Range, tree: Parser.Tree | undefined): QueryCapture[] { const query = this._ensureQuery(); if (!tree || !query) { return []; } // Tree sitter row is 0 based, column is 0 based - return query.captures(tree.rootNode, { startPosition: { row: lineNumber - 1, column: columnRange.startColumn - 1 }, endPosition: { row: lineNumber - 1, column: columnRange.endColumnExclusive - 1 } }); + return query.captures(tree.rootNode, { startPosition: { row: range.startLineNumber - 1, column: range.startColumn - 1 }, endPosition: { row: range.endLineNumber - 1, column: range.endColumn - 1 } }).map(capture => ( + { + name: capture.name, + text: capture.node.text, + node: { + startIndex: capture.node.startIndex, + endIndex: capture.node.endIndex + } + } + )); } /** @@ -174,20 +371,26 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok return this._tokenizeEncoded(lineNumber, textModel); } - private _tokenizeEncoded(lineNumber: number, textModel: ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined { - const stopwatch = StopWatch.create(); - const lineLength = textModel.getLineMaxColumn(lineNumber); + private _getTreeAndCaptures(range: Range, textModel: ITextModel): { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] } { const tree = this._getTree(textModel); - const captures = this._captureAtRange(lineNumber, new ColumnRange(1, lineLength), tree?.tree); - const encodedLanguageId = this._languageIdCodec.encodeLanguageId(this._languageId); + const captures = this._captureAtRange(range, tree?.tree); + return { tree, captures }; + } + + private _tokenize(encodedLanguageId: LanguageId, range: Range, rangeStartOffset: number, rangeEndOffset: number, textModel: ITextModel): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { + const { tree, captures } = this._getTreeAndCaptures(range, textModel); + return this._tokenizeCapturesWithMetadata(tree, captures, encodedLanguageId, rangeStartOffset, rangeEndOffset); + } + + private _createTokensFromCaptures(tree: ITreeSitterParseResult | undefined, captures: QueryCapture[], rangeStartOffset: number, rangeEndOffset: number): { endOffsets: { endOffset: number; scopes: string[] }[]; captureTime: number } | undefined { + const stopwatch = StopWatch.create(); + const rangeLength = rangeEndOffset - rangeStartOffset; if (captures.length === 0) { if (tree) { stopwatch.stop(); - const result = new Uint32Array(2); - result[0] = lineLength; - result[1] = findMetadata(this._colorThemeData, [], encodedLanguageId); - return { result, captureTime: stopwatch.elapsed(), metadataTime: 0 }; + const endOffsetsAndMetadata = [{ endOffset: rangeLength, scopes: [] }]; + return { endOffsets: endOffsetsAndMetadata, captureTime: stopwatch.elapsed() }; } return undefined; } @@ -195,19 +398,18 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok const endOffsetsAndScopes: { endOffset: number; scopes: string[] }[] = Array(captures.length); endOffsetsAndScopes.fill({ endOffset: 0, scopes: [] }); let tokenIndex = 0; - const lineStartOffset = textModel.getOffsetAt({ lineNumber: lineNumber, column: 1 }); const increaseSizeOfTokensByOneToken = () => { endOffsetsAndScopes.push({ endOffset: 0, scopes: [] }); }; - for (let captureIndex = 0; captureIndex < captures.length; captureIndex++) { const capture = captures[captureIndex]; - const tokenEndIndex = capture.node.endIndex < lineStartOffset + lineLength ? capture.node.endIndex : lineStartOffset + lineLength; - const tokenStartIndex = capture.node.startIndex < lineStartOffset ? lineStartOffset : capture.node.startIndex; + const tokenEndIndex = capture.node.endIndex < rangeEndOffset ? ((capture.node.endIndex < rangeStartOffset) ? rangeStartOffset : capture.node.endIndex) : rangeEndOffset; + const tokenStartIndex = capture.node.startIndex < rangeStartOffset ? rangeStartOffset : ((capture.node.startIndex > tokenEndIndex) ? tokenEndIndex : capture.node.startIndex); + + const lineRelativeOffset = tokenEndIndex - rangeStartOffset; - const lineRelativeOffset = tokenEndIndex - lineStartOffset; // Not every character will get captured, so we need to make sure that our current capture doesn't bleed toward the start of the line and cover characters that it doesn't apply to. // We do this by creating a new token in the array if the previous token ends before the current token starts. let previousTokenEnd: number; @@ -215,7 +417,7 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok if (captureIndex > 0) { previousTokenEnd = endOffsetsAndScopes[(tokenIndex - 1)].endOffset; } else { - previousTokenEnd = tokenStartIndex - lineStartOffset - 1; + previousTokenEnd = tokenStartIndex - rangeStartOffset - 1; } const intermediateTokenOffset = lineRelativeOffset - currentTokenLength; if ((previousTokenEnd >= 0) && (previousTokenEnd < intermediateTokenOffset)) { @@ -248,9 +450,9 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok if (previousPreviousTokenEndOffset !== intermediateTokenOffset) { endOffsetsAndScopes[tokenIndex - 1] = { endOffset: intermediateTokenOffset, scopes: endOffsetsAndScopes[tokenIndex - 1].scopes }; addCurrentTokenToArray(); - originalPreviousTokenScopes = endOffsetsAndScopes[tokenIndex - 2].scopes; + originalPreviousTokenScopes = [...endOffsetsAndScopes[tokenIndex - 2].scopes]; } else { - originalPreviousTokenScopes = endOffsetsAndScopes[tokenIndex - 1].scopes; + originalPreviousTokenScopes = [...endOffsetsAndScopes[tokenIndex - 1].scopes]; endOffsetsAndScopes[tokenIndex - 1] = { endOffset: lineRelativeOffset, scopes: [capture.name] }; } @@ -270,25 +472,66 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok } // Account for uncaptured characters at the end of the line - if (endOffsetsAndScopes[tokenIndex - 1].endOffset < lineLength - 1) { - increaseSizeOfTokensByOneToken(); - endOffsetsAndScopes[tokenIndex] = { endOffset: lineLength - 1, scopes: endOffsetsAndScopes[tokenIndex].scopes }; - tokenIndex++; + if ((endOffsetsAndScopes[tokenIndex - 1].endOffset < rangeLength)) { + if (rangeLength - endOffsetsAndScopes[tokenIndex - 1].endOffset > 0) { + increaseSizeOfTokensByOneToken(); + endOffsetsAndScopes[tokenIndex] = { endOffset: rangeLength, scopes: endOffsetsAndScopes[tokenIndex].scopes }; + tokenIndex++; + } } - const captureTime = stopwatch.elapsed(); - stopwatch.reset(); - - const tokens: Uint32Array = new Uint32Array((tokenIndex) * 2); - for (let i = 0; i < tokenIndex; i++) { + for (let i = 0; i < endOffsetsAndScopes.length; i++) { const token = endOffsetsAndScopes[i]; - if (token.endOffset === 0 && token.scopes.length === 0) { + if (token.endOffset === 0 && token.scopes.length === 0 && i !== 0) { + endOffsetsAndScopes.splice(i, endOffsetsAndScopes.length - i); break; } - tokens[i * 2] = token.endOffset; - tokens[i * 2 + 1] = findMetadata(this._colorThemeData, token.scopes, encodedLanguageId); } + const captureTime = stopwatch.elapsed(); + return { endOffsets: endOffsetsAndScopes as { endOffset: number; scopes: string[] }[], captureTime }; + + } + + private _tokenizeCapturesWithMetadata(tree: ITreeSitterParseResult | undefined, captures: QueryCapture[], encodedLanguageId: LanguageId, rangeStartOffset: number, rangeEndOffset: number): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { + const stopwatch = StopWatch.create(); + const emptyTokens = this._createTokensFromCaptures(tree, captures, rangeStartOffset, rangeEndOffset); + if (!emptyTokens) { + return undefined; + } + const endOffsetsAndScopes: { endOffset: number; scopes: string[]; metadata?: number }[] = emptyTokens.endOffsets; + for (let i = 0; i < endOffsetsAndScopes.length; i++) { + const token = endOffsetsAndScopes[i]; + token.metadata = findMetadata(this._colorThemeData, token.scopes, encodedLanguageId); + } + const metadataTime = stopwatch.elapsed(); - return { result: tokens, captureTime, metadataTime }; + return { endOffsetsAndMetadata: endOffsetsAndScopes as { endOffset: number; scopes: string[]; metadata: number }[], captureTime: emptyTokens.captureTime, metadataTime }; + } + + private _emptyToken(encodedLanguageId: number) { + return findMetadata(this._colorThemeData, [], encodedLanguageId); + } + + private _tokenizeEncoded(lineNumber: number, textModel: ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined { + const encodedLanguageId = this._languageIdCodec.encodeLanguageId(this._languageId); + const lineOffset = textModel.getOffsetAt({ lineNumber: lineNumber, column: 1 }); + const maxLine = textModel.getLineCount(); + const lineEndOffset = (lineNumber + 1 <= maxLine) ? textModel.getOffsetAt({ lineNumber: lineNumber + 1, column: 1 }) : textModel.getValueLength(); + const lineLength = lineEndOffset - lineOffset; + + const result = this._tokenize(encodedLanguageId, new Range(lineNumber, 1, lineNumber, lineLength), lineOffset, lineEndOffset, textModel); + if (!result) { + return undefined; + } + + const tokens: Uint32Array = new Uint32Array((result.endOffsetsAndMetadata.length) * 2); + + for (let i = 0; i < result.endOffsetsAndMetadata.length; i++) { + const token = result.endOffsetsAndMetadata[i]; + tokens[i * 2] = token.endOffset; + tokens[i * 2 + 1] = token.metadata; + } + + return { result: tokens, captureTime: result.captureTime, metadataTime: result.metadataTime }; } override dispose() { diff --git a/code/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts b/code/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index 5b53a994a37..53932b615d8 100644 --- a/code/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/code/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -61,7 +61,29 @@ export interface INewUntitledTextEditorWithAssociatedResourceOptions extends INe type IInternalUntitledTextEditorOptions = IExistingUntitledTextEditorOptions & INewUntitledTextEditorWithAssociatedResourceOptions; -export interface IUntitledTextEditorModelManager { +export interface IUntitledTextEditorModelSaveEvent { + + /** + * The source untitled file that was saved. It is disposed at this point. + */ + readonly source: URI; + + /** + * The target file the untitled was saved to. Is never untitled. + */ + readonly target: URI; +} + +export interface IUntitledTextEditorService { + + readonly _serviceBrand: undefined; + + /** + * An event for when an untitled editor model was saved to disk. + * At the point the event fires, the untitled editor model is + * disposed. + */ + readonly onDidSave: Event; /** * Events for when untitled text editors change (e.g. getting dirty, saved or reverted). @@ -131,17 +153,23 @@ export interface IUntitledTextEditorModelManager { canDispose(model: IUntitledTextEditorModel): true | Promise; } -export interface IUntitledTextEditorService extends IUntitledTextEditorModelManager { +export interface IUntitledTextEditorModelManager extends IUntitledTextEditorService { - readonly _serviceBrand: undefined; + /** + * Internal method: triggers the onDidSave event. + */ + notifyDidSave(source: URI, target: URI): void; } -export class UntitledTextEditorService extends Disposable implements IUntitledTextEditorService { +export class UntitledTextEditorService extends Disposable implements IUntitledTextEditorModelManager { declare readonly _serviceBrand: undefined; private static readonly UNTITLED_WITHOUT_ASSOCIATED_RESOURCE_REGEX = /Untitled-\d+/; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -311,6 +339,10 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe return true; } + + notifyDidSave(source: URI, target: URI): void { + this._onDidSave.fire({ source, target }); + } } registerSingleton(IUntitledTextEditorService, UntitledTextEditorService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts b/code/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts index e268a0b1beb..fdb412bdd23 100644 --- a/code/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts +++ b/code/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts @@ -81,7 +81,7 @@ export class SettingsResource implements IProfileResource { private getIgnoredSettings(): string[] { const allSettings = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); + const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.APPLICATION_MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); return ignoredSettings; } diff --git a/code/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/code/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index e4b29d57e53..cc56d9a6ac5 100644 --- a/code/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/code/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -15,7 +15,6 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IRequestService, asJson } from '../../../../platform/request/common/request.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { isEmptyWorkspaceIdentifier, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; @@ -25,16 +24,6 @@ import { IExtensionService } from '../../extensions/common/extensions.js'; import { IHostService } from '../../host/browser/host.js'; import { DidChangeUserDataProfileEvent, IProfileTemplateInfo, IUserDataProfileManagementService, IUserDataProfileService } from '../common/userDataProfile.js'; -export type ProfileManagementActionExecutedClassification = { - owner: 'sandy081'; - comment: 'Logged when profile management action is excuted'; - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run.' }; -}; - -export type ProfileManagementActionExecutedEvent = { - id: string; -}; - export class UserDataProfileManagementService extends Disposable implements IUserDataProfileManagementService { readonly _serviceBrand: undefined; @@ -46,7 +35,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, @IRequestService private readonly requestService: IRequestService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -122,14 +110,12 @@ export class UserDataProfileManagementService extends Disposable implements IUse async createAndEnterProfile(name: string, options?: IUserDataProfileOptions): Promise { const profile = await this.userDataProfilesService.createNamedProfile(name, options, toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); await this.changeCurrentProfile(profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'createAndEnterProfile' }); return profile; } async createAndEnterTransientProfile(): Promise { const profile = await this.userDataProfilesService.createTransientProfile(toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); await this.changeCurrentProfile(profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'createAndEnterTransientProfile' }); return profile; } @@ -141,7 +127,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default profile")); } const updatedProfile = await this.userDataProfilesService.updateProfile(profile, updateOptions); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'updateProfile' }); return updatedProfile; } @@ -153,7 +138,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse throw new Error(localize('cannotDeleteDefaultProfile', "Cannot delete the default profile")); } await this.userDataProfilesService.removeProfile(profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'removeProfile' }); } async switchProfile(profile: IUserDataProfile): Promise { @@ -169,7 +153,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse } const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceContextService.getWorkspace()); await this.userDataProfilesService.setProfileForWorkspace(workspaceIdentifier, profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'switchProfile' }); if (isEmptyWorkspaceIdentifier(workspaceIdentifier)) { await this.changeCurrentProfile(profile); } diff --git a/code/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/code/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 6a1eb065c6c..ea27f81b3bd 100644 --- a/code/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/code/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, IUserDataSyncStoreManagementService, SyncStatus, IUserDataSyncEnablementService, IUserDataSyncResource, IResourcePreview, USER_DATA_SYNC_SCHEME, USER_DATA_SYNC_LOG_ID, } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE, SYNC_CONFLICTS_VIEW_ID, CONTEXT_ENABLE_SYNC_CONFLICTS_VIEW, CONTEXT_HAS_CONFLICTS, IUserDataSyncConflictsView, getSyncAreaLabel } from '../common/userDataSync.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -104,7 +103,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IStorageService private readonly storageService: IStorageService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @IProductService private readonly productService: IProductService, @IExtensionService private readonly extensionService: IExtensionService, @@ -716,7 +714,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async onDidAuthFailure(): Promise { - this.telemetryService.publicLog2<{}, { owner: 'sandy081'; comment: 'Report when there are successive auth failures during settings sync' }>('sync/successiveAuthFailures'); this.currentSessionId = undefined; await this.update('auth failure'); } diff --git a/code/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/code/src/vs/workbench/services/views/browser/viewDescriptorService.ts index c667e719ae4..c528489f5a0 100644 --- a/code/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/code/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -23,6 +23,7 @@ import { IStringDictionary } from '../../../../base/common/collections.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { IViewsService } from '../common/viewsService.js'; +import { windowLogGroup } from '../../log/common/logConstants.js'; interface IViewsCustomizations { viewContainerLocations: IStringDictionary; @@ -79,7 +80,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor ) { super(); - this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, hidden: true })); + this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, group: windowLogGroup })); this.activeViewContextKeys = new Map>(); this.movableViewContextKeys = new Map>(); diff --git a/code/src/vs/workbench/services/views/common/viewContainerModel.ts b/code/src/vs/workbench/services/views/common/viewContainerModel.ts index e7d3665d9f9..c109542cafe 100644 --- a/code/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/code/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -9,7 +9,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { Registry } from '../../../../platform/registry/common/platform.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { URI } from '../../../../base/common/uri.js'; import { coalesce, move } from '../../../../base/common/arrays.js'; import { isUndefined, isUndefinedOrNull } from '../../../../base/common/types.js'; @@ -17,29 +17,9 @@ import { isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { IOutputService } from '../../output/common/output.js'; import { CounterSet } from '../../../../base/common/map.js'; -import { localize2 } from '../../../../nls.js'; import { Lazy } from '../../../../base/common/lazy.js'; - -registerAction2(class extends Action2 { - constructor() { - super({ - id: '_workbench.output.showViewsLog', - title: localize2('showViewsLog', "Show Views Log"), - category: Categories.Developer, - f1: true - }); - } - async run(servicesAccessor: ServicesAccessor): Promise { - const loggerService = servicesAccessor.get(ILoggerService); - const outputService = servicesAccessor.get(IOutputService); - loggerService.setVisibility(VIEWS_LOG_ID, true); - outputService.showChannel(VIEWS_LOG_ID); - } -}); +import { windowLogGroup } from '../../log/common/logConstants.js'; export function getViewsStateStorageId(viewContainerStorageId: string): string { return `${viewContainerStorageId}.hidden`; } @@ -84,7 +64,7 @@ class ViewDescriptorsState extends Disposable { ) { super(); - this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, hidden: true })); + this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, group: windowLogGroup })); this.globalViewsStateStorageId = getViewsStateStorageId(viewContainerStorageId); this.workspaceViewsStateStorageId = viewContainerStorageId; @@ -354,7 +334,7 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode ) { super(); - this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, hidden: true })); + this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, group: windowLogGroup })); this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(() => this.onDidChangeContext())); this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, viewContainer.storageId || `${viewContainer.id}.state`, typeof viewContainer.title === 'string' ? viewContainer.title : viewContainer.title.original)); diff --git a/code/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/code/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 80ad8dadfce..017d52d081e 100644 --- a/code/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/code/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -277,7 +277,7 @@ export class FileWorkingCopyManager try { await workingCopy.resolve(resolveOptions); } catch (error) { - onUnexpectedError(error); + if (!workingCopy.isDisposed()) { + onUnexpectedError(error); // only log if the working copy is still around + } } })(); } diff --git a/code/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/code/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 371ff18863d..4edbabee146 100644 --- a/code/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/code/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -16,6 +16,19 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from './abstractFileWorkingCopyManager.js'; import { ResourceMap } from '../../../../base/common/map.js'; +export interface IUntitledFileWorkingCopySaveEvent { + + /** + * The source untitled file working copy that was saved. It is disposed at this point. + */ + readonly source: URI; + + /** + * The target file working copy the untitled was saved to. Is never untitled. + */ + readonly target: URI; +} + /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and * handle all operations that are working copy related, such as save/revert, @@ -23,6 +36,13 @@ import { ResourceMap } from '../../../../base/common/map.js'; */ export interface IUntitledFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { + /** + * An event for when an untitled file working copy was saved. + * At the point the event fires, the untitled file working copy is + * disposed. + */ + readonly onDidSave: Event; + /** * An event for when a untitled file working copy changed it's dirty state. */ @@ -57,6 +77,11 @@ export interface IUntitledFileWorkingCopyManager>; + + /** + * Internal method: triggers the onDidSave event. + */ + notifyDidSave(source: URI, target: URI): void; } export interface INewUntitledFileWorkingCopyOptions { @@ -105,6 +130,9 @@ export class UntitledFileWorkingCopyManager()); + readonly onDidSave = this._onDidSave.event; + private readonly _onDidChangeDirty = this._register(new Emitter>()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -269,4 +297,8 @@ export class UntitledFileWorkingCopyManager { }); test('save - without associated resource', async () => { + let savedEvent: { source: URI; target: URI } | undefined = undefined; + disposables.add(manager.untitled.onDidSave(e => { + savedEvent = e; + })); + const workingCopy = await manager.untitled.resolve(); workingCopy.model?.updateContents('Simple Save'); @@ -255,11 +260,18 @@ suite('UntitledFileWorkingCopyManager', () => { assert.ok(result); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + assert.strictEqual(savedEvent!.source.toString(), workingCopy.resource.toString()); + assert.strictEqual(savedEvent!.target.toString(), URI.file('simple/file.txt').toString()); workingCopy.dispose(); }); test('save - with associated resource', async () => { + let savedEvent: { source: URI; target: URI } | undefined = undefined; + disposables.add(manager.untitled.onDidSave(e => { + savedEvent = e; + })); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); @@ -269,6 +281,8 @@ suite('UntitledFileWorkingCopyManager', () => { assert.ok(result); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + assert.strictEqual(savedEvent!.source.toString(), workingCopy.resource.toString()); + assert.strictEqual(savedEvent!.target.toString(), URI.file('/some/associated.txt').toString()); workingCopy.dispose(); }); diff --git a/code/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts b/code/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts index c0a79706de5..426ef2e55e1 100644 --- a/code/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts +++ b/code/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { StatusbarViewModel } from '../../../../browser/parts/statusbar/statusbarModel.js'; +import { IStatusbarViewModelEntry, StatusbarViewModel } from '../../../../browser/parts/statusbar/statusbarModel.js'; import { TestStorageService } from '../../../common/workbenchTestServices.js'; import { StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -24,13 +24,13 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.entries.length, 0); - const entry1 = { id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry1: IStatusbarViewModelEntry = { id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry1); - const entry2 = { id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry2: IStatusbarViewModelEntry = { id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry2); - const entry3 = { id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry3: IStatusbarViewModelEntry = { id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry3); - const entry4 = { id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry4: IStatusbarViewModelEntry = { id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry4); assert.strictEqual(model.entries.length, 4); @@ -84,9 +84,9 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.entries.length, 0); - model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 2 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 3 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 2 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 3 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); const entries = model.entries; assert.strictEqual(entries[0].id, '3'); @@ -100,9 +100,9 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.entries.length, 0); - model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); const entries = model.entries; assert.strictEqual(entries[0].id, '1'); @@ -115,10 +115,10 @@ suite('Workbench status bar model', () => { const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); // Existing reference, Alignment: left - model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); - let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); let entries = model.entries; @@ -130,7 +130,7 @@ suite('Workbench status bar model', () => { model.remove(entry); // Existing reference, Alignment: right - entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); entries = model.entries; @@ -145,10 +145,10 @@ suite('Workbench status bar model', () => { const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); // Nonexistent reference, Alignment: left - model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); - let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); let entries = model.entries; @@ -160,7 +160,7 @@ suite('Workbench status bar model', () => { model.remove(entry); // Nonexistent reference, Alignment: right - entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); entries = model.entries; @@ -174,9 +174,9 @@ suite('Workbench status bar model', () => { const container = document.createElement('div'); const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); - model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); let entries = model.entries; assert.strictEqual(entries.length, 3); @@ -184,7 +184,7 @@ suite('Workbench status bar model', () => { assert.strictEqual(entries[1].id, 'b'); assert.strictEqual(entries[2].id, 'c'); - const entry = { id: 'not-existing', alignment: StatusbarAlignment.LEFT, name: 'not-existing', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry = { id: 'not-existing', alignment: StatusbarAlignment.LEFT, name: 'not-existing', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); entries = model.entries; @@ -207,16 +207,16 @@ suite('Workbench status bar model', () => { const container = document.createElement('div'); const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); - model.add({ id: '1-left', alignment: StatusbarAlignment.LEFT, name: '1-left', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2-left', alignment: StatusbarAlignment.LEFT, name: '2-left', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1-left', alignment: StatusbarAlignment.LEFT, name: '1-left', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2-left', alignment: StatusbarAlignment.LEFT, name: '2-left', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); - model.add({ id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2-right', alignment: StatusbarAlignment.RIGHT, name: '2-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2-right', alignment: StatusbarAlignment.RIGHT, name: '2-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 2); assert.strictEqual(model.getEntries(StatusbarAlignment.RIGHT).length, 2); - const relativeEntryLeft = { id: 'relative', alignment: StatusbarAlignment.LEFT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const relativeEntryLeft = { id: 'relative', alignment: StatusbarAlignment.LEFT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(relativeEntryLeft); assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 3); @@ -225,7 +225,7 @@ suite('Workbench status bar model', () => { model.remove(relativeEntryLeft); - const relativeEntryRight = { id: 'relative', alignment: StatusbarAlignment.RIGHT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const relativeEntryRight = { id: 'relative', alignment: StatusbarAlignment.RIGHT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(relativeEntryRight); assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 2); diff --git a/code/src/vs/workbench/test/browser/workbenchTestServices.ts b/code/src/vs/workbench/test/browser/workbenchTestServices.ts index 259de7b24b0..8c5965ec8f4 100644 --- a/code/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/code/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -19,7 +19,7 @@ import { IWorkbenchLayoutService, PanelAlignment, Parts, Position as PartPositio import { TextModelResolverService } from '../../services/textmodelResolver/common/textModelResolverService.js'; import { ITextModelService } from '../../../editor/common/services/resolverService.js'; import { IEditorOptions, IResourceEditorInput, IResourceEditorInputIdentifier, ITextResourceEditorInput, ITextEditorOptions } from '../../../platform/editor/common/editor.js'; -import { IUntitledTextEditorService, UntitledTextEditorService } from '../../services/untitled/common/untitledTextEditorService.js'; +import { IUntitledTextEditorModelManager, IUntitledTextEditorService, UntitledTextEditorService } from '../../services/untitled/common/untitledTextEditorService.js'; import { IWorkspaceContextService, IWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js'; import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from '../../services/lifecycle/common/lifecycle.js'; import { ServiceCollection } from '../../../platform/instantiation/common/serviceCollection.js'; @@ -414,7 +414,7 @@ export class TestTextFileService extends BrowserTextFileService { constructor( @IFileService fileService: IFileService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @@ -2134,6 +2134,7 @@ export class TestQuickInputService implements IQuickInputService { accept(): Promise { throw new Error('not implemented.'); } back(): Promise { throw new Error('not implemented.'); } cancel(): Promise { throw new Error('not implemented.'); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { throw new Error('not implemented.'); } } class TestLanguageDetectionService implements ILanguageDetectionService { @@ -2247,6 +2248,8 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens getExtensions(): Promise { throw new Error('Method not implemented.'); } resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { throw new Error('Method not implemented.'); } getInstallableServers(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } + isPublisherTrusted(extension: IGalleryExtension): boolean { return false; } + trustPublishers(...publishers: string[]): void { } } export class TestUserDataProfileService implements IUserDataProfileService { diff --git a/code/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts b/code/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts new file mode 100644 index 00000000000..1eab214ca26 --- /dev/null +++ b/code/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts @@ -0,0 +1,377 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; +import { TreeSitterTextModelService } from '../../../editor/common/services/treeSitter/treeSitterParserService.js'; +import { IModelService } from '../../../editor/common/services/model.js'; +import { Event } from '../../../base/common/event.js'; +import { URI } from '../../../base/common/uri.js'; +import { IFileService } from '../../../platform/files/common/files.js'; +import { ILogService, NullLogService } from '../../../platform/log/common/log.js'; +import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js'; +import { ClassifiedEvent, OmitMetadata, IGDPRProperty, StrictPropertyCheck } from '../../../platform/telemetry/common/gdprTypings.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js'; +import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; +import { ModelService } from '../../../editor/common/services/modelService.js'; +// eslint-disable-next-line local/code-layering, local/code-import-patterns +import { TreeSitterTokenizationFeature } from '../../services/treeSitter/browser/treeSitterTokenizationFeature.js'; +import { ITreeSitterParserService, TreeUpdateEvent } from '../../../editor/common/services/treeSitterParserService.js'; +import { ITreeSitterTokenizationSupport, TreeSitterTokenizationRegistry } from '../../../editor/common/languages.js'; +import { FileService } from '../../../platform/files/common/fileService.js'; +import { Schemas } from '../../../base/common/network.js'; +import { DiskFileSystemProvider } from '../../../platform/files/node/diskFileSystemProvider.js'; +import { ILanguageService } from '../../../editor/common/languages/language.js'; +import { LanguageService } from '../../../editor/common/services/languageService.js'; +import { TestColorTheme, TestThemeService } from '../../../platform/theme/test/common/testThemeService.js'; +import { IThemeService } from '../../../platform/theme/common/themeService.js'; +import { ITextResourcePropertiesService } from '../../../editor/common/services/textResourceConfiguration.js'; +import { TestTextResourcePropertiesService } from '../common/workbenchTestServices.js'; +import { TestLanguageConfigurationService } from '../../../editor/test/common/modes/testLanguageConfigurationService.js'; +import { ILanguageConfigurationService } from '../../../editor/common/languages/languageConfigurationRegistry.js'; +import { IUndoRedoService } from '../../../platform/undoRedo/common/undoRedo.js'; +import { UndoRedoService } from '../../../platform/undoRedo/common/undoRedoService.js'; +import { TestDialogService } from '../../../platform/dialogs/test/common/testDialogService.js'; +import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js'; +import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; +import { ProbeScope, TokenStyle } from '../../../platform/theme/common/tokenClassificationRegistry.js'; +import { TextMateThemingRuleDefinitions } from '../../services/themes/common/colorThemeData.js'; +import { Color } from '../../../base/common/color.js'; +import { ITreeSitterTokenizationStoreService } from '../../../editor/common/model/treeSitterTokenStoreService.js'; +import { Range } from '../../../editor/common/core/range.js'; +import { ITextModel } from '../../../editor/common/model.js'; +import { TokenUpdate } from '../../../editor/common/model/tokenStore.js'; + +class MockTelemetryService implements ITelemetryService { + _serviceBrand: undefined; + telemetryLevel: TelemetryLevel = TelemetryLevel.NONE; + sessionId: string = ''; + machineId: string = ''; + sqmId: string = ''; + devDeviceId: string = ''; + firstSessionDate: string = ''; + sendErrorTelemetry: boolean = false; + publicLog(eventName: string, data?: ITelemetryData): void { + } + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void { + } + publicLogError(errorEventName: string, data?: ITelemetryData): void { + } + publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void { + } + setExperimentProperty(name: string, value: string): void { + } +} + +class MockTokenStoreService implements ITreeSitterTokenizationStoreService { + getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[] { + throw new Error('Method not implemented.'); + } + + _serviceBrand: undefined; + setTokens(model: ITextModel, tokens: TokenUpdate[]): void { + } + getTokens(model: ITextModel, line: number): Uint32Array | undefined { + return undefined; + } + updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void { + } + markForRefresh(model: ITextModel, range: Range): void { + } + hasTokens(model: ITextModel, accurateForRange?: Range): boolean { + return true; + } + +} + +class TestTreeSitterColorTheme extends TestColorTheme { + public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined { + return new TokenStyle(Color.red, undefined, undefined, undefined, undefined); + } + public getTokenColorIndex(): { get: () => number } { + return { get: () => 10 }; + } +} + +suite('Tree Sitter TokenizationFeature', function () { + + let instantiationService: TestInstantiationService; + let modelService: IModelService; + let fileService: IFileService; + let textResourcePropertiesService: ITextResourcePropertiesService; + let languageConfigurationService: ILanguageConfigurationService; + const telemetryService: ITelemetryService = new MockTelemetryService(); + const logService: ILogService = new NullLogService(); + const configurationService: IConfigurationService = new TestConfigurationService({ 'editor.experimental.preferTreeSitter': ['typescript'] }); + const themeService: IThemeService = new TestThemeService(new TestTreeSitterColorTheme()); + let languageService: ILanguageService; + const environmentService: IEnvironmentService = {} as IEnvironmentService; + const tokenStoreService: ITreeSitterTokenizationStoreService = new MockTokenStoreService(); + let treeSitterParserService: TreeSitterTextModelService; + let treeSitterTokenizationSupport: ITreeSitterTokenizationSupport; + + let disposables: DisposableStore; + + setup(async () => { + disposables = new DisposableStore(); + instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.set(IEnvironmentService, environmentService); + instantiationService.set(IConfigurationService, configurationService); + instantiationService.set(ILogService, logService); + instantiationService.set(ITelemetryService, telemetryService); + instantiationService.set(ITreeSitterTokenizationStoreService, tokenStoreService); + languageService = disposables.add(instantiationService.createInstance(LanguageService)); + instantiationService.set(ILanguageService, languageService); + instantiationService.set(IThemeService, themeService); + textResourcePropertiesService = instantiationService.createInstance(TestTextResourcePropertiesService); + instantiationService.set(ITextResourcePropertiesService, textResourcePropertiesService); + languageConfigurationService = disposables.add(instantiationService.createInstance(TestLanguageConfigurationService)); + instantiationService.set(ILanguageConfigurationService, languageConfigurationService); + + fileService = disposables.add(instantiationService.createInstance(FileService)); + const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService)); + disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider)); + + instantiationService.set(IFileService, fileService); + + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); + instantiationService.set(IUndoRedoService, undoRedoService); + modelService = new ModelService( + configurationService, + textResourcePropertiesService, + undoRedoService, + instantiationService + ); + instantiationService.set(IModelService, modelService); + treeSitterParserService = disposables.add(instantiationService.createInstance(TreeSitterTextModelService)); + treeSitterParserService.isTest = true; + instantiationService.set(ITreeSitterParserService, treeSitterParserService); + disposables.add(instantiationService.createInstance(TreeSitterTokenizationFeature)); + treeSitterTokenizationSupport = disposables.add(await TreeSitterTokenizationRegistry.getOrCreate('typescript') as (ITreeSitterTokenizationSupport & IDisposable)); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function tokensContentSize(tokens: TokenUpdate[]) { + return tokens[tokens.length - 1].startOffsetInclusive + tokens[tokens.length - 1].length; + } + + let nameNumber = 1; + async function getModelAndPrepTree(content: string) { + const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file(`file${nameNumber++}.ts`))); + const tree = disposables.add(await treeSitterParserService.getTextModelTreeSitter(model)); + const treeParseResult = new Promise(resolve => { + const disposable = treeSitterParserService.onDidUpdateTree(e => { + if (e.textModel === model) { + disposable.dispose(); + resolve(); + } + }); + }); + await tree.parse(); + await treeParseResult; + + assert.ok(tree); + return model; + } + + function verifyTokens(tokens: TokenUpdate[] | undefined) { + assert.ok(tokens); + for (let i = 1; i < tokens.length; i++) { + const previousToken: TokenUpdate = tokens[i - 1]; + const token: TokenUpdate = tokens[i]; + assert.deepStrictEqual(previousToken.startOffsetInclusive + previousToken.length, token.startOffsetInclusive); + } + } + + test('File single line file', async () => { + const content = `console.log('x');`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 1, 18), 0, 17); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 7); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with new lines at beginning and end', async () => { + const content = ` +console.log('x'); +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 3, 1), 0, 19); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 9); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with new lines at beginning and end \\r\\n', async () => { + const content = '\r\nconsole.log(\'x\');\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 3, 1), 0, 21); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 9); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with empty lines in the middle', async () => { + const content = ` +console.log('x'); + +console.log('7'); +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 38); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 17); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with empty lines in the middle \\r\\n', async () => { + const content = '\r\nconsole.log(\'x\');\r\n\r\nconsole.log(\'7\');\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 42); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 17); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with non-empty lines that match no scopes', async () => { + const content = `console.log('x'); +; +{ +} +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 24); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with non-empty lines that match no scopes \\r\\n', async () => { + const content = 'console.log(\'x\');\r\n;\r\n{\r\n}\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 28); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tree-sitter token that spans multiple lines', async () => { + const content = `/** +**/ + +console.log('x'); + +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 6, 1), 0, 28); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tree-sitter token that spans multiple lines \\r\\n', async () => { + const content = '/**\r\n**/\r\n\r\nconsole.log(\'x\');\r\n\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 6, 1), 0, 33); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tabs', async () => { + const content = `function x() { + return true; +} + +class Y { + private z = false; +}`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 7, 1), 0, 63); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 22); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tabs \\r\\n', async () => { + const content = 'function x() {\r\n\treturn true;\r\n}\r\n\r\nclass Y {\r\n\tprivate z = false;\r\n}'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 7, 1), 0, 69); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 22); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('Three changes come back to back ', async () => { + const content = `/** +**/ +class x { +} + + + + +class y { +}`; + const model = await getModelAndPrepTree(content); + + let updateListener: IDisposable | undefined; + let change: TreeUpdateEvent | undefined; + + const updatePromise = new Promise(resolve => { + updateListener = treeSitterParserService.onDidUpdateTree(async e => { + change = e; + resolve(); + }); + }); + + const edit1 = new Promise(resolve => { + model.applyEdits([{ range: new Range(7, 1, 8, 1), text: '' }]); + resolve(); + }); + const edit2 = new Promise(resolve => { + model.applyEdits([{ range: new Range(6, 1, 7, 1), text: '' }]); + resolve(); + }); + const edit3 = new Promise(resolve => { + model.applyEdits([{ range: new Range(5, 1, 6, 1), text: '' }]); + resolve(); + }); + Promise.all([edit1, edit2, edit3]); + await updatePromise; + assert.ok(change); + + assert.strictEqual(change.versionId, 4); + assert.strictEqual(change.ranges[0].newRangeStartOffset, 7); + assert.strictEqual(change.ranges[0].newRangeEndOffset, 32); + assert.strictEqual(change.ranges[0].newRange.startLineNumber, 2); + assert.strictEqual(change.ranges[0].newRange.endLineNumber, 7); + assert.strictEqual(change.ranges[0].oldRangeLength, 28); + + updateListener?.dispose(); + modelService.destroyModel(model.uri); + }); +}); diff --git a/code/src/vs/workbench/workbench.common.main.ts b/code/src/vs/workbench/workbench.common.main.ts index 054f3e75a6d..f43004533d7 100644 --- a/code/src/vs/workbench/workbench.common.main.ts +++ b/code/src/vs/workbench/workbench.common.main.ts @@ -246,9 +246,6 @@ import './contrib/mergeEditor/browser/mergeEditor.contribution.js'; // Multi Diff Editor import './contrib/multiDiffEditor/browser/multiDiffEditor.contribution.js'; -// Mapped Edits -import './contrib/mappedEdits/common/mappedEdits.contribution.js'; - // Commands import './contrib/commands/common/commands.contribution.js'; @@ -272,7 +269,6 @@ import './contrib/extensions/browser/extensions.contribution.js'; import './contrib/extensions/browser/extensionsViewlet.js'; // Output View -import './contrib/output/common/outputChannelModelService.js'; import './contrib/output/browser/output.contribution.js'; import './contrib/output/browser/outputView.js'; diff --git a/code/src/vs/workbench/workbench.desktop.main.ts b/code/src/vs/workbench/workbench.desktop.main.ts index 755d3c95beb..6cb01c72874 100644 --- a/code/src/vs/workbench/workbench.desktop.main.ts +++ b/code/src/vs/workbench/workbench.desktop.main.ts @@ -127,9 +127,6 @@ import './contrib/remote/electron-sandbox/remote.contribution.js'; // Configuration Exporter import './contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.js'; -// Output View -import './contrib/output/electron-sandbox/output.contribution.js'; - // Terminal import './contrib/terminal/electron-sandbox/terminal.contribution.js'; diff --git a/code/src/vscode-dts/vscode.d.ts b/code/src/vscode-dts/vscode.d.ts index f306830ba12..804721590e9 100644 --- a/code/src/vscode-dts/vscode.d.ts +++ b/code/src/vscode-dts/vscode.d.ts @@ -5496,7 +5496,7 @@ declare module 'vscode' { */ export enum InlayHintKind { /** - * An inlay hint that for a type annotation. + * An inlay hint that is for a type annotation. */ Type = 1, /** @@ -14869,22 +14869,41 @@ declare module 'vscode' { /** * Registers a new {@link DocumentDropEditProvider}. * + * Multiple drop providers can be registered for a language. When dropping content into an editor, all + * registered providers for the editor's language will be invoked based on the mimetypes they handle + * as specified by their {@linkcode DocumentDropEditProviderMetadata}. + * + * Each provider can return one or more {@linkcode DocumentDropEdit DocumentDropEdits}. The edits are sorted + * using the {@linkcode DocumentDropEdit.yieldTo} property. By default the first edit will be applied. If there + * are any additional edits, these will be shown to the user as selectable drop options in the drop widget. + * * @param selector A selector that defines the documents this provider applies to. * @param provider A drop provider. * @param metadata Additional metadata about the provider. * - * @returns A {@link Disposable} that unregisters this provider when disposed of. + * @returns A {@linkcode Disposable} that unregisters this provider when disposed of. */ export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider, metadata?: DocumentDropEditProviderMetadata): Disposable; /** * Registers a new {@linkcode DocumentPasteEditProvider}. * + * Multiple providers can be registered for a language. All registered providers for a language will be invoked + * for copy and paste operations based on their handled mimetypes as specified by the {@linkcode DocumentPasteProviderMetadata}. + * + * For {@link DocumentPasteEditProvider.prepareDocumentPaste copy operations}, changes to the {@linkcode DataTransfer} + * made by each provider will be merged into a single {@linkcode DataTransfer} that is used to populate the clipboard. + * + * For {@link DocumentPasteEditProvider.providerDocumentPasteEdits paste operations}, each provider will be invoked + * and can return one or more {@linkcode DocumentPasteEdit DocumentPasteEdits}. The edits are sorted using + * the {@linkcode DocumentPasteEdit.yieldTo} property. By default the first edit will be applied + * and the rest of the edits will be shown to the user as selectable paste options in the paste widget. + * * @param selector A selector that defines the documents this provider applies to. * @param provider A paste editor provider. * @param metadata Additional metadata about the provider. * - * @returns A {@link Disposable} that unregisters this provider when disposed of. + * @returns A {@linkcode Disposable} that unregisters this provider when disposed of. */ export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider, metadata: DocumentPasteProviderMetadata): Disposable; diff --git a/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 1be58f20bfb..374ff087cc1 100644 --- a/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -240,6 +240,17 @@ declare module 'vscode' { * Provide a set of variables that can only be used with this participant. */ participantVariableProvider?: { provider: ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; + + /** + * Event that fires when a request is paused or unpaused. + * Chat requests are initialy unpaused in the {@link requestHandler}. + */ + onDidChangePauseState: Event; + } + + export interface ChatParticipantPauseStateEvent { + request: ChatRequest; + isPaused: boolean; } export interface ChatParticipantCompletionItemProvider { diff --git a/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index d124c4dde5a..0aff2d2091b 100644 --- a/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -118,4 +118,13 @@ declare module 'vscode' { export interface LanguageModelIgnoredFileProvider { provideFileIgnored(uri: Uri, token: CancellationToken): ProviderResult; } + + export interface LanguageModelToolInvocationOptions { + chatRequestId?: string; + } + + export interface PreparedToolInvocation { + pastTenseMessage?: string | MarkdownString; + tooltip?: string | MarkdownString; + } } diff --git a/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 851a0f2ad03..68dda301e7d 100644 --- a/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -68,6 +68,10 @@ declare module 'vscode' { // TODO@API maybe an enum, LanguageModelChatProviderPickerAvailability? readonly isDefault?: boolean; readonly isUserSelectable?: boolean; + readonly capabilities?: { + readonly vision?: boolean; + readonly toolCalling?: boolean; + }; } export interface ChatResponseProviderMetadata { diff --git a/code/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts b/code/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts new file mode 100644 index 00000000000..0d91b3ed4e7 --- /dev/null +++ b/code/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +declare module 'vscode' { + + export interface ChatPromptReference { + /** + * When true, the user has indicated at the reference is informational only. + * The model should avoid changing or suggesting changes to the reference. + */ + readonly isReadonly?: boolean; + } + +} diff --git a/code/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts b/code/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts index 72a193d368e..ec10006fbe6 100644 --- a/code/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts @@ -24,6 +24,11 @@ declare module 'vscode' { */ data(): Thenable; + /** + * + */ + readonly reference?: Uri; + /** * @param mimeType The MIME type of the binary data. * @param data The binary data of the reference. diff --git a/code/src/vscode-dts/vscode.proposed.inlineEdit.d.ts b/code/src/vscode-dts/vscode.proposed.inlineEdit.d.ts index 12e87cab4cf..ad6fe45dbec 100644 --- a/code/src/vscode-dts/vscode.proposed.inlineEdit.d.ts +++ b/code/src/vscode-dts/vscode.proposed.inlineEdit.d.ts @@ -71,6 +71,9 @@ declare module 'vscode' { } export interface InlineEditProvider { + + readonly displayName?: string; + /** * Provide inline edit for the given document. * diff --git a/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 04d95579f4d..ac72b52ff37 100644 --- a/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -5,18 +5,27 @@ declare module 'vscode' { + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface DocumentContextItem { readonly uri: Uri; readonly version: number; readonly ranges: Range[]; } + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface ConversationRequest { // eslint-disable-next-line local/vscode-dts-string-type-literals readonly type: 'request'; readonly message: string; } + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface ConversationResponse { // eslint-disable-next-line local/vscode-dts-string-type-literals readonly type: 'response'; @@ -25,6 +34,9 @@ declare module 'vscode' { readonly references?: DocumentContextItem[]; } + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface MappedEditsContext { readonly documents: DocumentContextItem[][]; /** @@ -36,6 +48,7 @@ declare module 'vscode' { /** * Interface for providing mapped edits for a given document. + * @deprecated Use `MappedEditsProvider2` instead. */ export interface MappedEditsProvider { /** @@ -55,9 +68,13 @@ declare module 'vscode' { ): ProviderResult; } + /** + * Interface for providing mapped edits for a given document. + */ export interface MappedEditsRequest { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; - readonly conversation: Array; // for every prior response that contains codeblocks, make sure we pass the code as well as the resources based on the reported codemapper URIs + readonly location?: string; + readonly chatRequestId?: string; } export interface MappedEditsResponseStream { @@ -80,6 +97,9 @@ declare module 'vscode' { } namespace chat { + /** + * @deprecated Use `MappedEditsProvider2` instead. + */ export function registerMappedEditsProvider(documentSelector: DocumentSelector, provider: MappedEditsProvider): Disposable; export function registerMappedEditsProvider2(provider: MappedEditsProvider2): Disposable; diff --git a/code/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/code/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 8ab6b7cbd66..9eedd16d767 100644 --- a/code/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -48,10 +48,12 @@ declare module 'vscode' { export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; + readonly subject: string; readonly message: string; readonly displayId?: string; readonly author?: string; readonly authorEmail?: string; + readonly authorIcon?: IconPath; readonly timestamp?: number; readonly statistics?: SourceControlHistoryItemStatistics; readonly references?: SourceControlHistoryItemRef[]; @@ -63,14 +65,13 @@ declare module 'vscode' { readonly description?: string; readonly revision?: string; readonly category?: string; - readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + readonly icon?: IconPath; } export interface SourceControlHistoryItemChange { readonly uri: Uri; readonly originalUri: Uri | undefined; readonly modifiedUri: Uri | undefined; - readonly renameUri: Uri | undefined; } export interface SourceControlHistoryItemRefsChangeEvent { diff --git a/code/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts b/code/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts new file mode 100644 index 00000000000..5db0495b5a7 --- /dev/null +++ b/code/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/234339 + + export interface StatusBarItem { + + /** + * The tooltip text when you hover over this entry. + * + * Can optionally return the tooltip in a thenable if the computation is expensive. + */ + tooltip2: string | MarkdownString | undefined | ((token: CancellationToken) => ProviderResult); + } +} diff --git a/code/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts b/code/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts index 095fdcc867a..90e922f9304 100644 --- a/code/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts @@ -40,6 +40,12 @@ declare module 'vscode' { */ detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | MarkdownString; + /** * The completion's kind. Note that this will map to an icon. */ @@ -122,5 +128,9 @@ declare module 'vscode' { * The path separator to use when constructing paths. */ pathSeparator: string; + /** + * Environment variables to use when constructing paths. + */ + env?: { [key: string]: string | null | undefined }; } } diff --git a/code/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts b/code/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts new file mode 100644 index 00000000000..965d48e164b --- /dev/null +++ b/code/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // @anthonykim1 @tyriar https://github.com/microsoft/vscode/issues/227467 + + export interface TerminalShellIntegration { + /** + * The environment of the shell process. This is undefined if the shell integration script + * does not send the environment. + */ + readonly env: { [key: string]: string | undefined } | undefined; + } + + // TODO: Is it fine that this shares onDidChangeTerminalShellIntegration with cwd and the shellIntegration object itself? +} diff --git a/code/src/vscode-dts/vscode.proposed.terminalShellType.d.ts b/code/src/vscode-dts/vscode.proposed.terminalShellType.d.ts new file mode 100644 index 00000000000..e76defc6568 --- /dev/null +++ b/code/src/vscode-dts/vscode.proposed.terminalShellType.d.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/230165 + + /** + * Known terminal shell types. + */ + export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 + } + + // Part of TerminalState since the shellType can change multiple times and this comes with an event. + export interface TerminalState { + /** + * The current detected shell type of the terminal. New shell types may be added in the + * future in which case they will be returned as a number that is not part of + * {@link TerminalShellType}. + * Includes number type to prevent the breaking change when new enum members are added? + */ + readonly shellType?: TerminalShellType | number | undefined; + } + +} diff --git a/code/test/automation/src/code.ts b/code/test/automation/src/code.ts index a925cdd65bc..b297dd50a65 100644 --- a/code/test/automation/src/code.ts +++ b/code/test/automation/src/code.ts @@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; +import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -26,8 +27,10 @@ export interface LaunchOptions { readonly remote?: boolean; readonly web?: boolean; readonly tracing?: boolean; + snapshots?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly quality: Quality; } interface ICodeInstance { @@ -77,7 +80,7 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess); + return new Code(driver, options.logger, serverProcess, options.quality); } // Electron smoke tests (playwright) @@ -85,7 +88,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess); + return new Code(driver, options.logger, electronProcess, options.quality); } } @@ -96,7 +99,8 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + private readonly mainProcess: cp.ChildProcess, + readonly quality: Quality ) { this.driver = new Proxy(driver, { get(target, prop) { @@ -242,6 +246,10 @@ export class Code { await this.poll(() => this.driver.typeInEditor(selector, text), () => true, `type in editor '${selector}'`); } + async waitForEditorSelection(selector: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise { + await this.poll(() => this.driver.getEditorSelection(selector), accept, `get editor selection '${selector}'`); + } + async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise { await this.poll(() => this.driver.getTerminalBuffer(selector), accept, `get terminal buffer '${selector}'`); } diff --git a/code/test/automation/src/debug.ts b/code/test/automation/src/debug.ts index b7b7d427f4b..e2e227fc35e 100644 --- a/code/test/automation/src/debug.ts +++ b/code/test/automation/src/debug.ts @@ -9,6 +9,7 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; +const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -127,8 +129,9 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - await this.code.waitForActiveElement(REPL_FOCUSED); - await this.code.waitForSetValue(REPL_FOCUSED, text); + const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; + await this.code.waitForActiveElement(selector); + await this.code.waitForSetValue(selector, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/code/test/automation/src/editor.ts b/code/test/automation/src/editor.ts index 538866bfc06..9f5a9c90170 100644 --- a/code/test/automation/src/editor.ts +++ b/code/test/automation/src/editor.ts @@ -6,6 +6,7 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; +import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -78,10 +79,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const textarea = `${editor} textarea`; + const editContext = `${editor} ${this._editContextSelector()}`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(textarea); + await this.code.waitForActiveElement(editContext); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -92,14 +93,23 @@ export class Editor { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this._editContextSelector()}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } + async waitForEditorSelection(filename: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise { + const selector = `${EDITOR(filename)} ${this._editContextSelector()}`; + await this.code.waitForEditorSelection(selector, accept); + } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; + } + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/code/test/automation/src/editors.ts b/code/test/automation/src/editors.ts index b3a914ffff0..472385c8534 100644 --- a/code/test/automation/src/editors.ts +++ b/code/test/automation/src/editors.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -53,7 +54,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/code/test/automation/src/extensions.ts b/code/test/automation/src/extensions.ts index 2a481f9fe76..3713faa6700 100644 --- a/code/test/automation/src/extensions.ts +++ b/code/test/automation/src/extensions.ts @@ -8,6 +8,7 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; +import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -20,7 +21,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); + await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; @@ -51,8 +52,22 @@ export class Extensions extends Viewlet { async installExtension(id: string, waitUntilEnabled: boolean): Promise { await this.searchForExtension(id); - await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"] .extension-list-item .monaco-action-bar .action-item:not(.disabled) .extension-action.install`); - await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`); + + // try to install extension 3 times + let attempt = 1; + while (true) { + await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"] .extension-list-item .monaco-action-bar .action-item:not(.disabled) .extension-action.install`); + + try { + await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`); + break; + } catch (err) { + if (attempt++ === 3) { + throw err; + } + } + } + if (waitUntilEnabled) { await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) a[aria-label="Disable this extension"]`); } diff --git a/code/test/automation/src/notebook.ts b/code/test/automation/src/notebook.ts index dff250027db..cd46cbdb0dd 100644 --- a/code/test/automation/src/notebook.ts +++ b/code/test/automation/src/notebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -46,10 +47,10 @@ export class Notebook { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/code/test/automation/src/playwrightBrowser.ts b/code/test/automation/src/playwrightBrowser.ts index 1d65537db4d..044da21bc9f 100644 --- a/code/test/automation/src/playwrightBrowser.ts +++ b/code/test/automation/src/playwrightBrowser.ts @@ -88,7 +88,7 @@ async function launchServer(options: LaunchOptions) { } async function launchBrowser(options: LaunchOptions, endpoint: string) { - const { logger, workspacePath, tracing, headless } = options; + const { logger, workspacePath, tracing, snapshots, headless } = options; const browser = await measureAndLog(() => playwright[options.browser ?? 'chromium'].launch({ headless: headless ?? false, @@ -102,7 +102,7 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) { if (tracing) { try { - await measureAndLog(() => context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger); } catch (error) { logger.log(`Playwright (Browser): Failed to start playwright tracing (${error})`); // do not fail the build when this fails } diff --git a/code/test/automation/src/playwrightDriver.ts b/code/test/automation/src/playwrightDriver.ts index 681ca4ef540..dbde9955766 100644 --- a/code/test/automation/src/playwrightDriver.ts +++ b/code/test/automation/src/playwrightDriver.ts @@ -288,6 +288,10 @@ export class PlaywrightDriver { return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const); } + async getEditorSelection(selector: string) { + return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const); + } + async getTerminalBuffer(selector: string) { return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const); } diff --git a/code/test/automation/src/playwrightElectron.ts b/code/test/automation/src/playwrightElectron.ts index 660320be235..41eb4ec4150 100644 --- a/code/test/automation/src/playwrightElectron.ts +++ b/code/test/automation/src/playwrightElectron.ts @@ -27,7 +27,7 @@ export async function launch(options: LaunchOptions): Promise<{ electronProcess: } async function launchElectron(configuration: IElectronConfiguration, options: LaunchOptions) { - const { logger, tracing } = options; + const { logger, tracing, snapshots } = options; const electron = await measureAndLog(() => playwright._electron.launch({ executablePath: configuration.electronPath, @@ -45,7 +45,7 @@ async function launchElectron(configuration: IElectronConfiguration, options: La if (tracing) { try { - await measureAndLog(() => context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger); } catch (error) { logger.log(`Playwright (Electron): Failed to start playwright tracing (${error})`); // do not fail the build when this fails } diff --git a/code/test/automation/src/scm.ts b/code/test/automation/src/scm.ts index 9f950f2b16a..6489badbe8a 100644 --- a/code/test/automation/src/scm.ts +++ b/code/test/automation/src/scm.ts @@ -6,9 +6,11 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; +const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -44,7 +46,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(SCM_INPUT); + await this.code.waitForElement(this._editContextSelector()); } async waitForChange(name: string, type?: string): Promise { @@ -71,9 +73,13 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(SCM_INPUT); - await this.code.waitForActiveElement(SCM_INPUT); - await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForSetValue(this._editContextSelector(), message); await this.code.waitAndClick(COMMIT_COMMAND); } + + private _editContextSelector(): string { + return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; + } } diff --git a/code/test/automation/src/settings.ts b/code/test/automation/src/settings.ts index 68401eb0eda..a7a40ece431 100644 --- a/code/test/automation/src/settings.ts +++ b/code/test/automation/src/settings.ts @@ -7,8 +7,10 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { Quality } from './application'; -const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; +const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -23,6 +25,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); + await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1); await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`); await this.editors.saveOpenedFile(); } @@ -37,6 +40,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); + await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1); await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join('')); await this.editors.saveOpenedFile(); } @@ -45,6 +49,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.quickaccess.runCommand('editor.action.selectAll'); await this.code.dispatchKeybinding('Delete'); + await this.editor.waitForEditorContents('settings.json', contents => contents === ''); await this.editor.waitForTypeInEditor('settings.json', `{`); // will auto close } await this.editors.saveOpenedFile(); await this.quickaccess.runCommand('workbench.action.closeActiveEditor'); @@ -57,13 +62,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForActiveElement(this._editContextSelector()); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitAndClick(this._editContextSelector()); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -71,7 +76,11 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); + await this.code.waitForTypeInEditor(this._editContextSelector(), query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; + } } diff --git a/code/test/smoke/src/areas/extensions/extensions.test.ts b/code/test/smoke/src/areas/extensions/extensions.test.ts index c78cbe87089..c20700cbc91 100644 --- a/code/test/smoke/src/areas/extensions/extensions.test.ts +++ b/code/test/smoke/src/areas/extensions/extensions.test.ts @@ -10,7 +10,10 @@ export function setup(logger: Logger) { describe('Extensions', () => { // Shared before/after handling - installAllHandlers(logger); + installAllHandlers(logger, opts => { + opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension + return opts; + }); it('install and enable vscode-smoketest-check extension', async function () { const app = this.app as Application; diff --git a/code/test/smoke/src/areas/workbench/localization.test.ts b/code/test/smoke/src/areas/workbench/localization.test.ts index 12e49ce549e..c3ce9b04fc2 100644 --- a/code/test/smoke/src/areas/workbench/localization.test.ts +++ b/code/test/smoke/src/areas/workbench/localization.test.ts @@ -9,8 +9,12 @@ import { installAllHandlers } from '../../utils'; export function setup(logger: Logger) { describe('Localization', () => { + // Shared before/after handling - installAllHandlers(logger); + installAllHandlers(logger, opts => { + opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension + return opts; + }); it('starts with "DE" locale and verifies title and viewlets text is in German', async function () { const app = this.app as Application; diff --git a/code/test/unit/electron/renderer.js b/code/test/unit/electron/renderer.js index b93d91a78e9..033667df0aa 100644 --- a/code/test/unit/electron/renderer.js +++ b/code/test/unit/electron/renderer.js @@ -294,10 +294,12 @@ async function loadTests(opts) { // should not have unexpected errors const errors = _unexpectedErrors.concat(_loaderErrors); if (errors.length) { + const msg = []; for (const error of errors) { console.error(`Error: Test run should not have unexpected errors:\n${error}`); + msg.push(String(error)) } - assert.ok(false, 'Error: Test run should not have unexpected errors.'); + assert.ok(false, `Error: Test run should not have unexpected errors:\n${msg.join('\n')}`); } });